From 87818812ad22921819845296460e8fd9bdfe5f3f Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:19:19 +0200 Subject: [PATCH] Stop the desktop app from launching a process every second when no local daemon runs (#5636) * fix(desktop): stop launching the CLI every second when no local daemon runs The app polls the local daemon status once a second until it learns a server id, and a stopped daemon has none, so the poll never ends. Each poll ran `paseo daemon status` in a new process, which on Windows is Paseo.exe and shows the busy cursor about once a second while the app sits idle. The status handler now answers "stopped" in-process when the home has no running daemon, which is what the CLI reports in that case. * fix(desktop): report an errored daemon when its state file cannot be read Reading the daemon instance record ran before the try in resolveDesktopDaemonStatus, so an unreadable paseo.pid made the status IPC reject instead of returning the errored status the CLI failure path produced. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../desktop/src/daemon/daemon-manager.test.ts | 30 +++++++++++++++++++ packages/desktop/src/daemon/daemon-manager.ts | 8 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/daemon/daemon-manager.test.ts b/packages/desktop/src/daemon/daemon-manager.test.ts index 4da4e5319e..d18a8cd48c 100644 --- a/packages/desktop/src/daemon/daemon-manager.test.ts +++ b/packages/desktop/src/daemon/daemon-manager.test.ts @@ -125,6 +125,36 @@ describe("daemon-manager commands", () => { }); }); + it("reports a stopped daemon without launching the CLI when no local daemon runs", async () => { + mkdirSync(mocks.paseoHome); + writeFileSync(path.join(mocks.paseoHome, "server-id"), "srv_existing\n"); + mocks.runExternalCliJsonCommand.mockResolvedValue({ + home: mocks.paseoHome, + pid: null, + startedAt: null, + listen: null, + hostname: null, + localDaemon: "stopped", + desktopManaged: false, + connectedDaemon: "not_probed", + }); + + const status = await createDaemonCommandHandlers().desktop_daemon_status(); + + expect(status).toMatchObject({ serverId: "", status: "stopped", pid: null }); + expect(mocks.runExternalCliJsonCommand).not.toHaveBeenCalled(); + }); + + it("reports an errored daemon when the local daemon state cannot be read", async () => { + mkdirSync(mocks.paseoHome); + writeFileSync(path.join(mocks.paseoHome, "paseo.pid"), "garbage"); + + const status = await createDaemonCommandHandlers().desktop_daemon_status(); + + expect(status).toMatchObject({ serverId: "", status: "errored", pid: null }); + expect(status.error).toBeTruthy(); + }); + it("returns a local credential only for its live managed daemon listen", async () => { mkdirSync(mocks.paseoHome); const token = "a".repeat(43); diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts index 8c1cf8dcc8..5f3f5394c6 100644 --- a/packages/desktop/src/daemon/daemon-manager.ts +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -224,6 +224,12 @@ export async function resolveDesktopDaemonStatus(): Promise const home = getPaseoHome(); try { + // The app polls this while no local daemon runs. Answer that case in-process, + // since launching the CLI once per poll keeps spawning processes while idle. + if (!(await readDaemonInstance(home))) { + return statusFromDaemonProbe({ localDaemon: "stopped" }, home); + } + const payload = (await runExternalCliJsonCommand([ "daemon", "status", @@ -234,7 +240,7 @@ export async function resolveDesktopDaemonStatus(): Promise return statusFromDaemonProbe(payload, home); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - logDesktopDaemonLifecycle("resolveStatus CLI command failed", { error: errorMessage }); + logDesktopDaemonLifecycle("resolveStatus failed", { error: errorMessage }); return { serverId: "", status: "errored",