Make daemon shutdowns easier to diagnose (#1790)

* fix(daemon): log stop reasons and client identity

Record websocket client identity, process memory/uptime, and shutdown reasons across CLI, desktop, supervisor, and worker paths so daemon drops can be traced from the triggering client to worker termination.

* fix(daemon): keep shutdown diagnostics in sync

Test drift: supervisor and relay tests still asserted the old log text and metadata shape after shutdown diagnostics started logging structured reasons and relay connection ids. Update those assertions to the new diagnostic contract.

Also centralize client lifecycle reason normalization and derive desktop daemon stop reasons from one tuple so future changes cannot silently drift.
This commit is contained in:
Mohamed Boudra
2026-06-29 17:31:11 +02:00
committed by GitHub
parent b613bea9f6
commit e63a971968
27 changed files with 567 additions and 86 deletions

View File

@@ -169,6 +169,23 @@ describe("supervisor durable logging", () => {
expect(result.log).toContain("raw stderr line\n");
});
test("logs the worker shutdown reason before signaling the worker", async () => {
const result = await runSupervisorFixture({
workerSource: `
process.send?.({ type: "paseo:shutdown", reason: "client_shutdown_rpc" });
setInterval(() => {}, 1000);
`,
});
expect(result.code).toBe(0);
expect(result.signal).toBeNull();
expect(result.log).toContain('"msg":"Worker requested shutdown"');
expect(result.log).toContain('"reason":"client_shutdown_rpc"');
expect(result.log).toContain('"msg":"Supervisor sending signal to worker"');
expect(result.log).toContain('"signal":"SIGTERM"');
expect(result.log).toContain('"workerPid":');
});
// POSIX-only: Windows reports the worker self-kill as an exit code, not SIGKILL.
test.skipIf(isPlatform("win32"))(
"logs worker signal exits even when the worker cannot log",

View File

@@ -14,6 +14,7 @@ interface SupervisorLogFileOptions {
type WorkerLifecycleMessage =
| {
type: "paseo:shutdown";
reason?: string;
}
| {
type: "paseo:ready";
@@ -56,7 +57,11 @@ function parseLifecycleMessage(msg: unknown): WorkerLifecycleMessage | null {
}
const type = (msg as { type?: unknown }).type;
if (type === "paseo:shutdown") {
return { type: "paseo:shutdown" };
const reason = (msg as { reason?: unknown }).reason;
return {
type: "paseo:shutdown",
...(typeof reason === "string" && reason.trim().length > 0 ? { reason } : {}),
};
}
if (type === "paseo:ready") {
const listen = (msg as { listen?: unknown }).listen;
@@ -236,16 +241,15 @@ export function runSupervisor(options: SupervisorOptions): void {
}
if (lifecycleMessage.type === "paseo:shutdown") {
writeLifecycleLog("Worker requested shutdown");
requestShutdown("Shutdown requested by worker");
const reason = lifecycleMessage.reason ?? "worker_requested_shutdown";
writeLifecycleLog("Worker requested shutdown", { reason });
requestShutdown(reason);
return;
}
writeLifecycleLog(
"Worker requested restart",
lifecycleMessage.reason ? { reason: lifecycleMessage.reason } : {},
);
requestRestart("Restart requested by worker");
const reason = lifecycleMessage.reason ?? "worker_requested_restart";
writeLifecycleLog("Worker requested restart", { reason });
requestRestart(reason);
});
child.on("close", (code, signal) => {
@@ -279,6 +283,19 @@ export function runSupervisor(options: SupervisorOptions): void {
});
};
const signalWorker = (signal: NodeJS.Signals, reason: string): void => {
if (!child) {
return;
}
writeLifecycleLog("Supervisor sending signal to worker", {
reason,
signal,
supervisorPid: process.pid,
workerPid: child.pid ?? null,
});
child.kill(signal);
};
const requestRestart = (reason: string) => {
if (!child || restarting || shuttingDown) {
return;
@@ -286,7 +303,7 @@ export function runSupervisor(options: SupervisorOptions): void {
restarting = true;
writeLifecycleLog("Restart requested", { reason });
log(`${reason}. Stopping worker for restart...`);
child.kill("SIGTERM");
signalWorker("SIGTERM", reason);
};
const requestShutdown = (reason: string) => {
@@ -301,11 +318,11 @@ export function runSupervisor(options: SupervisorOptions): void {
exitSupervisor(0);
return;
}
child.kill("SIGTERM");
signalWorker("SIGTERM", reason);
};
const forwardSignal = (signal: NodeJS.Signals) => {
requestShutdown(`Received ${signal}`);
requestShutdown(`supervisor_received_${signal}`);
};
process.on("SIGINT", () => forwardSignal("SIGINT"));