chore(windows): instrument detached daemon launch
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { spawn, spawnSync } from "node:child_process";
|
import { spawn, spawnSync } from "node:child_process";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { loadConfig, resolvePaseoHome } from "@getpaseo/server";
|
import { loadConfig, resolvePaseoHome } from "@getpaseo/server";
|
||||||
@@ -64,6 +64,7 @@ const DETACHED_STARTUP_GRACE_MS = 1200;
|
|||||||
const PID_POLL_INTERVAL_MS = 100;
|
const PID_POLL_INTERVAL_MS = 100;
|
||||||
const KILL_TIMEOUT_MS = 3000;
|
const KILL_TIMEOUT_MS = 3000;
|
||||||
const DAEMON_LOG_FILENAME = "daemon.log";
|
const DAEMON_LOG_FILENAME = "daemon.log";
|
||||||
|
const DAEMON_LAUNCH_LOG_FILENAME = "daemon-launch.log";
|
||||||
const DAEMON_PID_FILENAME = "paseo.pid";
|
const DAEMON_PID_FILENAME = "paseo.pid";
|
||||||
|
|
||||||
export const DEFAULT_STOP_TIMEOUT_MS = 15_000;
|
export const DEFAULT_STOP_TIMEOUT_MS = 15_000;
|
||||||
@@ -114,6 +115,24 @@ function buildChildEnv(options: DaemonStartOptions): NodeJS.ProcessEnv {
|
|||||||
return childEnv;
|
return childEnv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendLaunchLog(home: string, message: string, details?: Record<string, unknown>): void {
|
||||||
|
const line = [
|
||||||
|
new Date().toISOString(),
|
||||||
|
"[CLI daemon]",
|
||||||
|
`pid=${process.pid}`,
|
||||||
|
message,
|
||||||
|
details ? JSON.stringify(details) : "",
|
||||||
|
]
|
||||||
|
.filter((part) => part.length > 0)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
try {
|
||||||
|
appendFileSync(path.join(home, DAEMON_LAUNCH_LOG_FILENAME), `${line}\n`, "utf-8");
|
||||||
|
} catch {
|
||||||
|
// Keep launch debugging best-effort so daemon startup behavior is unchanged.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDaemonRunnerEntry(): string {
|
function resolveDaemonRunnerEntry(): string {
|
||||||
const serverExportPath = require.resolve("@getpaseo/server");
|
const serverExportPath = require.resolve("@getpaseo/server");
|
||||||
let currentDir = path.dirname(serverExportPath);
|
let currentDir = path.dirname(serverExportPath);
|
||||||
@@ -354,6 +373,14 @@ export async function startLocalDaemonDetached(
|
|||||||
const paseoHome = resolvePaseoHome(childEnv);
|
const paseoHome = resolvePaseoHome(childEnv);
|
||||||
const logPath = path.join(paseoHome, DAEMON_LOG_FILENAME);
|
const logPath = path.join(paseoHome, DAEMON_LOG_FILENAME);
|
||||||
const daemonRunnerEntry = resolveDaemonRunnerEntry();
|
const daemonRunnerEntry = resolveDaemonRunnerEntry();
|
||||||
|
appendLaunchLog(paseoHome, "starting detached daemon", {
|
||||||
|
execPath: process.execPath,
|
||||||
|
execArgv: process.execArgv,
|
||||||
|
daemonRunnerEntry,
|
||||||
|
runnerArgs: buildRunnerArgs(options),
|
||||||
|
listen: childEnv.PASEO_LISTEN ?? null,
|
||||||
|
allowedHosts: childEnv.PASEO_ALLOWED_HOSTS ?? null,
|
||||||
|
});
|
||||||
const child = spawn(
|
const child = spawn(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
[...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)],
|
[...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)],
|
||||||
@@ -364,6 +391,12 @@ export async function startLocalDaemonDetached(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
appendLaunchLog(paseoHome, "detached spawn returned", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
spawnfile: child.spawnfile,
|
||||||
|
spawnargs: child.spawnargs,
|
||||||
|
});
|
||||||
|
|
||||||
child.unref();
|
child.unref();
|
||||||
|
|
||||||
const startup = await new Promise<DetachedStartupResult>((resolve) => {
|
const startup = await new Promise<DetachedStartupResult>((resolve) => {
|
||||||
@@ -378,16 +411,33 @@ export async function startLocalDaemonDetached(
|
|||||||
const timer = setTimeout(() => finish(startupReady()), DETACHED_STARTUP_GRACE_MS);
|
const timer = setTimeout(() => finish(startupReady()), DETACHED_STARTUP_GRACE_MS);
|
||||||
|
|
||||||
child.once("error", (error) => {
|
child.once("error", (error) => {
|
||||||
|
appendLaunchLog(paseoHome, "detached child emitted error during grace period", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
finish(startupExited({ code: null, signal: null, error }));
|
finish(startupExited({ code: null, signal: null, error }));
|
||||||
});
|
});
|
||||||
|
|
||||||
child.once("exit", (code, signal) => {
|
child.once("exit", (code, signal) => {
|
||||||
|
appendLaunchLog(paseoHome, "detached child emitted exit during grace period", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
code,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
finish(startupExited({ code, signal }));
|
finish(startupExited({ code, signal }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appendLaunchLog(paseoHome, "detached startup grace period completed", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
exitedEarly: startup.exitedEarly,
|
||||||
|
code: startup.exitedEarly ? startup.code : null,
|
||||||
|
signal: startup.exitedEarly ? startup.signal : null,
|
||||||
|
error: startup.exitedEarly ? startup.error?.message ?? null : null,
|
||||||
|
});
|
||||||
|
|
||||||
if (startup.exitedEarly) {
|
if (startup.exitedEarly) {
|
||||||
const reason = startup.error
|
const reason = startup.error
|
||||||
? startup.error.message
|
? startup.error.message
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { spawn, type ChildProcess } from "node:child_process";
|
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 path from "node:path";
|
||||||
import { app, ipcMain } from "electron";
|
import { app, ipcMain } from "electron";
|
||||||
import { loadConfig, resolvePaseoHome, getOrCreateServerId } from "@getpaseo/server";
|
import { loadConfig, resolvePaseoHome, getOrCreateServerId } from "@getpaseo/server";
|
||||||
@@ -27,6 +27,7 @@ const STOP_TIMEOUT_MS = 15_000;
|
|||||||
const KILL_TIMEOUT_MS = 3_000;
|
const KILL_TIMEOUT_MS = 3_000;
|
||||||
const DETACHED_STARTUP_GRACE_MS = 1200;
|
const DETACHED_STARTUP_GRACE_MS = 1200;
|
||||||
const DEFAULT_ELECTRON_DEV_SERVER_URL = "http://localhost:8081";
|
const DEFAULT_ELECTRON_DEV_SERVER_URL = "http://localhost:8081";
|
||||||
|
const DAEMON_LAUNCH_LOG_FILENAME = "daemon-launch.log";
|
||||||
|
|
||||||
type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
|
type DesktopDaemonState = "starting" | "running" | "stopped" | "errored";
|
||||||
|
|
||||||
@@ -75,6 +76,10 @@ function logFilePath(): string {
|
|||||||
return path.join(getPaseoHome(), DAEMON_LOG_FILENAME);
|
return path.join(getPaseoHome(), DAEMON_LOG_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function launchLogFilePath(): string {
|
||||||
|
return path.join(getPaseoHome(), DAEMON_LAUNCH_LOG_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
function isProcessRunning(pid: number): boolean {
|
function isProcessRunning(pid: number): boolean {
|
||||||
try {
|
try {
|
||||||
process.kill(pid, 0);
|
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 {
|
function buildDesktopDaemonCorsOriginsEnv(): string | undefined {
|
||||||
const origins = new Set(
|
const origins = new Set(
|
||||||
(process.env.PASEO_CORS_ORIGINS ?? "")
|
(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(
|
const child: ChildProcess = spawn(
|
||||||
invocation.command,
|
invocation.command,
|
||||||
invocation.args,
|
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();
|
child.unref();
|
||||||
|
|
||||||
// Wait for process to survive the grace period
|
// 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);
|
const timer = setTimeout(() => finish(false), DETACHED_STARTUP_GRACE_MS);
|
||||||
|
|
||||||
child.once("error", () => {
|
child.once("error", () => {
|
||||||
|
appendLaunchLog("detached child emitted error during grace period", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
});
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
finish(true);
|
finish(true);
|
||||||
});
|
});
|
||||||
child.once("exit", () => {
|
child.once("exit", () => {
|
||||||
|
appendLaunchLog("detached child emitted exit during grace period", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
});
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
finish(true);
|
finish(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
appendLaunchLog("detached startup grace period completed", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
exitedEarly,
|
||||||
|
});
|
||||||
|
|
||||||
if (exitedEarly) {
|
if (exitedEarly) {
|
||||||
const logs = tailFile(logFilePath(), 15);
|
const logs = tailFile(logFilePath(), 15);
|
||||||
throw new Error(`Daemon failed to start.${logs ? `\n\nRecent logs:\n${logs}` : ""}`);
|
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
|
// Poll for PID file with server ID
|
||||||
for (let attempt = 0; attempt < STARTUP_POLL_MAX_ATTEMPTS; attempt++) {
|
for (let attempt = 0; attempt < STARTUP_POLL_MAX_ATTEMPTS; attempt++) {
|
||||||
const status = resolveStatus();
|
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;
|
if (status.status === "running" && status.serverId) return status;
|
||||||
await sleep(STARTUP_POLL_INTERVAL_MS);
|
await sleep(STARTUP_POLL_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { existsSync } from "node:fs";
|
import { appendFileSync, existsSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { loadConfig } from "../src/server/config.js";
|
import { loadConfig } from "../src/server/config.js";
|
||||||
import { acquirePidLock, PidLockError, releasePidLock } from "../src/server/pid-lock.js";
|
import { acquirePidLock, PidLockError, releasePidLock } from "../src/server/pid-lock.js";
|
||||||
@@ -68,6 +68,28 @@ function resolvePackagedNodeEntrypointRunnerPath(currentScriptPath: string): str
|
|||||||
return existsSync(runnerPath) ? runnerPath : null;
|
return existsSync(runnerPath) ? runnerPath : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendLaunchLog(
|
||||||
|
launchLogPath: string,
|
||||||
|
message: string,
|
||||||
|
details?: Record<string, unknown>,
|
||||||
|
): void {
|
||||||
|
const line = [
|
||||||
|
new Date().toISOString(),
|
||||||
|
"[DaemonRunner]",
|
||||||
|
`pid=${process.pid}`,
|
||||||
|
message,
|
||||||
|
details ? JSON.stringify(details) : "",
|
||||||
|
]
|
||||||
|
.filter((part) => part.length > 0)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
try {
|
||||||
|
appendFileSync(launchLogPath, `${line}\n`, "utf-8");
|
||||||
|
} catch {
|
||||||
|
// Keep launch debugging best-effort so daemon startup behavior is unchanged.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const config = parseConfig(process.argv.slice(2));
|
const config = parseConfig(process.argv.slice(2));
|
||||||
const workerEntry = config.devMode ? resolveDevWorkerEntry() : resolveWorkerEntry();
|
const workerEntry = config.devMode ? resolveDevWorkerEntry() : resolveWorkerEntry();
|
||||||
@@ -84,14 +106,34 @@ async function main(): Promise<void> {
|
|||||||
applySherpaLoaderEnv(workerEnv);
|
applySherpaLoaderEnv(workerEnv);
|
||||||
|
|
||||||
const paseoHome = resolvePaseoHome(workerEnv);
|
const paseoHome = resolvePaseoHome(workerEnv);
|
||||||
|
const launchLogPath = path.join(paseoHome, "daemon-launch.log");
|
||||||
const daemonConfig = loadConfig(paseoHome, { env: workerEnv });
|
const daemonConfig = loadConfig(paseoHome, { env: workerEnv });
|
||||||
|
|
||||||
|
appendLaunchLog(launchLogPath, "daemon runner starting", {
|
||||||
|
argv: process.argv.slice(2),
|
||||||
|
execPath: process.execPath,
|
||||||
|
execArgv: process.execArgv,
|
||||||
|
workerEntry,
|
||||||
|
workerExecArgv,
|
||||||
|
devMode: config.devMode,
|
||||||
|
listen: daemonConfig.listen,
|
||||||
|
packagedNodeEntrypointRunner,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await acquirePidLock(paseoHome, daemonConfig.listen, {
|
await acquirePidLock(paseoHome, daemonConfig.listen, {
|
||||||
ownerPid: process.pid,
|
ownerPid: process.pid,
|
||||||
});
|
});
|
||||||
|
appendLaunchLog(launchLogPath, "pid lock acquired", {
|
||||||
|
ownerPid: process.pid,
|
||||||
|
listen: daemonConfig.listen,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PidLockError) {
|
if (error instanceof PidLockError) {
|
||||||
|
appendLaunchLog(launchLogPath, "pid lock acquisition failed", {
|
||||||
|
message: error.message,
|
||||||
|
existingLock: error.existingLock,
|
||||||
|
});
|
||||||
process.stderr.write(`${error.message}\n`);
|
process.stderr.write(`${error.message}\n`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
return;
|
return;
|
||||||
@@ -105,6 +147,9 @@ async function main(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lockReleased = true;
|
lockReleased = true;
|
||||||
|
appendLaunchLog(launchLogPath, "releasing pid lock", {
|
||||||
|
ownerPid: process.pid,
|
||||||
|
});
|
||||||
await releasePidLock(paseoHome, {
|
await releasePidLock(paseoHome, {
|
||||||
ownerPid: process.pid,
|
ownerPid: process.pid,
|
||||||
});
|
});
|
||||||
@@ -112,6 +157,7 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
runSupervisor({
|
runSupervisor({
|
||||||
name: "DaemonRunner",
|
name: "DaemonRunner",
|
||||||
|
launchLogPath,
|
||||||
startupMessage: config.devMode
|
startupMessage: config.devMode
|
||||||
? "Starting daemon worker (dev mode, crash restarts enabled)"
|
? "Starting daemon worker (dev mode, crash restarts enabled)"
|
||||||
: "Starting daemon worker (IPC restart enabled)",
|
: "Starting daemon worker (IPC restart enabled)",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fork, spawn, type ChildProcess } from "child_process";
|
import { fork, spawn, type ChildProcess } from "child_process";
|
||||||
|
import { appendFileSync } from "node:fs";
|
||||||
|
|
||||||
type WorkerLifecycleMessage =
|
type WorkerLifecycleMessage =
|
||||||
| {
|
| {
|
||||||
@@ -12,6 +13,7 @@ type WorkerLifecycleMessage =
|
|||||||
type SupervisorOptions = {
|
type SupervisorOptions = {
|
||||||
name: string;
|
name: string;
|
||||||
startupMessage: string;
|
startupMessage: string;
|
||||||
|
launchLogPath?: string;
|
||||||
resolveWorkerEntry: () => string;
|
resolveWorkerEntry: () => string;
|
||||||
workerArgs?: string[];
|
workerArgs?: string[];
|
||||||
workerEnv?: NodeJS.ProcessEnv;
|
workerEnv?: NodeJS.ProcessEnv;
|
||||||
@@ -63,6 +65,28 @@ export function runSupervisor(options: SupervisorOptions): void {
|
|||||||
process.stderr.write(`[${options.name}] ${message}\n`);
|
process.stderr.write(`[${options.name}] ${message}\n`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logLaunch = (message: string, details?: Record<string, unknown>): void => {
|
||||||
|
if (!options.launchLogPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = [
|
||||||
|
new Date().toISOString(),
|
||||||
|
`[${options.name}]`,
|
||||||
|
`pid=${process.pid}`,
|
||||||
|
message,
|
||||||
|
details ? JSON.stringify(details) : "",
|
||||||
|
]
|
||||||
|
.filter((part) => part.length > 0)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
try {
|
||||||
|
appendFileSync(options.launchLogPath, `${line}\n`, "utf-8");
|
||||||
|
} catch {
|
||||||
|
// Keep launch debugging best-effort so daemon startup behavior is unchanged.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const exitSupervisor = (code: number): void => {
|
const exitSupervisor = (code: number): void => {
|
||||||
if (exiting) {
|
if (exiting) {
|
||||||
return;
|
return;
|
||||||
@@ -92,11 +116,22 @@ export function runSupervisor(options: SupervisorOptions): void {
|
|||||||
|
|
||||||
const spawnSpec = resolveWorkerSpawnSpec?.(workerEntry) ?? null;
|
const spawnSpec = resolveWorkerSpawnSpec?.(workerEntry) ?? null;
|
||||||
if (spawnSpec) {
|
if (spawnSpec) {
|
||||||
|
logLaunch("spawning worker via explicit spawn spec", {
|
||||||
|
workerEntry,
|
||||||
|
command: spawnSpec.command,
|
||||||
|
args: spawnSpec.args,
|
||||||
|
workerArgs,
|
||||||
|
});
|
||||||
child = spawn(spawnSpec.command, spawnSpec.args, {
|
child = spawn(spawnSpec.command, spawnSpec.args, {
|
||||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||||
env: spawnSpec.env ?? workerEnv,
|
env: spawnSpec.env ?? workerEnv,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
logLaunch("spawning worker via fork", {
|
||||||
|
workerEntry,
|
||||||
|
workerArgs,
|
||||||
|
workerExecArgv,
|
||||||
|
});
|
||||||
child = fork(workerEntry, workerArgs, {
|
child = fork(workerEntry, workerArgs, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
env: workerEnv,
|
env: workerEnv,
|
||||||
@@ -104,8 +139,35 @@ export function runSupervisor(options: SupervisorOptions): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logLaunch("worker spawn returned", {
|
||||||
|
childPid: child.pid ?? null,
|
||||||
|
connected: child.connected,
|
||||||
|
spawnfile: child.spawnfile,
|
||||||
|
spawnargs: child.spawnargs,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("spawn", () => {
|
||||||
|
logLaunch("worker emitted spawn", {
|
||||||
|
childPid: child?.pid ?? null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
logLaunch("worker emitted error", {
|
||||||
|
childPid: child?.pid ?? null,
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
child.on("message", (msg: unknown) => {
|
child.on("message", (msg: unknown) => {
|
||||||
const lifecycleMessage = parseLifecycleMessage(msg);
|
const lifecycleMessage = parseLifecycleMessage(msg);
|
||||||
|
logLaunch("worker emitted ipc message", {
|
||||||
|
childPid: child?.pid ?? null,
|
||||||
|
rawType:
|
||||||
|
typeof msg === "object" && msg !== null && "type" in msg
|
||||||
|
? (msg as { type?: unknown }).type
|
||||||
|
: typeof msg,
|
||||||
|
});
|
||||||
if (!lifecycleMessage) {
|
if (!lifecycleMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -119,6 +181,13 @@ export function runSupervisor(options: SupervisorOptions): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
child.on("exit", (code, signal) => {
|
child.on("exit", (code, signal) => {
|
||||||
|
logLaunch("worker emitted exit", {
|
||||||
|
childPid: child?.pid ?? null,
|
||||||
|
code,
|
||||||
|
signal,
|
||||||
|
restarting,
|
||||||
|
shuttingDown,
|
||||||
|
});
|
||||||
const exitDescriptor = describeExit(code, signal);
|
const exitDescriptor = describeExit(code, signal);
|
||||||
|
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
@@ -137,6 +206,20 @@ export function runSupervisor(options: SupervisorOptions): void {
|
|||||||
log(`Worker exited (${exitDescriptor}). Supervisor exiting.`);
|
log(`Worker exited (${exitDescriptor}). Supervisor exiting.`);
|
||||||
exitSupervisor(typeof code === "number" ? code : 0);
|
exitSupervisor(typeof code === "number" ? code : 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
child.on("close", (code, signal) => {
|
||||||
|
logLaunch("worker emitted close", {
|
||||||
|
childPid: child?.pid ?? null,
|
||||||
|
code,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("disconnect", () => {
|
||||||
|
logLaunch("worker emitted disconnect", {
|
||||||
|
childPid: child?.pid ?? null,
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestRestart = (reason: string) => {
|
const requestRestart = (reason: string) => {
|
||||||
@@ -169,6 +252,12 @@ export function runSupervisor(options: SupervisorOptions): void {
|
|||||||
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
||||||
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
||||||
|
|
||||||
|
logLaunch("supervisor starting", {
|
||||||
|
startupMessage: options.startupMessage,
|
||||||
|
restartOnCrash,
|
||||||
|
workerArgs,
|
||||||
|
workerExecArgv,
|
||||||
|
});
|
||||||
process.stdout.write(`[${options.name}] ${options.startupMessage}\n`);
|
process.stdout.write(`[${options.name}] ${options.startupMessage}\n`);
|
||||||
spawnWorker();
|
spawnWorker();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user