chore(windows): instrument detached daemon launch

This commit is contained in:
Mohamed Boudra
2026-03-29 14:38:18 +07:00
parent 094864d779
commit 831e3289e7
4 changed files with 247 additions and 3 deletions
+60 -1
View File
@@ -1,5 +1,5 @@
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { app, ipcMain } from "electron";
import { loadConfig, resolvePaseoHome, getOrCreateServerId } from "@getpaseo/server";
@@ -27,6 +27,7 @@ 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";
const DAEMON_LAUNCH_LOG_FILENAME = "daemon-launch.log";
type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
@@ -75,6 +76,10 @@ function logFilePath(): string {
return path.join(getPaseoHome(), DAEMON_LOG_FILENAME);
}
function launchLogFilePath(): string {
return path.join(getPaseoHome(), DAEMON_LAUNCH_LOG_FILENAME);
}
function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0);
@@ -140,6 +145,24 @@ function tailFile(filePath: string, lines = 50): string {
}
}
function appendLaunchLog(message: string, details?: Record<string, unknown>): void {
const line = [
new Date().toISOString(),
"[desktop daemon]",
`pid=${process.pid}`,
message,
details ? JSON.stringify(details) : "",
]
.filter((part) => part.length > 0)
.join(" ");
try {
appendFileSync(launchLogFilePath(), `${line}\n`, "utf-8");
} catch {
// Keep launch debugging best-effort so desktop startup behavior is unchanged.
}
}
function buildDesktopDaemonCorsOriginsEnv(): string | undefined {
const origins = new Set(
(process.env.PASEO_CORS_ORIGINS ?? "")
@@ -303,6 +326,16 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
},
});
appendLaunchLog("starting detached daemon", {
appIsPackaged: app.isPackaged,
daemonRunnerEntry: daemonRunner.entryPath,
daemonRunnerExecArgv: daemonRunner.execArgv,
command: invocation.command,
args: invocation.args,
listen: process.env.PASEO_LISTEN ?? null,
corsOrigins: corsOrigins ?? null,
});
const child: ChildProcess = spawn(
invocation.command,
invocation.args,
@@ -313,6 +346,12 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
},
);
appendLaunchLog("detached spawn returned", {
childPid: child.pid ?? null,
spawnfile: child.spawnfile,
spawnargs: child.spawnargs,
});
child.unref();
// Wait for process to survive the grace period
@@ -327,15 +366,26 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
const timer = setTimeout(() => finish(false), DETACHED_STARTUP_GRACE_MS);
child.once("error", () => {
appendLaunchLog("detached child emitted error during grace period", {
childPid: child.pid ?? null,
});
clearTimeout(timer);
finish(true);
});
child.once("exit", () => {
appendLaunchLog("detached child emitted exit during grace period", {
childPid: child.pid ?? null,
});
clearTimeout(timer);
finish(true);
});
});
appendLaunchLog("detached startup grace period completed", {
childPid: child.pid ?? null,
exitedEarly,
});
if (exitedEarly) {
const logs = tailFile(logFilePath(), 15);
throw new Error(`Daemon failed to start.${logs ? `\n\nRecent logs:\n${logs}` : ""}`);
@@ -344,6 +394,15 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
// Poll for PID file with server ID
for (let attempt = 0; attempt < STARTUP_POLL_MAX_ATTEMPTS; attempt++) {
const status = resolveStatus();
if (attempt === 0 || attempt === STARTUP_POLL_MAX_ATTEMPTS - 1 || attempt % 10 === 9) {
appendLaunchLog("polling daemon status after detached start", {
attempt: attempt + 1,
status: status.status,
pid: status.pid,
listen: status.listen,
serverId: status.serverId || null,
});
}
if (status.status === "running" && status.serverId) return status;
await sleep(STARTUP_POLL_INTERVAL_MS);
}