fix(server): fix daemon startup race and reduce log bloat

The desktop app could time out connecting to the daemon on first launch
because the PID file advertised a listen address before the HTTP server
was actually listening. The supervisor now writes the PID lock with
listen: null and updates it only after the worker sends a paseo:ready
IPC message confirming the server is listening.

- Rename daemon-runner.ts to supervisor-entrypoint.ts
- PID lock acquired with listen: null, updated via paseo:ready IPC
- Worker sends paseo:ready after httpServer.listen() resolves
- Desktop polls until listen is non-null before returning to the app
- Remove PASEO_PID_LOCK_MODE and external lock mode
- Remove unnecessary env overrides (PASEO_HOME, PASEO_CORS_ORIGINS) from
  desktop daemon spawn
- Reduce trace log bloat: inbound/outbound WebSocket messages now log
  only message type and payload size instead of full payloads
- Supervisor restarts worker on SIGKILL (covers OOM)
This commit is contained in:
Mohamed Boudra
2026-03-30 12:37:07 +07:00
parent e758734807
commit 8d0a9f9224
21 changed files with 198 additions and 147 deletions
+13 -40
View File
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { app, ipcMain } from "electron";
import log from "electron-log/main";
import { loadConfig, resolvePaseoHome, getOrCreateServerId } from "@getpaseo/server";
import { resolvePaseoHome, getOrCreateServerId } from "@getpaseo/server";
import {
copyAttachmentFileToManagedStorage,
deleteManagedAttachmentFile,
@@ -27,14 +27,13 @@ const STARTUP_POLL_MAX_ATTEMPTS = 150;
const STOP_TIMEOUT_MS = 15_000;
const KILL_TIMEOUT_MS = 3_000;
const DETACHED_STARTUP_GRACE_MS = 1200;
const DEFAULT_ELECTRON_DEV_SERVER_URL = "http://localhost:8081";
type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
type DesktopDaemonStatus = {
serverId: string;
status: DesktopDaemonState;
listen: string;
listen: string | null;
hostname: string | null;
pid: number | null;
home: string;
@@ -148,30 +147,6 @@ function logDesktopDaemonLifecycle(message: string, details?: Record<string, unk
});
}
function buildDesktopDaemonCorsOriginsEnv(): string | undefined {
const origins = new Set(
(process.env.PASEO_CORS_ORIGINS ?? "")
.split(",")
.map((value) => value.trim())
.filter((value) => value.length > 0),
);
const devServerUrl = process.env.EXPO_DEV_URL ?? DEFAULT_ELECTRON_DEV_SERVER_URL;
try {
const parsed = new URL(devServerUrl);
origins.add(parsed.origin);
if (parsed.hostname === "localhost") {
origins.add(`${parsed.protocol}//127.0.0.1${parsed.port ? `:${parsed.port}` : ""}`);
} else if (parsed.hostname === "127.0.0.1") {
origins.add(`${parsed.protocol}//localhost${parsed.port ? `:${parsed.port}` : ""}`);
}
} catch {
// Ignore malformed dev server URLs and preserve any explicit env configuration.
}
return origins.size > 0 ? Array.from(origins).join(",") : undefined;
}
function toTrimmedString(value: unknown): string | null {
if (typeof value !== "string") {
@@ -246,12 +221,11 @@ function resolveDesktopAppVersion(): string {
function resolveStatus(): DesktopDaemonStatus {
const home = getPaseoHome();
const config = loadConfig(home, { env: process.env });
const pidPath = pidFilePath();
let pid: number | null = null;
let hostname: string | null = null;
let listen: string = config.listen;
let listen: string | null = null;
try {
if (existsSync(pidPath)) {
@@ -266,7 +240,7 @@ function resolveStatus(): DesktopDaemonStatus {
: typeof parsed.sockPath === "string"
? (parsed.sockPath as string)
: null;
if (pidListen) listen = pidListen;
listen = pidListen;
}
}
} catch {
@@ -297,18 +271,12 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
const current = resolveStatus();
if (current.status === "running") return current;
const home = getPaseoHome();
const daemonRunner = resolveDaemonRunnerEntrypoint();
const corsOrigins = buildDesktopDaemonCorsOriginsEnv();
const invocation = createNodeEntrypointInvocation({
entrypoint: daemonRunner,
argvMode: "node-script",
args: [],
baseEnv: {
...process.env,
PASEO_HOME: home,
...(corsOrigins ? { PASEO_CORS_ORIGINS: corsOrigins } : {}),
},
baseEnv: process.env,
});
logDesktopDaemonLifecycle("starting detached daemon", {
@@ -317,8 +285,6 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
daemonRunnerExecArgv: daemonRunner.execArgv,
command: invocation.command,
args: invocation.args,
listen: process.env.PASEO_LISTEN ?? null,
corsOrigins: corsOrigins ?? null,
});
const child: ChildProcess = spawn(
@@ -388,7 +354,7 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
serverId: status.serverId || null,
});
}
if (status.status === "running" && status.serverId) return status;
if (status.status === "running" && status.serverId && status.listen) return status;
await sleep(STARTUP_POLL_INTERVAL_MS);
}
@@ -439,6 +405,9 @@ async function getDaemonPairing(): Promise<DesktopPairingOffer> {
}
try {
if (!status.listen) {
throw new Error("Daemon listen target is unavailable.");
}
const baseUrl = buildDaemonHttpBaseUrl(status.listen);
if (!baseUrl) {
throw new Error(`Daemon listen target is not a TCP endpoint: ${status.listen}`);
@@ -480,6 +449,10 @@ async function getLocalDaemonVersion(): Promise<{
};
}
if (!status.listen) {
return { version: null, error: "Daemon listen target is unavailable." };
}
const baseUrl = buildDaemonHttpBaseUrl(status.listen);
if (!baseUrl) {
return { version: null, error: `Daemon listen target is not a TCP endpoint: ${status.listen}` };
+8 -3
View File
@@ -107,7 +107,7 @@ export function resolveDaemonRunnerEntrypoint(): NodeEntrypointSpec {
"server",
"dist",
"scripts",
"daemon-runner.js",
"supervisor-entrypoint.js",
),
}),
execArgv: [],
@@ -115,7 +115,12 @@ export function resolveDaemonRunnerEntrypoint(): NodeEntrypointSpec {
}
const serverPackage = resolveServerPackageInfo();
const distRunner = path.join(serverPackage.root, "dist", "scripts", "daemon-runner.js");
const distRunner = path.join(
serverPackage.root,
"dist",
"scripts",
"supervisor-entrypoint.js",
);
if (existsSync(distRunner)) {
return {
entryPath: distRunner,
@@ -126,7 +131,7 @@ export function resolveDaemonRunnerEntrypoint(): NodeEntrypointSpec {
return {
entryPath: assertPathExists({
label: "Daemon runner source",
filePath: path.join(serverPackage.root, "scripts", "daemon-runner.ts"),
filePath: path.join(serverPackage.root, "scripts", "supervisor-entrypoint.ts"),
}),
execArgv: ["--import", "tsx"],
};