Use tree-kill for daemon shutdown (#788)

* Use tree-kill for daemon shutdown

* fix: update lockfile signatures and Nix hash

* Use tree-kill for daemon shutdown

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Mohamed Boudra
2026-05-07 13:18:39 +08:00
committed by GitHub
co-authored by github-actions[bot] <github-actions[bot]@users.noreply.github.com>
parent 7558bd7947
commit 80bdb4d45b
16 changed files with 561 additions and 494 deletions
@@ -112,4 +112,46 @@ describe("daemon-manager commands", () => {
expect(mocks.runCliJsonCommand).toHaveBeenCalledWith(["daemon", "status", "--json"]);
});
it("routes running desktop daemon stops through CLI daemon stop", async () => {
mocks.runCliJsonCommand
.mockResolvedValueOnce({
localDaemon: "running",
serverId: "server-1",
pid: 4242,
listen: "127.0.0.1:6767",
desktopManaged: true,
})
.mockResolvedValueOnce({ action: "stopped" })
.mockResolvedValueOnce({
localDaemon: "stopped",
serverId: "",
});
const handlers = createDaemonCommandHandlers();
await expect(handlers.stop_desktop_daemon()).resolves.toEqual({
serverId: "",
status: "stopped",
listen: null,
hostname: null,
pid: null,
home: "/tmp/paseo-home",
version: null,
desktopManaged: false,
error: null,
});
expect(mocks.runCliJsonCommand).toHaveBeenNthCalledWith(1, ["daemon", "status", "--json"]);
expect(mocks.runCliJsonCommand).toHaveBeenNthCalledWith(2, [
"daemon",
"stop",
"--json",
"--timeout",
"5",
"--force",
"--kill-timeout",
"5",
]);
expect(mocks.runCliJsonCommand).toHaveBeenNthCalledWith(3, ["daemon", "status", "--json"]);
});
});
+1 -56
View File
@@ -43,11 +43,8 @@ import { getDesktopSettingsStore } from "../settings/desktop-settings-electron.j
import { isRunningUnderARM64Translation } from "../system/arm64-translation.js";
const DAEMON_LOG_FILENAME = "daemon.log";
const PID_POLL_INTERVAL_MS = 100;
const STARTUP_POLL_INTERVAL_MS = 200;
const STARTUP_POLL_MAX_ATTEMPTS = 150;
const STOP_TIMEOUT_MS = 15_000;
const KILL_TIMEOUT_MS = 3_000;
const DETACHED_STARTUP_GRACE_MS = 1200;
type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
@@ -136,52 +133,12 @@ function isProcessRunning(pid: number): boolean {
}
}
function signalProcessSafely(pid: number, signal: NodeJS.Signals): boolean {
if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false;
try {
process.kill(pid, signal);
return true;
} catch (err) {
if (typeof err === "object" && err !== null && "code" in err) {
if (err.code === "ESRCH") return false;
if (err.code === "EPERM") return true;
}
throw err;
}
}
function signalProcessGroupSafely(pid: number, signal: NodeJS.Signals): boolean {
if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false;
if (process.platform === "win32") return signalProcessSafely(pid, signal);
try {
process.kill(-pid, signal);
return true;
} catch (err) {
if (typeof err === "object" && err !== null && "code" in err) {
if (err.code === "ESRCH") return signalProcessSafely(pid, signal);
if (err.code === "EPERM") return true;
}
throw err;
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
async function poll(): Promise<boolean> {
if (!isProcessRunning(pid)) return true;
if (Date.now() >= deadline) return !isProcessRunning(pid);
await sleep(PID_POLL_INTERVAL_MS);
return poll();
}
return poll();
}
function tailFile(filePath: string, lines = 50): string {
try {
const content = readFileSync(filePath, "utf-8");
@@ -449,19 +406,7 @@ export async function stopDesktopDaemon(): Promise<DesktopDaemonStatus> {
const status = await resolveDesktopDaemonStatus();
if (status.status !== "running" || !status.pid) return status;
const pid = status.pid;
signalProcessSafely(pid, "SIGTERM");
let stopped = await waitForPidExit(pid, STOP_TIMEOUT_MS);
if (!stopped) {
signalProcessGroupSafely(pid, "SIGKILL");
stopped = await waitForPidExit(pid, KILL_TIMEOUT_MS);
}
if (!stopped) {
throw new Error(`Timed out waiting for daemon PID ${pid} to stop`);
}
await stopDesktopDaemonViaCli();
return await resolveDesktopDaemonStatus();
}