mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Keep the daemon running when nobody reads its output (#6117)
* fix(server): keep the daemon running when its stdout reader is gone `paseo daemon run` in the foreground hands its stdout and stderr to the supervisor. When the program that started it closed its end, as `paseo daemon run | head -1` does, the supervisor's next write failed with EPIPE and nothing handled the error. The supervisor exited 1, the worker lost its parent and exited too, and the CLI reported "Daemon failed to start". The supervisor now drops output nobody reads and keeps supervising the worker. daemon.log still records everything. Other stdout and stderr errors surface as before. Closes #6114 * test(server): fail on unexpected daemon.log read errors and check stderr reaches the log --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,8 @@ async function runSupervisorFixture(options: {
|
||||
fileSizeLimitBlocks?: number;
|
||||
/** Occupy the log path with a directory so the supervisor cannot open daemon.log. */
|
||||
blockLogPath?: boolean;
|
||||
/** Close the reading end of the supervisor's stdout and stderr as soon as it starts. */
|
||||
closeOutput?: boolean;
|
||||
}): Promise<{
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
@@ -86,6 +88,11 @@ async function runSupervisorFixture(options: {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
if (options.closeOutput) {
|
||||
child.stdout.destroy();
|
||||
child.stderr.destroy();
|
||||
}
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
@@ -116,10 +123,19 @@ async function runSupervisorFixture(options: {
|
||||
});
|
||||
});
|
||||
|
||||
const log = await readFile(logPath, "utf8");
|
||||
const log = await readLogIfWritten(logPath);
|
||||
return { code, signal, elapsedMs: Date.now() - startedAt, log, stdout, stderr };
|
||||
}
|
||||
|
||||
async function readLogIfWritten(logPath: string): Promise<string> {
|
||||
try {
|
||||
return await readFile(logPath, "utf8");
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
describe("supervisor durable logging", () => {
|
||||
test("resolves rotation defaults", () => {
|
||||
const paseoHome = path.join(path.sep, "tmp", "paseo-home");
|
||||
@@ -189,6 +205,30 @@ describe("supervisor durable logging", () => {
|
||||
expect(result.stderr).toContain('"worker-json-stderr"');
|
||||
});
|
||||
|
||||
test("keeps supervising the worker when nobody reads its stdout or stderr", async () => {
|
||||
const result = await runSupervisorFixture({
|
||||
closeOutput: true,
|
||||
workerSource: `
|
||||
process.on("message", (message) => {
|
||||
if (message?.type === "paseo:graceful-shutdown") process.exit(0);
|
||||
});
|
||||
process.stdout.write("first stdout line\\n");
|
||||
process.stderr.write("first stderr line\\n");
|
||||
setTimeout(() => {
|
||||
process.stdout.write("later stdout line\\n");
|
||||
process.send?.({ type: "paseo:shutdown", reason: "closed_output_probe" });
|
||||
}, 500);
|
||||
setInterval(() => {}, 1000);
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.log).toContain("first stderr line\n");
|
||||
expect(result.log).toContain("later stdout line\n");
|
||||
expect(result.log).toContain('"reason":"closed_output_probe"');
|
||||
});
|
||||
|
||||
test("preserves raw non-JSON stdout and stderr lines", async () => {
|
||||
const result = await runSupervisorFixture({
|
||||
workerSource: `
|
||||
|
||||
@@ -166,6 +166,15 @@ function createDurableLog(
|
||||
};
|
||||
}
|
||||
|
||||
// The program that started the daemon can close its end of stdout or stderr, as
|
||||
// `paseo daemon run | head -1` does. Drop the output nobody reads and keep supervising
|
||||
// the worker; daemon.log still records it.
|
||||
function ignoreClosedOutput(error: NodeJS.ErrnoException): void {
|
||||
if (error.code !== "EPIPE") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function runSupervisor(options: SupervisorOptions): SupervisorController {
|
||||
const restartOnCrash = options.restartOnCrash ?? false;
|
||||
const workerArgs = options.workerArgs ?? process.argv.slice(2);
|
||||
@@ -482,6 +491,8 @@ export function runSupervisor(options: SupervisorOptions): SupervisorController
|
||||
|
||||
process.on("SIGINT", () => forwardSignal("SIGINT"));
|
||||
process.on("SIGTERM", () => forwardSignal("SIGTERM"));
|
||||
process.stdout.on("error", ignoreClosedOutput);
|
||||
process.stderr.on("error", ignoreClosedOutput);
|
||||
|
||||
process.stdout.write(`[${options.name}] ${options.startupMessage}\n`);
|
||||
writeLifecycleLog(options.startupMessage);
|
||||
|
||||
Reference in New Issue
Block a user