From 7cfd8fe1b80c5fe29713c1f7cb36a06b5e537009 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 17 Mar 2026 22:38:46 +0000 Subject: [PATCH] feat: daemonize bastion start, fix status for root-owned processes - `lab init bastion standalone start` now runs in background by default - `--foreground` flag for running in foreground (debugging/containers) - Shows startup output then detaches with PID + log path - Status command uses /proc check instead of kill -0 (works cross-user) Co-Authored-By: Claude Opus 4.6 (1M context) --- bastion/src/cli/src/commands/serve.ts | 83 ++++++++++++++++++++++---- bastion/src/cli/src/commands/status.ts | 6 +- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/bastion/src/cli/src/commands/serve.ts b/bastion/src/cli/src/commands/serve.ts index 4be6aaf..e960ea8 100644 --- a/bastion/src/cli/src/commands/serve.ts +++ b/bastion/src/cli/src/commands/serve.ts @@ -1,6 +1,8 @@ // CLI command: init bastion standalone start -// Start the bastion server (HTTP + dnsmasq). +// Start the bastion server (HTTP + dnsmasq), daemonized by default. +import { spawn, type ChildProcess } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; import type { Command } from "commander"; import { startBastion } from "@lab/bastion"; @@ -18,6 +20,7 @@ export function registerStartCommand(parent: Command): void { .option("--locale ", "Locale", "en_GB.UTF-8") .option("--skip-dnsmasq", "Skip starting dnsmasq (for testing)") .option("--skip-artifacts", "Skip downloading boot artifacts (for testing)") + .option("--foreground", "Run in foreground (default: daemonize)") .action(async (opts: { port: string; dir: string; @@ -29,18 +32,74 @@ export function registerStartCommand(parent: Command): void { locale: string; skipDnsmasq?: boolean; skipArtifacts?: boolean; + foreground?: boolean; }) => { - await startBastion({ - httpPort: parseInt(opts.port, 10), - bastionDir: opts.dir, - domain: opts.domain, - dhcpMode: opts.dhcpMode as "proxy" | "full", - fedoraVersion: opts.fedora, - arch: opts.arch, - timezone: opts.timezone, - locale: opts.locale, - skipDnsmasq: opts.skipDnsmasq, - skipArtifacts: opts.skipArtifacts, + if (opts.foreground === true) { + // Run in foreground + await startBastion({ + httpPort: parseInt(opts.port, 10), + bastionDir: opts.dir, + domain: opts.domain, + dhcpMode: opts.dhcpMode as "proxy" | "full", + fedoraVersion: opts.fedora, + arch: opts.arch, + timezone: opts.timezone, + locale: opts.locale, + skipDnsmasq: opts.skipDnsmasq, + skipArtifacts: opts.skipArtifacts, + }); + return; + } + + // Daemonize: spawn ourselves with --foreground and detach + const logFile = `${opts.dir}/bastion.log`; + const args = process.argv.slice(1); + // Add --foreground flag + args.push("--foreground"); + + const child: ChildProcess = spawn(process.argv[0] ?? "lab", args, { + detached: true, + stdio: ["ignore", "pipe", "pipe"], + }); + + // Collect initial output to confirm startup + let output = ""; + const timeout = setTimeout(() => { + child.stdout?.removeAllListeners(); + child.stderr?.removeAllListeners(); + child.unref(); + console.log(`Bastion starting in background (PID ${child.pid})`); + console.log(`Log: ${logFile}`); + process.exit(0); + }, 3000); + + child.stdout?.on("data", (data: Buffer) => { + output += data.toString(); + process.stdout.write(data); + if (output.includes("Waiting for PXE boot requests")) { + clearTimeout(timeout); + child.stdout?.removeAllListeners(); + child.stderr?.removeAllListeners(); + child.unref(); + + // Check PID file + const pidFile = `${opts.dir}/bastion.pid`; + const pid = existsSync(pidFile) ? readFileSync(pidFile, "utf-8").trim() : String(child.pid); + console.log(""); + console.log(`Bastion running in background (PID ${pid})`); + console.log(`Log: ${logFile}`); + process.exit(0); + } + }); + + child.stderr?.on("data", (data: Buffer) => { + process.stderr.write(data); + }); + + child.on("exit", (code) => { + clearTimeout(timeout); + console.error(`Bastion exited with code ${code}`); + process.exit(code ?? 1); }); }); } diff --git a/bastion/src/cli/src/commands/status.ts b/bastion/src/cli/src/commands/status.ts index 0583348..5134223 100644 --- a/bastion/src/cli/src/commands/status.ts +++ b/bastion/src/cli/src/commands/status.ts @@ -5,9 +5,13 @@ import { readFileSync, existsSync, statSync } from "node:fs"; import type { Command } from "commander"; import type { BastionState } from "@lab/shared"; +import { execSync } from "node:child_process"; + function isProcessAlive(pid: number): boolean { try { - process.kill(pid, 0); + // process.kill(pid, 0) fails for root-owned processes when run as non-root + // Use kill -0 which works across users, or check /proc + execSync(`kill -0 ${pid} 2>/dev/null || test -d /proc/${pid}`, { stdio: "pipe" }); return true; } catch { return false;