Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
repositories/aaf-holdings/hq01/lib/process/signal.ts
2.6 KB
/**
* Process signalling primitives for the HQ01 Session Manager.
*
* These helpers wrap the POSIX semantics of `process.kill` so the rest of the
* code can reason about a child Claude CLI process in plain terms: is it alive,
* and please stop it. They are intentionally tiny and have no knowledge of
* sessions, the filesystem, or HTTP — they only know about PIDs and signals.
*/
/**
* Returns true if a process with the given PID is currently running.
*
* `process.kill(pid, 0)` sends no signal but performs the same permission and
* existence checks the kernel would for a real signal:
* - success → the process exists and we may signal it.
* - EPERM → the process exists but is owned by another user.
* - ESRCH/ENOENT → no such process.
*/
export function isAlive(pid: number | null | undefined): boolean {
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
return false;
}
try {
process.kill(pid, 0);
return true;
} catch (err) {
const code = (err as NodeJS.ErrnoException)?.code;
// EPERM means the process is alive but not ours to signal.
return code === "EPERM";
}
}
/** The outcome of a {@link terminate} call. */
export type TerminateResult = "gone" | "stopped" | "killed";
/**
* Gracefully terminate a process.
*
* Sends SIGTERM and waits up to `graceMs` for the process to exit on its own.
* If it is still alive after the grace period, escalates to SIGKILL. Polling is
* used rather than waiting on the child handle because the session manager runs
* in a different process than the one that spawned the child (the child is
* detached and survives request boundaries).
*
* - "gone" → the process was already dead.
* - "stopped" → exited cleanly after SIGTERM.
* - "killed" → required SIGKILL.
*/
export async function terminate(
pid: number | null | undefined,
{ graceMs = 5000, pollMs = 200 }: { graceMs?: number; pollMs?: number } = {},
): Promise<TerminateResult> {
if (!isAlive(pid)) return "gone";
const target = pid as number;
try {
process.kill(target, "SIGTERM");
} catch {
// Raced with the process exiting — treat as gone.
if (!isAlive(target)) return "gone";
}
const deadline = Date.now() + graceMs;
while (Date.now() < deadline) {
if (!isAlive(target)) return "stopped";
await sleep(pollMs);
}
if (!isAlive(target)) return "stopped";
// Still alive after the grace period — force it.
try {
process.kill(target, "SIGKILL");
} catch {
/* already gone */
}
return "killed";
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
root · /srv/aaf