Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions lib/mk-vm-apps.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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"; };
}
)
Expand Down
55 changes: 55 additions & 0 deletions lib/mk-vm-runner.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
'';
}