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:
@@ -124,11 +124,16 @@ function resolveDaemonRunnerEntry(): string {
|
||||
try {
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string };
|
||||
if (packageJson.name === "@getpaseo/server") {
|
||||
const distRunner = path.join(currentDir, "dist", "scripts", "daemon-runner.js");
|
||||
const distRunner = path.join(
|
||||
currentDir,
|
||||
"dist",
|
||||
"scripts",
|
||||
"supervisor-entrypoint.js",
|
||||
);
|
||||
if (existsSync(distRunner)) {
|
||||
return distRunner;
|
||||
}
|
||||
return path.join(currentDir, "scripts", "daemon-runner.ts");
|
||||
return path.join(currentDir, "scripts", "supervisor-entrypoint.ts");
|
||||
}
|
||||
} catch {
|
||||
// Continue searching up if package.json exists but is invalid.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/**
|
||||
* Regression: `paseo daemon stop` must stop supervised dev daemons
|
||||
* without allowing daemon-runner to respawn a new worker process.
|
||||
* without allowing the supervisor entrypoint to respawn a new worker process.
|
||||
*/
|
||||
|
||||
import assert from "node:assert";
|
||||
@@ -120,20 +120,24 @@ let supervisorProcess: ChildProcess | null = null;
|
||||
let recentSupervisorLogs = "";
|
||||
|
||||
try {
|
||||
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
|
||||
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
|
||||
|
||||
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
|
||||
cwd: cliRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
...testEnv,
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: `127.0.0.1:${port}`,
|
||||
PASEO_RELAY_ENABLED: "false",
|
||||
CI: "true",
|
||||
supervisorProcess = spawn(
|
||||
"npx",
|
||||
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
|
||||
{
|
||||
cwd: cliRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
...testEnv,
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: `127.0.0.1:${port}`,
|
||||
PASEO_RELAY_ENABLED: "false",
|
||||
CI: "true",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
);
|
||||
|
||||
supervisorProcess.stdout?.on("data", (chunk) => {
|
||||
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
|
||||
@@ -161,8 +165,9 @@ try {
|
||||
const command = readProcessCommand(daemonPid);
|
||||
assert(command !== null, "pid lock pid should resolve to a running process command");
|
||||
assert(
|
||||
command.includes("daemon-runner.ts") || command.includes("daemon-runner.js"),
|
||||
`pid lock pid should be daemon-runner process, got: ${command}`,
|
||||
command.includes("supervisor-entrypoint.ts") ||
|
||||
command.includes("supervisor-entrypoint.js"),
|
||||
`pid lock pid should be supervisor-entrypoint process, got: ${command}`,
|
||||
);
|
||||
console.log(`✓ dev daemon started with daemon pid ${daemonPid}\n`);
|
||||
|
||||
@@ -186,7 +191,7 @@ try {
|
||||
await waitFor(
|
||||
() => !isProcessRunning(supervisorProcess!.pid ?? -1),
|
||||
15000,
|
||||
"daemon-runner supervisor remained running after stop",
|
||||
"supervisor-entrypoint process remained running after stop",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Regression: a single SIGINT sent to a supervised daemon-runner must allow
|
||||
* Regression: a single SIGINT sent to the supervised supervisor entrypoint must allow
|
||||
* graceful daemon lifecycle shutdown to complete (no early forced exit path).
|
||||
*/
|
||||
|
||||
@@ -131,21 +131,25 @@ let supervisorProcess: ChildProcess | null = null;
|
||||
let recentSupervisorLogs = "";
|
||||
|
||||
try {
|
||||
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
|
||||
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
|
||||
|
||||
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
|
||||
cwd: cliRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
...testEnv,
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: `127.0.0.1:${port}`,
|
||||
PASEO_RELAY_ENABLED: "false",
|
||||
CI: "true",
|
||||
supervisorProcess = spawn(
|
||||
"npx",
|
||||
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
|
||||
{
|
||||
cwd: cliRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
...testEnv,
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: `127.0.0.1:${port}`,
|
||||
PASEO_RELAY_ENABLED: "false",
|
||||
CI: "true",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: process.platform !== "win32",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: process.platform !== "win32",
|
||||
});
|
||||
);
|
||||
|
||||
supervisorProcess.stdout?.on("data", (chunk) => {
|
||||
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
|
||||
|
||||
@@ -62,7 +62,7 @@ try {
|
||||
"-e",
|
||||
// Keep the process alive long enough for stop command assertions.
|
||||
"setInterval(() => {}, 1000)",
|
||||
"daemon-runner.ts",
|
||||
"supervisor-entrypoint.ts",
|
||||
],
|
||||
{
|
||||
env: {
|
||||
|
||||
@@ -122,20 +122,24 @@ let supervisorProcess: ChildProcess | null = null;
|
||||
let recentSupervisorLogs = "";
|
||||
|
||||
try {
|
||||
console.log("Test 1: start daemon-runner in dev mode with isolated PASEO_HOME");
|
||||
console.log("Test 1: start supervisor-entrypoint in dev mode with isolated PASEO_HOME");
|
||||
|
||||
supervisorProcess = spawn("npx", ["tsx", "../server/scripts/daemon-runner.ts", "--dev"], {
|
||||
cwd: cliRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
...testEnv,
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: host,
|
||||
PASEO_RELAY_ENABLED: "false",
|
||||
CI: "true",
|
||||
supervisorProcess = spawn(
|
||||
"npx",
|
||||
["tsx", "../server/scripts/supervisor-entrypoint.ts", "--dev"],
|
||||
{
|
||||
cwd: cliRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
...testEnv,
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: host,
|
||||
PASEO_RELAY_ENABLED: "false",
|
||||
CI: "true",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
);
|
||||
|
||||
supervisorProcess.stdout?.on("data", (chunk) => {
|
||||
recentSupervisorLogs = (recentSupervisorLogs + chunk.toString()).slice(-8000);
|
||||
|
||||
@@ -134,11 +134,6 @@ try {
|
||||
env: {
|
||||
...process.env,
|
||||
...testEnv,
|
||||
// This test validates direct unsupervised worker ownership semantics.
|
||||
// Agent-orchestrated shells may export PASEO_PID_LOCK_MODE=external,
|
||||
// which would delegate lock ownership away from this process and make
|
||||
// daemon status checks fail to observe a running owner PID.
|
||||
PASEO_PID_LOCK_MODE: "self",
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: host,
|
||||
PASEO_RELAY_ENABLED: "false",
|
||||
|
||||
Reference in New Issue
Block a user