Fix desktop daemon stale PID startup (#913)
This commit is contained in:
@@ -43,6 +43,7 @@ const DAEMON_LOG_FILENAME = "daemon.log";
|
||||
const STARTUP_POLL_INTERVAL_MS = 200;
|
||||
const STARTUP_POLL_MAX_ATTEMPTS = 150;
|
||||
const DETACHED_STARTUP_GRACE_MS = 1200;
|
||||
const STARTUP_OUTPUT_CAPTURE_LIMIT_CHARS = 64 * 1024;
|
||||
|
||||
type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
|
||||
|
||||
@@ -69,6 +70,11 @@ interface DesktopPairingOffer {
|
||||
qr: string | null;
|
||||
}
|
||||
|
||||
interface StartupOutputCapture {
|
||||
text: string;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
function parseReleaseChannel(
|
||||
args: Record<string, unknown> | undefined,
|
||||
): AppReleaseChannel | undefined {
|
||||
@@ -145,6 +151,30 @@ function tailFile(filePath: string, lines = 50): string {
|
||||
}
|
||||
}
|
||||
|
||||
function createStartupOutputCapture(): StartupOutputCapture {
|
||||
return { text: "", truncated: false };
|
||||
}
|
||||
|
||||
function appendStartupOutput(capture: StartupOutputCapture, chunk: Buffer): StartupOutputCapture {
|
||||
const nextText = capture.text + chunk.toString();
|
||||
if (nextText.length <= STARTUP_OUTPUT_CAPTURE_LIMIT_CHARS) {
|
||||
return { text: nextText, truncated: capture.truncated };
|
||||
}
|
||||
|
||||
return {
|
||||
text: nextText.slice(-STARTUP_OUTPUT_CAPTURE_LIMIT_CHARS),
|
||||
truncated: true,
|
||||
};
|
||||
}
|
||||
|
||||
function formatStartupOutput(capture: StartupOutputCapture): string {
|
||||
if (!capture.truncated) {
|
||||
return capture.text;
|
||||
}
|
||||
|
||||
return `[output truncated to the last ${STARTUP_OUTPUT_CAPTURE_LIMIT_CHARS} chars]\n${capture.text}`;
|
||||
}
|
||||
|
||||
function logDesktopDaemonLifecycle(message: string, details?: Record<string, unknown>): void {
|
||||
log.info("[desktop daemon]", message, {
|
||||
pid: process.pid,
|
||||
@@ -197,17 +227,28 @@ export async function resolveDesktopDaemonStatus(): Promise<DesktopDaemonStatus>
|
||||
unknown
|
||||
>;
|
||||
const localDaemon = typeof payload.localDaemon === "string" ? payload.localDaemon : "stopped";
|
||||
const running = localDaemon === "running";
|
||||
const connectedDaemon =
|
||||
typeof payload.connectedDaemon === "string" ? payload.connectedDaemon : "not_probed";
|
||||
const hasRunningLocalProcess = localDaemon === "running";
|
||||
const hasLocalProcess = hasRunningLocalProcess || localDaemon === "unresponsive";
|
||||
const apiReachable = connectedDaemon === "reachable";
|
||||
let status: DesktopDaemonState = "stopped";
|
||||
if (apiReachable || hasRunningLocalProcess) {
|
||||
status = "running";
|
||||
} else if (localDaemon === "unresponsive") {
|
||||
status = "errored";
|
||||
}
|
||||
|
||||
return {
|
||||
serverId: typeof payload.serverId === "string" ? payload.serverId : "",
|
||||
status: running ? "running" : "stopped",
|
||||
status,
|
||||
listen: typeof payload.listen === "string" ? payload.listen : null,
|
||||
hostname: running && typeof payload.hostname === "string" ? payload.hostname : null,
|
||||
pid: running && typeof payload.pid === "number" ? payload.pid : null,
|
||||
hostname:
|
||||
status === "running" && typeof payload.hostname === "string" ? payload.hostname : null,
|
||||
pid: hasLocalProcess && typeof payload.pid === "number" ? payload.pid : null,
|
||||
home,
|
||||
version: typeof payload.daemonVersion === "string" ? payload.daemonVersion : null,
|
||||
desktopManaged: payload.desktopManaged === true,
|
||||
desktopManaged: hasRunningLocalProcess && payload.desktopManaged === true,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -248,15 +289,17 @@ function assertBuiltInDaemonManagementEnabled(settings: DesktopSettings): void {
|
||||
|
||||
function buildStartupFailureError(
|
||||
result: { code: number | null; signal: string | null; error?: Error },
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
stdout: StartupOutputCapture,
|
||||
stderr: StartupOutputCapture,
|
||||
): Error {
|
||||
const reason = result.error
|
||||
? result.error.message
|
||||
: `exit code ${result.code ?? "unknown"}${result.signal ? ` (${result.signal})` : ""}`;
|
||||
const parts = [`Daemon failed to start: ${reason}`];
|
||||
if (stderr.trim()) parts.push(`stderr:\n${stderr.trim()}`);
|
||||
if (stdout.trim()) parts.push(`stdout:\n${stdout.trim()}`);
|
||||
const formattedStderr = formatStartupOutput(stderr).trim();
|
||||
const formattedStdout = formatStartupOutput(stdout).trim();
|
||||
if (formattedStderr) parts.push(`stderr:\n${formattedStderr}`);
|
||||
if (formattedStdout) parts.push(`stdout:\n${formattedStdout}`);
|
||||
const logs = tailFile(logFilePath(), 15);
|
||||
if (logs) parts.push(`Recent logs (${logFilePath()}):\n${logs}`);
|
||||
return new Error(parts.join("\n\n"));
|
||||
@@ -337,13 +380,13 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let stdout = createStartupOutputCapture();
|
||||
let stderr = createStartupOutputCapture();
|
||||
child.stdout!.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
stdout = appendStartupOutput(stdout, data);
|
||||
});
|
||||
child.stderr!.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
stderr = appendStartupOutput(stderr, data);
|
||||
});
|
||||
|
||||
logDesktopDaemonLifecycle("detached spawn returned", {
|
||||
@@ -381,8 +424,8 @@ async function startDaemon(): Promise<DesktopDaemonStatus> {
|
||||
logDesktopDaemonLifecycle("detached startup grace period completed", {
|
||||
childPid: child.pid ?? null,
|
||||
exitedEarly: result.exitedEarly,
|
||||
stdout: stdout.slice(0, 2000),
|
||||
stderr: stderr.slice(0, 2000),
|
||||
stdout: formatStartupOutput(stdout).slice(0, 2000),
|
||||
stderr: formatStartupOutput(stderr).slice(0, 2000),
|
||||
...(result.exitedEarly
|
||||
? {
|
||||
exitCode: result.code,
|
||||
|
||||
Reference in New Issue
Block a user