Recover desktop startup from stale daemon locks

Refs #1950
Refs #1904
Refs #769
This commit is contained in:
Mohamed Boudra
2026-07-15 15:48:09 +02:00
committed by GitHub
parent 7f011d16c5
commit f06792ae89
6 changed files with 516 additions and 75 deletions
@@ -16,6 +16,11 @@ const mocks = vi.hoisted(() => ({
},
runExternalCliJsonCommand: vi.fn(),
runExternalCliTextCommand: vi.fn(),
createNodeEntrypointInvocation: vi.fn(() => ({
command: "node",
args: [],
env: {},
})),
spawnProcess: vi.fn(),
logInfo: vi.fn(),
logError: vi.fn(),
@@ -59,11 +64,7 @@ vi.mock("../settings/desktop-settings-electron.js", () => ({
}));
vi.mock("./runtime-paths.js", () => ({
createNodeEntrypointInvocation: vi.fn(() => ({
command: "node",
args: [],
env: {},
})),
createNodeEntrypointInvocation: mocks.createNodeEntrypointInvocation,
resolveDaemonRunnerEntrypoint: vi.fn(() => ({
entryPath: "/tmp/daemon.js",
execArgv: [],
@@ -112,6 +113,8 @@ describe("daemon-manager commands", () => {
mocks.settings = DEFAULT_DESKTOP_SETTINGS;
mocks.runExternalCliJsonCommand.mockReset();
mocks.runExternalCliTextCommand.mockReset();
mocks.createNodeEntrypointInvocation.mockReset();
mocks.createNodeEntrypointInvocation.mockReturnValue({ command: "node", args: [], env: {} });
mocks.spawnProcess.mockReset();
mocks.logInfo.mockReset();
mocks.logError.mockReset();
@@ -439,6 +442,9 @@ describe("daemon-manager commands", () => {
expect(message).toContain("Daemon failed to start: exit code 1");
expect(recentLogsLabel?.split(/[\\/]/).at(-1)).toBe("daemon.log");
expect(message).toContain("recent daemon failure");
expect(mocks.createNodeEntrypointInvocation).toHaveBeenCalledWith(
expect.objectContaining({ args: [] }),
);
expect(mocks.spawnProcess).toHaveBeenCalledWith(
"node",
[],
@@ -450,6 +456,47 @@ describe("daemon-manager commands", () => {
);
});
it("passes stale lock reclaim only after a live desktop daemon is confirmed unresponsive", async () => {
mocks.runExternalCliJsonCommand.mockResolvedValue({
localDaemon: "unresponsive",
connectedDaemon: "unreachable",
serverId: "",
pid: 7675,
listen: "127.0.0.1:6767",
desktopManaged: true,
});
mocks.spawnProcess.mockImplementation(() => {
const child = createMockChildProcess();
scheduleFailedStartup(child);
return child;
});
await expect(createDaemonCommandHandlers().start_desktop_daemon()).rejects.toThrow(
"Daemon failed to start: exit code 1",
);
expect(mocks.createNodeEntrypointInvocation).toHaveBeenCalledWith(
expect.objectContaining({ args: ["--reclaim-stale-pid-lock"] }),
);
});
it("does not pass stale lock reclaim when the status command fails", async () => {
mocks.runExternalCliJsonCommand.mockRejectedValue(new Error("status command failed"));
mocks.spawnProcess.mockImplementation(() => {
const child = createMockChildProcess();
scheduleFailedStartup(child);
return child;
});
await expect(createDaemonCommandHandlers().start_desktop_daemon()).rejects.toThrow(
"Daemon failed to start: exit code 1",
);
expect(mocks.createNodeEntrypointInvocation).toHaveBeenCalledWith(
expect.objectContaining({ args: [] }),
);
});
it("returns the Electron main-process log tail from electron-log", () => {
writeFileSync(
mocks.appLogPath,
@@ -385,10 +385,12 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
}
const daemonRunner = resolveDaemonRunnerEntrypoint();
const reclaimStalePidLock =
current.status === "errored" && current.desktopManaged && current.error === null;
const invocation = createNodeEntrypointInvocation({
entrypoint: daemonRunner,
argvMode: "node-script",
args: [],
args: reclaimStalePidLock ? ["--reclaim-stale-pid-lock"] : [],
baseEnv: process.env,
});