From 04db65432cd2d10c103093b43f58bb968a3f7808 Mon Sep 17 00:00:00 2001 From: gotha Date: Sun, 28 Jun 2026 15:42:06 +0300 Subject: [PATCH] feat: add `vm-headless` app to run the VM detached in the background `nix run .#vm` keeps its current foreground behavior (serial console attached to the terminal). The new `nix run .#vm-headless` starts the VM fully detached and prints the SSH port to connect to, then returns control to the shell. - lib/mk-vm-runner.nix: new `background` runner that launches QEMU with `-daemonize -display none -monitor none`, captures the serial console to /tmp/-console.log, writes a pidfile, and prints the SSH command - lib/mk-vm-apps.nix: expose the runner as the `vm-headless` app - README: document the foreground / background / GUI run modes --- README.md | 19 ++++++++++++++- lib/mk-vm-apps.nix | 1 + lib/mk-vm-runner.nix | 55 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e94204..f661353 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,25 @@ Import agentbox into your own flake to create project-specific VMs. See the [exa After creating your `flake.nix`, run the VM with: ```bash -# Run VM in headless mode (recommended) +# Run VM in the foreground (serial console attached to your terminal) nix run .#vm + +# Run VM detached in the background; prints the SSH port and returns control +nix run .#vm-headless + +# Run VM with a graphical window +nix run .#vm-gui +``` + +`nix run .#vm-headless` daemonizes QEMU and prints the SSH command to connect, for example: + +```text +VM 'dev-vm' started in background. + + SSH: ssh dev@localhost -p 24817 + Port file: /tmp/dev-vm-ssh-port + Console log: /tmp/dev-vm-console.log + Stop: kill $(cat /tmp/dev-vm.pid) ``` The default credentials are: diff --git a/lib/mk-vm-apps.nix b/lib/mk-vm-apps.nix index cb7baed..71dbf26 100644 --- a/lib/mk-vm-apps.nix +++ b/lib/mk-vm-apps.nix @@ -27,6 +27,7 @@ forAllSystems (hostSystem: in { default = { type = "app"; program = "${vmRunner.headless}/bin/run-${vmName}"; }; vm = { type = "app"; program = "${vmRunner.headless}/bin/run-${vmName}"; }; + vm-headless = { type = "app"; program = "${vmRunner.background}/bin/run-${vmName}-headless"; }; vm-gui = { type = "app"; program = "${vmRunner.gui}/bin/run-${vmName}-gui"; }; } ) diff --git a/lib/mk-vm-runner.nix b/lib/mk-vm-runner.nix index 75546ed..914beb4 100644 --- a/lib/mk-vm-runner.nix +++ b/lib/mk-vm-runner.nix @@ -123,5 +123,60 @@ in echo "" exec ${vmDrv}/bin/run-*-vm $SHARE_ARGS $NET_ARGS "$@" ''; + + background = pkgs.writeShellScriptBin "run-${vmName}-headless" '' + CONSOLE_LOG="/tmp/${vmName}-console.log" + PID_FILE="/tmp/${vmName}.pid" + PORT_FILE="/tmp/${vmName}-ssh-port" + + # If a VM with this name is already running, don't try to start another one + # (QEMU would fail to lock the pidfile). Point the user at the running VM + # instead, and leave its recorded SSH port untouched. + if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then + echo "VM '${vmName}' is already running (PID $(cat "$PID_FILE"))." >&2 + if [ -s "$PORT_FILE" ]; then + echo "Connect with: ssh dev@localhost -p $(cat "$PORT_FILE")" >&2 + fi + echo "Stop it with: kill \$(cat $PID_FILE)" >&2 + exit 1 + fi + + # Remember the previously recorded port; commonScript overwrites PORT_FILE + # with a fresh random port below, so we can restore it if the launch fails. + PREV_PORT="$(cat "$PORT_FILE" 2>/dev/null || true)" + + ${commonScript} + + # Launch QEMU fully detached: no display, no monitor, serial captured to a + # log file, and qemu daemonizes itself while recording its PID for control. + if ! ${vmDrv}/bin/run-*-vm \ + -display none \ + -monitor none \ + -serial "file:$CONSOLE_LOG" \ + -pidfile "$PID_FILE" \ + -daemonize \ + $SHARE_ARGS $NET_ARGS "$@"; then + # Launch failed (commonScript already clobbered PORT_FILE) — restore the + # previous port so it keeps pointing at whatever VM is actually running. + if [ -n "$PREV_PORT" ]; then echo "$PREV_PORT" > "$PORT_FILE"; fi + echo "" >&2 + echo "Error: failed to start VM '${vmName}'." >&2 + echo "A VM with this name is probably already running." >&2 + if [ -n "$PREV_PORT" ]; then + echo "Try connecting to the existing one: ssh dev@localhost -p $PREV_PORT" >&2 + fi + echo "Find it with: pgrep -af '${vmName}'" >&2 + exit 1 + fi + + echo "" + echo "VM '${vmName}' started in background." + echo "" + echo " SSH: ssh dev@localhost -p $SSH_PORT" + echo " Port file: $PORT_FILE" + echo " Console log: $CONSOLE_LOG" + echo " Stop: kill \$(cat $PID_FILE)" + echo "" + ''; }