import type { GatewayService } from "../../daemon/service.js"; import { resolveIsNixMode } from "../../config/paths.js"; import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js"; import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js"; import { isWSL } from "../../infra/wsl.js"; import { defaultRuntime } from "../../runtime.js"; import { buildDaemonServiceSnapshot, createNullWriter, type DaemonAction, emitDaemonActionJson, } from "./response.js"; type DaemonLifecycleOptions = { json?: boolean; }; async function maybeAugmentSystemdHints(hints: string[]): Promise { if (process.platform !== "linux") { return hints; } const systemdAvailable = await isSystemdUserServiceAvailable().catch(() => false); if (systemdAvailable) { return hints; } return [...hints, ...renderSystemdUnavailableHints({ wsl: await isWSL() })]; } function createActionIO(params: { action: DaemonAction; json: boolean }) { const stdout = params.json ? createNullWriter() : process.stdout; const emit = (payload: { ok: boolean; result?: string; message?: string; error?: string; hints?: string[]; service?: { label: string; loaded: boolean; loadedText: string; notLoadedText: string; }; }) => { if (!params.json) { return; } emitDaemonActionJson({ action: params.action, ...payload }); }; const fail = (message: string, hints?: string[]) => { if (params.json) { emit({ ok: false, error: message, hints }); } else { defaultRuntime.error(message); } defaultRuntime.exit(1); }; return { stdout, emit, fail }; } export async function runServiceUninstall(params: { serviceNoun: string; service: GatewayService; opts?: DaemonLifecycleOptions; stopBeforeUninstall: boolean; assertNotLoadedAfterUninstall: boolean; }) { const json = Boolean(params.opts?.json); const { stdout, emit, fail } = createActionIO({ action: "uninstall", json }); if (resolveIsNixMode(process.env)) { fail("Nix mode detected; service uninstall is disabled."); return; } let loaded = false; try { loaded = await params.service.isLoaded({ env: process.env }); } catch { loaded = false; } if (loaded && params.stopBeforeUninstall) { try { await params.service.stop({ env: process.env, stdout }); } catch { // Best-effort stop; final loaded check gates success when enabled. } } try { await params.service.uninstall({ env: process.env, stdout }); } catch (err) { fail(`${params.serviceNoun} uninstall failed: ${String(err)}`); return; } loaded = false; try { loaded = await params.service.isLoaded({ env: process.env }); } catch { loaded = false; } if (loaded && params.assertNotLoadedAfterUninstall) { fail(`${params.serviceNoun} service still loaded after uninstall.`); return; } emit({ ok: true, result: "uninstalled", service: buildDaemonServiceSnapshot(params.service, loaded), }); } export async function runServiceStart(params: { serviceNoun: string; service: GatewayService; renderStartHints: () => string[]; opts?: DaemonLifecycleOptions; }) { const json = Boolean(params.opts?.json); const { stdout, emit, fail } = createActionIO({ action: "start", json }); let loaded = false; try { loaded = await params.service.isLoaded({ env: process.env }); } catch (err) { fail(`${params.serviceNoun} service check failed: ${String(err)}`); return; } if (!loaded) { const hints = await maybeAugmentSystemdHints(params.renderStartHints()); emit({ ok: true, result: "not-loaded", message: `${params.serviceNoun} service ${params.service.notLoadedText}.`, hints, service: buildDaemonServiceSnapshot(params.service, loaded), }); if (!json) { defaultRuntime.log(`${params.serviceNoun} service ${params.service.notLoadedText}.`); for (const hint of hints) { defaultRuntime.log(`Start with: ${hint}`); } } return; } try { await params.service.restart({ env: process.env, stdout }); } catch (err) { const hints = params.renderStartHints(); fail(`${params.serviceNoun} start failed: ${String(err)}`, hints); return; } let started = true; try { started = await params.service.isLoaded({ env: process.env }); } catch { started = true; } emit({ ok: true, result: "started", service: buildDaemonServiceSnapshot(params.service, started), }); } export async function runServiceStop(params: { serviceNoun: string; service: GatewayService; opts?: DaemonLifecycleOptions; }) { const json = Boolean(params.opts?.json); const { stdout, emit, fail } = createActionIO({ action: "stop", json }); let loaded = false; try { loaded = await params.service.isLoaded({ env: process.env }); } catch (err) { fail(`${params.serviceNoun} service check failed: ${String(err)}`); return; } if (!loaded) { emit({ ok: true, result: "not-loaded", message: `${params.serviceNoun} service ${params.service.notLoadedText}.`, service: buildDaemonServiceSnapshot(params.service, loaded), }); if (!json) { defaultRuntime.log(`${params.serviceNoun} service ${params.service.notLoadedText}.`); } return; } try { await params.service.stop({ env: process.env, stdout }); } catch (err) { fail(`${params.serviceNoun} stop failed: ${String(err)}`); return; } let stopped = false; try { stopped = await params.service.isLoaded({ env: process.env }); } catch { stopped = false; } emit({ ok: true, result: "stopped", service: buildDaemonServiceSnapshot(params.service, stopped), }); } export async function runServiceRestart(params: { serviceNoun: string; service: GatewayService; renderStartHints: () => string[]; opts?: DaemonLifecycleOptions; }): Promise { const json = Boolean(params.opts?.json); const { stdout, emit, fail } = createActionIO({ action: "restart", json }); let loaded = false; try { loaded = await params.service.isLoaded({ env: process.env }); } catch (err) { fail(`${params.serviceNoun} service check failed: ${String(err)}`); return false; } if (!loaded) { const hints = await maybeAugmentSystemdHints(params.renderStartHints()); emit({ ok: true, result: "not-loaded", message: `${params.serviceNoun} service ${params.service.notLoadedText}.`, hints, service: buildDaemonServiceSnapshot(params.service, loaded), }); if (!json) { defaultRuntime.log(`${params.serviceNoun} service ${params.service.notLoadedText}.`); for (const hint of hints) { defaultRuntime.log(`Start with: ${hint}`); } } return false; } try { await params.service.restart({ env: process.env, stdout }); let restarted = true; try { restarted = await params.service.isLoaded({ env: process.env }); } catch { restarted = true; } emit({ ok: true, result: "restarted", service: buildDaemonServiceSnapshot(params.service, restarted), }); return true; } catch (err) { const hints = params.renderStartHints(); fail(`${params.serviceNoun} restart failed: ${String(err)}`, hints); return false; } }