import type { Server } from "node:http"; import { SIDECAR_ENV, SIDECAR_MESSAGES, normalizeDaemonSidecarMessage, type DaemonStatusSnapshot, type SidecarStamp, } from "@open-design/sidecar-proto"; import { createJsonIpcServer, type JsonIpcServerHandle, type SidecarRuntimeContext, } from "@open-design/sidecar"; import { startServer } from "../src/server.js"; const DAEMON_PORT_ENV = SIDECAR_ENV.DAEMON_PORT; const TOOLS_DEV_PARENT_PID_ENV = SIDECAR_ENV.TOOLS_DEV_PARENT_PID; export type DaemonSidecarHandle = { status(): Promise; stop(): Promise; waitUntilStopped(): Promise; }; function parsePort(value: string | undefined): number { if (value == null || value.trim().length === 0) return 0; const port = Number(value); if (!Number.isInteger(port) || port < 0 || port > 65535) { throw new Error(`${DAEMON_PORT_ENV} must be an integer between 0 and 65535`); } return port; } async function closeHttpServer(server: Server): Promise { if (!server.listening) return; await new Promise((resolveClose, rejectClose) => { server.close((error) => (error == null ? resolveClose() : rejectClose(error))); }); } function isProcessAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } function attachParentMonitor(stop: () => Promise): void { const parentPid = Number(process.env[TOOLS_DEV_PARENT_PID_ENV]); if (!Number.isInteger(parentPid) || parentPid <= 0) return; const timer = setInterval(() => { if (isProcessAlive(parentPid)) return; clearInterval(timer); void stop().finally(() => process.exit(0)); }, 1000); timer.unref(); } export async function startDaemonSidecar(runtime: SidecarRuntimeContext): Promise { const started = await startServer({ port: parsePort(process.env[DAEMON_PORT_ENV]), returnServer: true }) as | string | { server: Server; url: string }; if (typeof started === "string") { throw new Error("daemon startServer did not return a server handle"); } const serverHandle = started; const state: DaemonStatusSnapshot = { pid: process.pid, state: "running", updatedAt: new Date().toISOString(), url: serverHandle.url, }; let ipcServer: JsonIpcServerHandle | null = null; let stopped = false; let resolveStopped!: () => void; const stoppedPromise = new Promise((resolveStop) => { resolveStopped = resolveStop; }); async function stop(): Promise { if (stopped) return; stopped = true; state.state = "stopped"; state.updatedAt = new Date().toISOString(); await ipcServer?.close().catch(() => undefined); await closeHttpServer(serverHandle.server).catch(() => undefined); resolveStopped(); } attachParentMonitor(stop); ipcServer = await createJsonIpcServer({ socketPath: runtime.ipc, handler: async (message: unknown) => { const request = normalizeDaemonSidecarMessage(message); switch (request.type) { case SIDECAR_MESSAGES.STATUS: return { ...state }; case SIDECAR_MESSAGES.SHUTDOWN: setImmediate(() => { void stop().finally(() => process.exit(0)); }); return { accepted: true }; } }, }); for (const signal of ["SIGINT", "SIGTERM"] as const) { process.on(signal, () => { void stop().finally(() => process.exit(0)); }); } return { async status() { return { ...state }; }, stop, waitUntilStopped() { return stoppedPromise; }, }; }