130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
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<DaemonStatusSnapshot>;
|
|
stop(): Promise<void>;
|
|
waitUntilStopped(): Promise<void>;
|
|
};
|
|
|
|
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<void> {
|
|
if (!server.listening) return;
|
|
await new Promise<void>((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>): 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<SidecarStamp>): Promise<DaemonSidecarHandle> {
|
|
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<void>((resolveStop) => {
|
|
resolveStopped = resolveStop;
|
|
});
|
|
|
|
async function stop(): Promise<void> {
|
|
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;
|
|
},
|
|
};
|
|
}
|