diff --git a/changelog.d/fixes/7773-termux-sqlite-boot.md b/changelog.d/fixes/7773-termux-sqlite-boot.md new file mode 100644 index 0000000000..52759ed194 --- /dev/null +++ b/changelog.d/fixes/7773-termux-sqlite-boot.md @@ -0,0 +1 @@ +- fix(db): log the fatal boot-time SQLite driver-cascade failure before it propagates instead of swallowing it silently, so `app.log` captures the real cause on Termux/Android (#7773) diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts index cacb948740..44827ad3bb 100755 --- a/src/instrumentation-node.ts +++ b/src/instrumentation-node.ts @@ -82,6 +82,17 @@ export async function ensureDbReadyForBoot( } catch (err: unknown) { const normalized = normalizeBootError(err); if (!TRANSIENT_DB_CLOSED_RE.test(normalized.message)) { + // Fatal, non-transient boot-time DB init failure (e.g. the entire + // better-sqlite3 -> node:sqlite -> sql.js driver cascade failed, as on + // Termux/Android when no SQLite driver is usable). This runs BEFORE + // initConsoleInterceptor() is wired up, so this is the only chance to + // get the real root cause into stdout/app.log — without it, the + // process keeps its HTTP listener up while every DB-touching route + // 500s forever with a permanently empty log (#7773). + console.error( + "[STARTUP] Fatal: Database driver initialization failed:", + normalized.message + ); throw normalized; } console.warn( @@ -91,7 +102,12 @@ export async function ensureDbReadyForBoot( try { await ensureDbInitialized(); } catch (retryErr: unknown) { - throw normalizeBootError(retryErr); + const normalizedRetryErr = normalizeBootError(retryErr); + console.error( + "[STARTUP] Fatal: Database driver initialization failed after retry:", + normalizedRetryErr.message + ); + throw normalizedRetryErr; } } } diff --git a/tests/unit/instrumentation-node-boot-db-failure.test.ts b/tests/unit/instrumentation-node-boot-db-failure.test.ts new file mode 100644 index 0000000000..dafd033597 --- /dev/null +++ b/tests/unit/instrumentation-node-boot-db-failure.test.ts @@ -0,0 +1,115 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { ensureDbReadyForBoot } from "../../src/instrumentation-node"; + +// Regression guard for #7773: on Termux/Android, when the whole SQLite driver +// cascade (better-sqlite3 -> node:sqlite -> sql.js) fails at boot, +// ensureDbReadyForBoot() used to re-throw the fatal error WITHOUT ever +// logging it, and this happens before initConsoleInterceptor() runs — so the +// one message that would explain the crash never reached app.log. The server +// kept listening (hence "OmniRoute is running") but every DB-touching route +// 500s forever with a permanently empty log. + +function captureConsole(): { captured: string[]; restore: () => void } { + const originalError = console.error; + const originalWarn = console.warn; + const originalLog = console.log; + const captured: string[] = []; + const join = (args: unknown[]): string => args.map((arg) => String(arg)).join(" "); + console.error = (...args: unknown[]) => { + captured.push(join(args)); + }; + console.warn = (...args: unknown[]) => { + captured.push(join(args)); + }; + console.log = (...args: unknown[]) => { + captured.push(join(args)); + }; + return { + captured, + restore: () => { + console.error = originalError; + console.warn = originalWarn; + console.log = originalLog; + }, + }; +} + +test("issue #7773: a fatal (non-transient) boot-time DB init failure must be logged before it propagates", async () => { + const { captured, restore } = captureConsole(); + + // Mirrors the real message driverFactory.ts's openSqliteDatabase() throws once + // better-sqlite3 + node:sqlite are both unavailable AND sql.js pre-init itself + // failed (core.ts:172-176) — the exact shape a Termux install with no working + // SQLite driver at all would surface. + const fatalMessage = + "[DB] Nenhum driver SQLite disponível para '/data/data/com.termux/files/home/.omniroute/storage.sqlite'. " + + "Drivers testados: better-sqlite3 (falhou), node:sqlite (indisponível), " + + "sql.js (falhou: ENOENT: no such file or directory, open '.../sql-wasm.wasm')."; + + const fakeEnsureDbInitialized = async () => { + throw new Error(fatalMessage); + }; + + try { + await assert.rejects( + () => ensureDbReadyForBoot(fakeEnsureDbInitialized), + (err: Error) => err.message === fatalMessage + ); + + const loggedRootCause = captured.some((line) => line.includes(fatalMessage)); + assert.equal( + loggedRootCause, + true, + "Expected the fatal boot-time DB init failure to be logged (console.error/warn) " + + "before propagating, so app.log captures the real driver-cascade failure reason " + + "instead of staying empty (#7773). It was NOT logged." + ); + } finally { + restore(); + } +}); + +test("issue #7773: a fatal failure on the retry-after-#6560 path must also be logged before it propagates", async () => { + const { captured, restore } = captureConsole(); + + const fatalMessage = "[DB] sql.js pre-init failed: WASM asset missing on retry"; + let calls = 0; + const fakeEnsureDbInitialized = async () => { + calls += 1; + if (calls === 1) { + // Triggers the existing #6560 transient-retry branch first. + throw "Database closed"; + } + throw new Error(fatalMessage); + }; + + try { + await assert.rejects( + () => ensureDbReadyForBoot(fakeEnsureDbInitialized), + (err: Error) => err.message === fatalMessage + ); + assert.equal(calls, 2, "must have retried exactly once before the fatal retry failure"); + + const loggedRootCause = captured.some((line) => line.includes(fatalMessage)); + assert.equal( + loggedRootCause, + true, + "Expected the fatal retry-path DB init failure to also be logged before propagating (#7773)." + ); + } finally { + restore(); + } +}); + +test("ensureDbReadyForBoot does not log anything extra on a clean successful boot", async () => { + const { captured, restore } = captureConsole(); + const fakeEnsureDbInitialized = async () => {}; + + try { + await assert.doesNotReject(ensureDbReadyForBoot(fakeEnsureDbInitialized)); + assert.equal(captured.length, 0, "a successful boot must not emit any startup DB log lines"); + } finally { + restore(); + } +});