Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions scripts/patch-linux-window-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const {
applyLinuxSettingsPersistencePatch,
} = require("./patches/impl/launch-actions.js");
const {
applyLinuxBootstrapFailureExitPatch,
applyLinuxMultiInstanceBootstrapPatch,
} = require("./patches/impl/bootstrap.js");
const {
Expand All @@ -130,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,
Expand Down Expand Up @@ -924,6 +929,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",
Expand Down Expand Up @@ -4547,6 +4553,208 @@ 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("");
}

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: "skipped-optional" },
],
);
assert.equal(criticalFailuresFromReport(report).length, 1);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
}
});

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,
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);";
Expand Down
33 changes: 23 additions & 10 deletions scripts/patches/core/all-linux/extracted-app/bootstrap/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,29 @@ 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,
// 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,
}),
];
81 changes: 77 additions & 4 deletions scripts/patches/impl/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@ 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$]*)\)/;
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 (
Expand Down Expand Up @@ -57,12 +88,39 @@ function applyLinuxMultiInstanceBootstrapPatch(currentSource) {
return 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" };
// 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)) {
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 = resolveBootstrapBundle(extractedDir);

const source = fs.readFileSync(target, "utf8");
const patched = applyLinuxMultiInstanceBootstrapPatch(source);
if (patched === source) {
Expand All @@ -73,7 +131,22 @@ function patchLinuxMultiInstanceBootstrap(extractedDir) {
return { changed: true };
}

function patchLinuxBootstrapFailureExit(extractedDir) {
const target = resolveBootstrapBundle(extractedDir);

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,
};