From 803954e7d805d4d900721307fb0757579751631c Mon Sep 17 00:00:00 2001 From: jheinem Date: Thu, 16 Jul 2026 21:52:54 -0700 Subject: [PATCH 1/3] Exit cleanly after Linux bootstrap failure --- scripts/patch-linux-window-ui.test.js | 61 +++++++++++++++++++ .../extracted-app/bootstrap/patch.js | 32 +++++++--- scripts/patches/impl/bootstrap.js | 56 +++++++++++++++++ 3 files changed, 139 insertions(+), 10 deletions(-) diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index d26896cfa..337603278 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -113,6 +113,7 @@ const { applyLinuxSettingsPersistencePatch, } = require("./patches/impl/launch-actions.js"); const { + applyLinuxBootstrapFailureExitPatch, applyLinuxMultiInstanceBootstrapPatch, } = require("./patches/impl/bootstrap.js"); const { @@ -924,6 +925,7 @@ test("default core patch descriptors are grouped and unique", () => { "linux-native-titlebar", "linux-menu", "linux-multi-instance-bootstrap-lock", + "linux-bootstrap-failure-exit", "linux-set-icon", "linux-resize-repaint", "linux-opaque-background", @@ -4547,6 +4549,65 @@ test("upgrades the legacy guarded bootstrap single-instance lock to the enforced assert.ok(!patched.includes("&&process.env.CODEX_LINUX_MULTI_LAUNCH")); }); +function bootstrapFailureBundleFixture() { + return [ + "async function boot(){try{throw Error(`boom`)}catch(e){", + "for(let t of i.BrowserWindow.getAllWindows())t.isDestroyed()||t.destroy();", + "l.ei().error(`Desktop bootstrap failed to start the main app`,{safe:{phase:`bootstrap-import-main`}}),", + "n.captureException(e,{tags:{phase:`bootstrap-import-main`}}),await oe(e)}}", + ].join(""); +} + +test("bounds Linux bootstrap failure dialogs and exits the failed instance", () => { + const patched = applyPatchTwice( + applyLinuxBootstrapFailureExitPatch, + bootstrapFailureBundleFixture(), + ); + + assert.match( + patched, + /process\.platform===`linux`\?Promise\.race\(\[oe\(e\),new Promise\(e=>setTimeout\(e,15e3\)\)\]\):oe\(e\)/, + ); + assert.match(patched, /process\.platform===`linux`&&i\.app\.exit\(1\)/); + assert.equal((patched.match(/i\.app\.exit\(1\)/g) ?? []).length, 1); +}); + +test("Linux bootstrap failure exits even when the native dialog never resolves", async () => { + const patched = applyLinuxBootstrapFailureExitPatch(bootstrapFailureBundleFixture()); + const calls = { capture: 0, destroy: 0, exit: [] }; + const context = { + Error, + Promise, + process: { platform: "linux" }, + setTimeout(callback) { + callback(); + return 1; + }, + i: { + BrowserWindow: { + getAllWindows: () => [{ + destroy: () => { calls.destroy += 1; }, + isDestroyed: () => false, + }], + }, + app: { + exit: (status) => calls.exit.push(status), + }, + }, + l: { ei: () => ({ error() {} }) }, + n: { captureException: () => { calls.capture += 1; } }, + oe: () => new Promise(() => {}), + }; + context.globalThis = context; + + vm.runInNewContext(`${patched};globalThis.runBootstrap=boot`, context); + await context.runBootstrap(); + + assert.equal(calls.destroy, 1); + assert.equal(calls.capture, 1); + assert.deepEqual(calls.exit, [1]); +}); + test("enforced bootstrap lock takes the Linux lock with upstream flag off and exits the loser", () => { const source = "var S=t.x({isMacOS:b,isPackaged:n.app.isPackaged});if(!(!S||n.app.requestSingleInstanceLock()))n.app.exit(0);"; diff --git a/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js b/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js index db3886557..9cc705f22 100644 --- a/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js +++ b/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js @@ -4,16 +4,28 @@ const { extractedAppPatch, } = require("../../../../descriptor.js"); const { + patchLinuxBootstrapFailureExit, patchLinuxMultiInstanceBootstrap, } = require("../../../../impl/bootstrap.js"); -module.exports = extractedAppPatch({ - id: "linux-multi-instance-bootstrap-lock", - phase: "extracted-app:pre-webview", - order: 125, - // On bundles where bootstrap.js owns the single-instance lock, this is the - // only duplicate-instance protection Linux gets (the main-bundle patch - // defers to it), so a drifted needle must fail the build, not warn. - ciPolicy: "required-upstream", - apply: patchLinuxMultiInstanceBootstrap, -}); +module.exports = [ + extractedAppPatch({ + id: "linux-multi-instance-bootstrap-lock", + phase: "extracted-app:pre-webview", + order: 125, + // On bundles where bootstrap.js owns the single-instance lock, this is the + // only duplicate-instance protection Linux gets (the main-bundle patch + // defers to it), so a drifted needle must fail the build, not warn. + ciPolicy: "required-upstream", + apply: patchLinuxMultiInstanceBootstrap, + }), + extractedAppPatch({ + id: "linux-bootstrap-failure-exit", + phase: "extracted-app:pre-webview", + order: 126, + // A missed handler leaves a windowless process holding the single-instance + // lock, so current upstream drift must reject the generated app. + ciPolicy: "required-upstream", + apply: patchLinuxBootstrapFailureExit, + }), +]; diff --git a/scripts/patches/impl/bootstrap.js b/scripts/patches/impl/bootstrap.js index 9f1382e27..7b39ef7c4 100644 --- a/scripts/patches/impl/bootstrap.js +++ b/scripts/patches/impl/bootstrap.js @@ -21,6 +21,12 @@ const guardedLockRegex = const unguardedLockRegex = /if\(!\(!([A-Za-z_$][\w$]*)\|\|([A-Za-z_$][\w$]*)\.app\.requestSingleInstanceLock\(\)\)\)/; +const bootstrapFailureTailRegex = + /(for\(let ([A-Za-z_$][\w$]*) of ([A-Za-z_$][\w$]*)\.BrowserWindow\.getAllWindows\(\)\)\2\.isDestroyed\(\)\|\|\2\.destroy\(\);[^]{0,400}?`Desktop bootstrap failed to start the main app`[^]{0,400}?),await ([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)/; +const patchedBootstrapFailureTailRegex = + /Desktop bootstrap failed to start the main app[^]*?process\.platform===`linux`\?Promise\.race\([^]*?process\.platform===`linux`&&[A-Za-z_$][\w$]*\.app\.exit\(1\)/; +const linuxBootstrapFailureExitTimeoutMs = "15e3"; + function enforcedLockCondition(enabledVar, appVar) { return ( "if(!(process.platform===`linux`?process.env.CODEX_LINUX_MULTI_LAUNCH===`1`||" + @@ -57,6 +63,38 @@ function applyLinuxMultiInstanceBootstrapPatch(currentSource) { return currentSource; } +// The upstream bootstrap catches failures from runMainAppStartup(), destroys +// every BrowserWindow, and waits on a native error dialog. On Linux, the main +// bundle may already have created the warm-start socket before that failure. +// If the native dialog is hidden or never resolves, the windowless process +// keeps the single-instance lock and acknowledges every later launch forever. +// Bound the dialog wait and terminate the failed bootstrap so the launcher can +// perform a real cold start on the next attempt. +function applyLinuxBootstrapFailureExitPatch(currentSource) { + if (patchedBootstrapFailureTailRegex.test(currentSource)) { + return currentSource; + } + + if (!bootstrapFailureTailRegex.test(currentSource)) { + if (currentSource.includes("Desktop bootstrap failed to start the main app")) { + console.warn( + "WARN: Could not find bootstrap failure handler — Linux failed starts may retain the single-instance lock", + ); + } + return currentSource; + } + + return currentSource.replace( + bootstrapFailureTailRegex, + (_match, prefix, _windowVar, electronVar, failureDialogVar, errorVar) => { + const boundedFailureDialog = + `await (process.platform===\`linux\`?Promise.race([${failureDialogVar}(${errorVar}),` + + `new Promise(e=>setTimeout(e,${linuxBootstrapFailureExitTimeoutMs}))]):${failureDialogVar}(${errorVar}))`; + return `${prefix},${boundedFailureDialog},process.platform===\`linux\`&&${electronVar}.app.exit(1)`; + }, + ); +} + function patchLinuxMultiInstanceBootstrap(extractedDir) { const target = path.join(extractedDir, ".vite", "build", "bootstrap.js"); if (!fs.existsSync(target)) { @@ -73,7 +111,25 @@ function patchLinuxMultiInstanceBootstrap(extractedDir) { return { changed: true }; } +function patchLinuxBootstrapFailureExit(extractedDir) { + const target = path.join(extractedDir, ".vite", "build", "bootstrap.js"); + if (!fs.existsSync(target)) { + return { changed: false, reason: "bootstrap.js not found" }; + } + + const source = fs.readFileSync(target, "utf8"); + const patched = applyLinuxBootstrapFailureExitPatch(source); + if (patched === source) { + return { changed: false }; + } + + fs.writeFileSync(target, patched, "utf8"); + return { changed: true }; +} + module.exports = { + applyLinuxBootstrapFailureExitPatch, applyLinuxMultiInstanceBootstrapPatch, + patchLinuxBootstrapFailureExit, patchLinuxMultiInstanceBootstrap, }; From 70e42c6eeba2b85a2c33f4ca0d729407ee107e02 Mon Sep 17 00:00:00 2001 From: Yo_DDV <116607353+Yo-DDV@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:55:38 +0200 Subject: [PATCH 2/3] fix: resolve current bootstrap bundle target --- scripts/patch-linux-window-ui.test.js | 113 ++++++++++++++++++++++++++ scripts/patches/impl/bootstrap.js | 35 ++++++-- 2 files changed, 140 insertions(+), 8 deletions(-) diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index 337603278..1e63c4aad 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -131,10 +131,14 @@ const { featurePatchDescriptors, } = require("./patches/runner.js"); const { + applyExtractedAppPatchDescriptors, applyMainBundlePatchDescriptors, discoverCorePatchDescriptors, normalizePatchDescriptors, } = require("./patches/engine.js"); +const bootstrapPatchDescriptors = require( + "./patches/core/all-linux/extracted-app/bootstrap/patch.js", +); const { detectLinuxTargetContext, linuxTargetSummary, @@ -4558,6 +4562,115 @@ function bootstrapFailureBundleFixture() { ].join(""); } +function currentBootstrapBundleFixture() { + return [ + "var S=t.x({isMacOS:b,isPackaged:i.app.isPackaged});", + "if(!(!S||i.app.requestSingleInstanceLock()))i.app.exit(0);", + bootstrapFailureBundleFixture(), + ].join(""); +} + +function applyBootstrapDescriptors(extractedDir) { + const report = createPatchReport(); + applyExtractedAppPatchDescriptors( + extractedDir, + normalizePatchDescriptors(bootstrapPatchDescriptors), + {}, + report, + "extracted-app:pre-webview", + ); + return report; +} + +test("patches the hashed bootstrap bundle loaded by the production entrypoint", () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codex-bootstrap-layout-")); + try { + const buildDir = path.join(tempRoot, ".vite", "build"); + const bundlePath = path.join(buildDir, "bootstrap-C6R0_AGB.js"); + fs.mkdirSync(buildDir, { recursive: true }); + fs.writeFileSync( + path.join(buildDir, "early-bootstrap.js"), + 'require("./src-C7E6KJ89.js"),Promise.resolve().then(()=>require("./bootstrap-C6R0_AGB.js"));', + ); + fs.writeFileSync(bundlePath, currentBootstrapBundleFixture()); + + const firstReport = applyBootstrapDescriptors(tempRoot); + const patched = fs.readFileSync(bundlePath, "utf8"); + assert.match(patched, /CODEX_LINUX_MULTI_LAUNCH/); + assert.match(patched, /process\.platform===`linux`&&i\.app\.exit\(1\)/); + assert.deepEqual( + firstReport.patches.map(({ name, status }) => ({ name, status })), + [ + { name: "linux-multi-instance-bootstrap-lock", status: "applied" }, + { name: "linux-bootstrap-failure-exit", status: "applied" }, + ], + ); + + const secondReport = applyBootstrapDescriptors(tempRoot); + assert.equal(fs.readFileSync(bundlePath, "utf8"), patched); + assert.deepEqual( + secondReport.patches.map(({ name, status }) => ({ name, status })), + [ + { name: "linux-multi-instance-bootstrap-lock", status: "already-applied" }, + { name: "linux-bootstrap-failure-exit", status: "already-applied" }, + ], + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +}); + +test("fails required bootstrap patches when the production target is missing or ambiguous", async (t) => { + const cases = [ + { + name: "obsolete adjacent bootstrap.js only", + entrypoint: null, + bundles: [["bootstrap.js", currentBootstrapBundleFixture()]], + }, + { + name: "referenced hashed bundle missing", + entrypoint: 'require("./bootstrap-missing.js");', + bundles: [], + }, + { + name: "multiple hashed bundles referenced", + entrypoint: 'require("./bootstrap-one.js");require("./bootstrap-two.js");', + bundles: [ + ["bootstrap-one.js", currentBootstrapBundleFixture()], + ["bootstrap-two.js", currentBootstrapBundleFixture()], + ], + }, + ]; + + for (const fixture of cases) { + await t.test(fixture.name, () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codex-bootstrap-drift-")); + try { + const buildDir = path.join(tempRoot, ".vite", "build"); + fs.mkdirSync(buildDir, { recursive: true }); + if (fixture.entrypoint != null) { + fs.writeFileSync(path.join(buildDir, "early-bootstrap.js"), fixture.entrypoint); + } + for (const [name, source] of fixture.bundles) { + fs.writeFileSync(path.join(buildDir, name), source); + } + + const report = applyBootstrapDescriptors(tempRoot); + assert.deepEqual( + report.patches.map(({ name, status }) => ({ name, status })), + [ + { name: "linux-multi-instance-bootstrap-lock", status: "failed-required" }, + { name: "linux-bootstrap-failure-exit", status: "failed-required" }, + ], + ); + assert.equal(criticalFailuresFromReport(report).length, 2); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + } +}); + test("bounds Linux bootstrap failure dialogs and exits the failed instance", () => { const patched = applyPatchTwice( applyLinuxBootstrapFailureExitPatch, diff --git a/scripts/patches/impl/bootstrap.js b/scripts/patches/impl/bootstrap.js index 7b39ef7c4..b9e244d72 100644 --- a/scripts/patches/impl/bootstrap.js +++ b/scripts/patches/impl/bootstrap.js @@ -20,6 +20,7 @@ const guardedLockRegex = /if\(!\(!([A-Za-z_$][\w$]*)\|\|process\.platform===`linux`&&process\.env\.CODEX_LINUX_MULTI_LAUNCH===`1`\|\|([A-Za-z_$][\w$]*)\.app\.requestSingleInstanceLock\(\)\)\)/; const unguardedLockRegex = /if\(!\(!([A-Za-z_$][\w$]*)\|\|([A-Za-z_$][\w$]*)\.app\.requestSingleInstanceLock\(\)\)\)/; +const bootstrapImportRegex = /require\((["'])\.\/(bootstrap-[A-Za-z0-9_-]+\.js)\1\)/g; const bootstrapFailureTailRegex = /(for\(let ([A-Za-z_$][\w$]*) of ([A-Za-z_$][\w$]*)\.BrowserWindow\.getAllWindows\(\)\)\2\.isDestroyed\(\)\|\|\2\.destroy\(\);[^]{0,400}?`Desktop bootstrap failed to start the main app`[^]{0,400}?),await ([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)/; @@ -27,6 +28,30 @@ const patchedBootstrapFailureTailRegex = /Desktop bootstrap failed to start the main app[^]*?process\.platform===`linux`\?Promise\.race\([^]*?process\.platform===`linux`&&[A-Za-z_$][\w$]*\.app\.exit\(1\)/; const linuxBootstrapFailureExitTimeoutMs = "15e3"; +function resolveBootstrapBundle(extractedDir) { + const buildDir = path.join(extractedDir, ".vite", "build"); + const entrypoint = path.join(buildDir, "early-bootstrap.js"); + if (!fs.existsSync(entrypoint)) { + throw new Error(`Could not find current bootstrap entrypoint at ${entrypoint}`); + } + + const entrypointSource = fs.readFileSync(entrypoint, "utf8"); + const bundleNames = [ + ...new Set(Array.from(entrypointSource.matchAll(bootstrapImportRegex), (match) => match[2])), + ]; + if (bundleNames.length !== 1) { + throw new Error( + `Expected exactly one hashed bootstrap bundle in ${entrypoint}, found ${bundleNames.length}`, + ); + } + + const target = path.join(buildDir, bundleNames[0]); + if (!fs.existsSync(target) || !fs.statSync(target).isFile()) { + throw new Error(`Bootstrap bundle referenced by ${entrypoint} was not found at ${target}`); + } + return target; +} + function enforcedLockCondition(enabledVar, appVar) { return ( "if(!(process.platform===`linux`?process.env.CODEX_LINUX_MULTI_LAUNCH===`1`||" + @@ -96,10 +121,7 @@ function applyLinuxBootstrapFailureExitPatch(currentSource) { } function patchLinuxMultiInstanceBootstrap(extractedDir) { - const target = path.join(extractedDir, ".vite", "build", "bootstrap.js"); - if (!fs.existsSync(target)) { - return { changed: false, reason: "bootstrap.js not found" }; - } + const target = resolveBootstrapBundle(extractedDir); const source = fs.readFileSync(target, "utf8"); const patched = applyLinuxMultiInstanceBootstrapPatch(source); @@ -112,10 +134,7 @@ function patchLinuxMultiInstanceBootstrap(extractedDir) { } function patchLinuxBootstrapFailureExit(extractedDir) { - const target = path.join(extractedDir, ".vite", "build", "bootstrap.js"); - if (!fs.existsSync(target)) { - return { changed: false, reason: "bootstrap.js not found" }; - } + const target = resolveBootstrapBundle(extractedDir); const source = fs.readFileSync(target, "utf8"); const patched = applyLinuxBootstrapFailureExitPatch(source); From 25ab6a0f8b8e7c92acfc492ff611d6091375d09c Mon Sep 17 00:00:00 2001 From: Gary Lysenko Date: Sun, 19 Jul 2026 21:48:55 +0300 Subject: [PATCH 3/3] Make bootstrap failure recovery patch optional --- scripts/patch-linux-window-ui.test.js | 38 ++++++++++++++++++- .../extracted-app/bootstrap/patch.js | 7 ++-- scripts/patches/impl/bootstrap.js | 8 ++-- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index 1e63c4aad..23a10ec12 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -4660,10 +4660,10 @@ test("fails required bootstrap patches when the production target is missing or report.patches.map(({ name, status }) => ({ name, status })), [ { name: "linux-multi-instance-bootstrap-lock", status: "failed-required" }, - { name: "linux-bootstrap-failure-exit", status: "failed-required" }, + { name: "linux-bootstrap-failure-exit", status: "skipped-optional" }, ], ); - assert.equal(criticalFailuresFromReport(report).length, 2); + assert.equal(criticalFailuresFromReport(report).length, 1); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } @@ -4671,6 +4671,40 @@ test("fails required bootstrap patches when the production target is missing or } }); +test("warns without failing when the optional bootstrap failure handler drifts", () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "codex-bootstrap-handler-drift-")); + try { + const buildDir = path.join(tempRoot, ".vite", "build"); + const bundlePath = path.join(buildDir, "bootstrap-current.js"); + fs.mkdirSync(buildDir, { recursive: true }); + fs.writeFileSync( + path.join(buildDir, "early-bootstrap.js"), + 'require("./bootstrap-current.js");', + ); + fs.writeFileSync( + bundlePath, + "var S=t.x({isMacOS:b,isPackaged:i.app.isPackaged});" + + "if(!(!S||i.app.requestSingleInstanceLock()))i.app.exit(0);", + ); + + const report = applyBootstrapDescriptors(tempRoot); + assert.deepEqual( + report.patches.map(({ name, status }) => ({ name, status })), + [ + { name: "linux-multi-instance-bootstrap-lock", status: "applied" }, + { name: "linux-bootstrap-failure-exit", status: "skipped-optional" }, + ], + ); + assert.equal(criticalFailuresFromReport(report).length, 0); + assert.match( + report.patches[1].reason, + /Could not find bootstrap failure handler/, + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +}); + test("bounds Linux bootstrap failure dialogs and exits the failed instance", () => { const patched = applyPatchTwice( applyLinuxBootstrapFailureExitPatch, diff --git a/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js b/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js index 9cc705f22..b196febb2 100644 --- a/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js +++ b/scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js @@ -23,9 +23,10 @@ module.exports = [ id: "linux-bootstrap-failure-exit", phase: "extracted-app:pre-webview", order: 126, - // A missed handler leaves a windowless process holding the single-instance - // lock, so current upstream drift must reject the generated app. - ciPolicy: "required-upstream", + // This recovery keeps a failed Linux bootstrap from retaining the + // single-instance lock, but drift should remain fail-soft so a changed + // upstream failure path does not reject an otherwise usable app. + ciPolicy: "optional", apply: patchLinuxBootstrapFailureExit, }), ]; diff --git a/scripts/patches/impl/bootstrap.js b/scripts/patches/impl/bootstrap.js index b9e244d72..6e6600c35 100644 --- a/scripts/patches/impl/bootstrap.js +++ b/scripts/patches/impl/bootstrap.js @@ -101,11 +101,9 @@ function applyLinuxBootstrapFailureExitPatch(currentSource) { } if (!bootstrapFailureTailRegex.test(currentSource)) { - if (currentSource.includes("Desktop bootstrap failed to start the main app")) { - console.warn( - "WARN: Could not find bootstrap failure handler — Linux failed starts may retain the single-instance lock", - ); - } + console.warn( + "WARN: Could not find bootstrap failure handler — Linux failed starts may retain the single-instance lock", + ); return currentSource; }