feat: add serial console access via PTY + Unix socket proxy

Each spawned clone now runs under a _console-proxy daemon that connects
firecracker's ttyS0 (stdin/stdout) to a PTY and serves it on a Unix
socket at clones/<id>/console.sock for the VM's lifetime.

  sudo ./fc-orch spawn 1
  sudo ./fc-orch console 1   # Ctrl+] to detach

spawnOne delegates VM startup to the proxy process (Setsid, detached)
and waits for console.sock to appear before returning. Kill continues
to work via PID files — proxy and firecracker PIDs are both recorded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 10:24:09 +00:00
parent 04067f7e6b
commit 9089cbdbe9
6 changed files with 437 additions and 97 deletions

22
main.go
View File

@@ -16,6 +16,7 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
@@ -79,6 +80,26 @@ func main() {
fatal(orch.Kill())
case "cleanup":
fatal(orch.Cleanup())
case "console":
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "usage: %s console <id>\n", os.Args[0])
os.Exit(1)
}
var id int
fmt.Sscanf(os.Args[2], "%d", &id)
fatal(orchestrator.ConnectConsole(orchestrator.DefaultConfig(), id))
case "_console-proxy":
// Internal subcommand: started by spawnOne, runs as a background daemon.
fs := flag.NewFlagSet("_console-proxy", flag.ContinueOnError)
var id int
var tap string
fs.IntVar(&id, "id", 0, "clone ID")
fs.StringVar(&tap, "tap", "", "TAP device name")
if err := fs.Parse(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "console-proxy: %v\n", err)
os.Exit(1)
}
fatal(orchestrator.RunConsoleProxy(orchestrator.DefaultConfig(), id, tap))
default:
usage()
os.Exit(1)
@@ -95,6 +116,7 @@ Commands:
init Download kernel + create Alpine rootfs
golden Boot golden VM → pause → snapshot
spawn [N] Restore N clones from golden snapshot (default: 1)
console <id> Attach to the serial console of a running clone (Ctrl+] to detach)
status Show running clones
kill Kill all running VMs
cleanup Kill VMs + remove all state