fix(cli): surface fatal [STARTUP] boot diagnostics without --log (#13314) (#13779)

Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-09-16 06:15:15 -03:00
committed by GitHub
parent 94220af289
commit 0459c6dc0a
4 changed files with 108 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ import { stopProcessGracefully } from "../../../src/shared/platform/windowsProce
import {
isFatalInstrumentationHookFailure,
formatAndroidInstrumentationFailureHint,
isFatalStartupDiagnostic,
} from "../utils/ensureAndroidCacheDir.mjs";
const CRASH_LOG_LINES = 50;
@@ -55,12 +56,14 @@ export class ServerSupervisor {
this.child = null;
this.isShuttingDown = false;
this.instrumentationFailureHintPrinted = false;
this.fatalStartupDiagnosticPrinted = false;
}
start() {
this.startedAt = Date.now();
this.crashLog = [];
this.instrumentationFailureHintPrinted = false;
this.fatalStartupDiagnosticPrinted = false;
const showLog = process.env.OMNIROUTE_SHOW_LOG === "1";
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
@@ -99,6 +102,15 @@ export class ServerSupervisor {
)
);
}
// #13314: surface any `[STARTUP] Fatal:`-guarded boot diagnostic
// immediately, even without --log — otherwise it is only buffered and
// reaches the operator on exit/crash, which never happens when the
// HTTP listener still comes up after the fatal failure (every route
// then 500s with zero visible diagnostic anywhere).
if (!this.fatalStartupDiagnosticPrinted && isFatalStartupDiagnostic(text)) {
this.fatalStartupDiagnosticPrinted = true;
process.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
}
};
if (this.child.stdout) {

View File

@@ -105,6 +105,27 @@ export function isFatalInstrumentationHookFailure(text) {
return /Unsupported platform:\s*android/i.test(text);
}
/**
* Detect any fatal boot-time diagnostic guarded by the `[STARTUP] Fatal:`
* prefix (`src/instrumentation-node.ts::ensureDbReadyForBoot()`,
* `src/instrumentation.ts::register()`, and any future guard using the same
* marker). #13314: in the default `omniroute serve` mode (no `--log`),
* `ServerSupervisor` only buffers stdout/stderr and flushes it to the real
* console on exit/crash/readiness-timeout — so if the HTTP listener still
* comes up after a fatal boot diagnostic was already printed (e.g. the
* better-sqlite3 / node:sqlite driver cascade failing hard), the operator
* sees "OmniRoute is running!" with zero visible diagnostic anywhere, and
* every route 500s. This generalizes the #10028 Android/Termux carve-out to
* every `[STARTUP] Fatal:` guard, not just that one platform-specific string.
*
* @param {string} text
* @returns {boolean}
*/
export function isFatalStartupDiagnostic(text) {
if (!text) return false;
return /^\[STARTUP\] Fatal:/m.test(text);
}
/**
* Operator-facing hint when that instrumentation failure shows up in child
* output — defense in depth if prep was skipped or a future Next.js probe

View File

@@ -0,0 +1 @@
- **fix(cli):** `omniroute serve` now surfaces a fatal `[STARTUP] Fatal: ...` boot diagnostic (e.g. a DB driver init failure) to the console immediately, even without `--log`, instead of only when the process later crashes or restarts (#13314) — thanks @Orion1943

View File

@@ -0,0 +1,74 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { writeFileSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
// #13314: ServerSupervisor's default (non `--log`) mode only buffers a
// fatal `[STARTUP] Fatal: ...` boot diagnostic in-memory and flushes it to
// the real console only when the child process exits. If the HTTP listener
// still comes up after a fatal DB-driver-cascade failure was already
// printed, the operator sees "OmniRoute is running!" with every route
// 500ing and zero diagnostic output anywhere. This regression test asserts
// the fatal line now reaches the real console immediately, not only on
// exit/crash.
test("ServerSupervisor surfaces a fatal [STARTUP] Fatal: boot diagnostic to the real console even when the child never exits (default, non --log mode)", async () => {
const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs");
delete process.env.OMNIROUTE_SHOW_LOG;
const dir = mkdtempSync(join(tmpdir(), "omniroute-issue13314-"));
const childScript = join(dir, "fake-server.mjs");
writeFileSync(
childScript,
`
console.error("[STARTUP] Fatal: Database driver initialization failed: better-sqlite3 invalid, node:sqlite fallback also failed");
console.log("Ready on 0.0.0.0:20128");
setInterval(() => {}, 1000);
`
);
const seenOnRealConsole: string[] = [];
const origStdoutWrite = process.stdout.write.bind(process.stdout);
const origStderrWrite = process.stderr.write.bind(process.stderr);
process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
seenOnRealConsole.push(String(chunk));
// @ts-expect-error - forwarding varargs to the real writer
return origStdoutWrite(chunk, ...rest);
}) as typeof process.stdout.write;
process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
seenOnRealConsole.push(String(chunk));
// @ts-expect-error - forwarding varargs to the real writer
return origStderrWrite(chunk, ...rest);
}) as typeof process.stderr.write;
const supervisor = new ServerSupervisor({
serverPath: childScript,
env: { ...process.env },
maxRestarts: 2,
memoryLimit: 256,
});
try {
supervisor.start();
await new Promise((resolve) => setTimeout(resolve, 1500));
const bufferedLog = supervisor.getRecentLog().join("\n");
const printedToRealConsole = seenOnRealConsole.join("");
assert.match(
bufferedLog,
/\[STARTUP\] Fatal: Database driver initialization failed/,
"expected the fatal boot diagnostic to be captured into the supervisor's buffer"
);
assert.match(
printedToRealConsole,
/\[STARTUP\] Fatal: Database driver initialization failed/,
"expected the fatal boot diagnostic to reach the real console even though the child process never exits"
);
} finally {
process.stdout.write = origStdoutWrite;
process.stderr.write = origStderrWrite;
supervisor.stop();
}
});