first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user