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>
This commit is contained in:
paseo-bot[bot]
2026-09-29 11:19:19 +02:00
committed by GitHub
parent b9ac1d4982
commit 87818812ad
2 changed files with 37 additions and 1 deletions
@@ -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);
@@ -224,6 +224,12 @@ export async function resolveDesktopDaemonStatus(): Promise<DesktopDaemonStatus>
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<DesktopDaemonStatus>
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",