diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index eb609ac63..85b659130 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -84,6 +84,7 @@ const { applyLinuxExplicitQuitPromptBypassPatch, applyLinuxExplicitTrayQuitPatch, applyLinuxQuitGuardPatch, + applyLinuxSqliteBackfillQuitClosePatch, applyLinuxWillQuitDrainTimeoutPatch, } = require("./patches/impl/main-process/quit-lifecycle.js"); const { @@ -905,6 +906,7 @@ test("default core patch descriptors are grouped and unique", () => { "linux-quit-guard", "linux-ready-to-show-window-state", "linux-explicit-quit-prompt-bypass", + "linux-sqlite-backfill-quit-close", "linux-explicit-quit-drain-timeout", "linux-explicit-tray-quit", "linux-explicit-ipc-quit", @@ -983,6 +985,15 @@ test("default core patch descriptors are grouped and unique", () => { assert.equal(new Set(ids).size, ids.length); assert.deepEqual([...ids].sort(), [...expectedIds].sort()); + const lifecycleOrder = [ + "linux-quit-guard", + "linux-explicit-quit-prompt-bypass", + "linux-explicit-quit-drain-timeout", + "linux-tray", + "linux-single-instance", + "linux-launch-actions", + ].map((id) => ids.indexOf(id)); + assert.ok(lifecycleOrder.every((index, position) => position === 0 || lifecycleOrder[position - 1] < index)); assert.ok(descriptors.every((descriptor) => descriptor.sourcePath.includes(`${path.sep}core${path.sep}`))); assert.equal( descriptors.find((descriptor) => descriptor.id === "package-desktop-name")?.phase, @@ -1179,7 +1190,7 @@ test("subagent nickname metadata descriptor follows upstream metadata bundle nam function trayBundleFixture() { return [ - "async function Hw(e){return process.platform!==`win32`&&process.platform!==`darwin`?null:(zw=!0,Lw??Rw??(Rw=(async()=>{let r=await Ww(e.buildFlavor,e.appBrand,e.repoRoot),i=new n.Tray(r.defaultIcon);return i})()))}", + "var ire=!1;function G9(){zw=!1}async function Hw(e){return process.platform!==`win32`&&process.platform!==`darwin`?null:(zw=!0,Lw??Rw??(ire||=(n.app.on(`before-quit`,()=>{G9()}),!0),Rw=(async()=>{let r=await Ww(e.buildFlavor,e.appBrand,e.repoRoot),i=new n.Tray(r.defaultIcon);return i})()))}", "async function Ww(e,t,i){if(process.platform===`darwin`){return null}let r=K9(e,t,i);return r==null?{defaultIcon:await n.app.getFileIcon(process.execPath,{size:`small`}),chronicleRunningIcon:null}:{defaultIcon:r,chronicleRunningIcon:null}}", "function K9(e,t,r){let a=[(0,i.join)(r,`electron`,`src`,`icons`,`tray.png`)];for(let e of a){let t=n.nativeImage.createFromPath(e);if(!t.isEmpty())return t}return null}", "var pb=class{nativeTrayClickSuppressionReason=null;clearNativeTrayClickSuppressionTimeout=null;chronicleTrayIconRefreshInterval=null;chronicleTrayIconState=`default`;isNativeTrayMenuOpen=!1;trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};constructor(){this.tray={on(){},setContextMenu(){},popUpContextMenu(){}};this.onTrayButtonClick=()=>{};this.tray.on(`click`,()=>{this.onTrayButtonClick()}),this.tray.on(`right-click`,()=>{this.openNativeTrayMenu()})}async handleMessage(e){switch(e.type){case`tray-menu-threads-changed`:this.trayMenuThreads=e.trayMenuThreads;return}}openNativeTrayMenu(){this.updateChronicleTrayIcon();let e=n.Menu.buildFromTemplate(this.getNativeTrayMenuItems());e.once(`menu-will-show`,()=>{this.isNativeTrayMenuOpen=!0}),e.once(`menu-will-close`,()=>{this.isNativeTrayMenuOpen=!1,this.handleNativeTrayMenuClosed()}),this.tray.popUpContextMenu(e)}updateChronicleTrayIcon(){}getNativeTrayMenuItems(){return[]}}", @@ -1210,10 +1221,14 @@ function explicitQuitBundleFixture() { function beforeQuitConfirmationBundleFixture() { return [ - "n.app.on(`before-quit`,o=>{let s=BI(),c=t.sr().some(e=>e.status===`ACTIVE`);if(e||i.canQuitWithoutPrompt()||r||!s&&!c){g=!0,a.markAppQuitting();return}let l=n.app.getName();if(n.dialog.showMessageBoxSync({type:`warning`,buttons:[`Quit`,`Cancel`],defaultId:0,cancelId:1,noLink:!0,title:`Quit ${l}?`,message:`Quit ${l}?`,detail:vB({hasInProgressLocalConversation:s,hasEnabledAutomations:c})})!==0){o.preventDefault();return}i.markQuitApproved(),g=!0,a.markAppQuitting()});", + "n.app.on(`before-quit`,o=>{let s=BI(),c=t.sr().some(e=>e.status===`ACTIVE`);if(e||i.canQuitWithoutPrompt()||r||!s&&!c){g=!0,a.markAppQuitting();return}let l=n.app.getName(),u=h.H(),f=u.formatMessage({messageId:x6,defaultMessage:S6,values:{appName:l}});if(n.dialog.showMessageBoxSync({type:`warning`,buttons:[u.formatMessage({messageId:`desktop.quitConfirmation.quit`,defaultMessage:`Quit`}),u.formatMessage({messageId:`desktop.quitConfirmation.cancel`,defaultMessage:`Cancel`})],defaultId:0,cancelId:1,noLink:!0,title:f,message:f,detail:K6({nativeIntl:u,appName:l,hasInProgressLocalConversation:s,hasEnabledAutomations:c})})!==0){o.preventDefault();return}i.markQuitApproved(),g=!0,a.markAppQuitting()});", ].join(""); } +function sqliteBackfillQuitCloseBundleFixture() { + return "s.on(`close`,t=>{if(!(i||o)){t.preventDefault(),o=!0;try{e()}finally{o=!1}}})"; +} + function willQuitDrainBundleFixture() { return [ "n.app.on(`will-quit`,e=>{if(g=!0,!h){if(i.shouldSkipDrainBeforeQuit()){mB({hotkeyWindowLifecycleManager:c,globalDictationLifecycleManager:l,flushAndDisposeContexts:d,disposables:f});return}e.preventDefault(),h=!0,c.dispose(),l.dispose(),Promise.all([u.flush(),p.flush()]).finally(()=>{d(),f.dispose(),n.app.quit()})}});", @@ -2340,12 +2355,18 @@ test("adds the Linux quit guard to the current comma-declared Electron prelude", const patched = applyPatchTwice(applyLinuxQuitGuardPatch, source); - assert.match(patched, /codexLinuxQuitInProgress=!1/); - assert.match(patched, /codexLinuxExplicitQuitApproved=!1/); - assert.match(patched, /codexLinuxMarkQuitInProgress=\(\)=>\{codexLinuxQuitInProgress=!0,codexLinuxDestroyTray\(\)\}/); - assert.match(patched, /codexLinuxPrepareForExplicitQuit=\(\)=>\{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress\(\)\}/); - assert.match(patched, /codexLinuxShouldBypassQuitPrompt=\(\)=>codexLinuxExplicitQuitApproved===!0/); - assert.match(patched, /codexLinuxIsQuitInProgress=\(\)=>codexLinuxQuitInProgress===!0/); + assert.match(patched, /codexLinuxQuitCommitted=!1/); + assert.match(patched, /codexLinuxQuitAttempting=!1/); + assert.match(patched, /codexLinuxQuitCommitCallback=null/); + assert.match(patched, /codexLinuxExplicitQuitTicket=!1/); + assert.match(patched, /codexLinuxQuitWatchdogTimer=null/); + assert.match(patched, /codexLinuxPrepareForExplicitQuit=\(\)=>\{codexLinuxExplicitQuitTicket=!0,queueMicrotask\(\(\)=>\{codexLinuxExplicitQuitTicket=!1\}\)\}/); + assert.match(patched, /codexLinuxShouldBypassQuitPrompt=\(\)=>\{if\(codexLinuxQuitCommitted===!0\)return!0;if\(codexLinuxExplicitQuitTicket!==!0\)return!1;codexLinuxExplicitQuitTicket=!1;return!0\}/); + assert.match(patched, /codexLinuxIsQuitInProgress=\(\)=>codexLinuxQuitAttempting===!0\|\|codexLinuxQuitCommitted===!0/); + assert.match(patched, /codexLinuxAcceptQuitAttempt=\(e,t\)=>\{if\(process\.platform!==`linux`\)\{e\(\);return\}/); + assert.match(patched, /codexLinuxArmQuitWatchdog=\(\)=>\{if\(process\.platform!==`linux`\)return;/); + assert.match(patched, /c\.app\.on\(`will-quit`,\(\)=>\{process\.platform===`linux`&&codexLinuxCommitQuit\(\)\}\)/); + assert.doesNotMatch(patched, /app\.on\(`before-quit`,\(\)=>codexLinuxDestroyTray/); }); test("keeps the current Linux quit guard module-scoped after helper declarations", () => { @@ -2354,9 +2375,9 @@ test("keeps the current Linux quit guard module-scoped after helper declarations const patched = applyPatchTwice(applyLinuxQuitGuardPatch, source); assert.match(patched, /p=e\.o\(p\);let codexLinuxTray=null/); - assert.match(patched, /codexLinuxExplicitQuitApproved=!1/); - assert.match(patched, /codexLinuxPrepareForExplicitQuit=\(\)=>\{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress\(\)\}/); - assert.equal((patched.match(/codexLinuxQuitInProgress=!1/g) ?? []).length, 1); + assert.match(patched, /codexLinuxExplicitQuitTicket=!1/); + assert.match(patched, /codexLinuxPrepareForExplicitQuit=\(\)=>\{codexLinuxExplicitQuitTicket=!0,queueMicrotask/); + assert.equal((patched.match(/codexLinuxQuitCommitted=!1/g) ?? []).length, 1); }); test("adds the Linux quit guard for the current interleaved bundler prelude", () => { @@ -2366,12 +2387,12 @@ test("adds the Linux quit guard for the current interleaved bundler prelude", () assert.match(patched, /let m=require\(`node:fs\/promises`\);/); assert.match(patched, /p=e\.o\(p\);let codexLinuxTray=null/); - assert.match(patched, /codexLinuxExplicitQuitApproved=!1/); - assert.match(patched, /codexLinuxPrepareForExplicitQuit=\(\)=>\{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress\(\)\}/); - assert.equal((patched.match(/codexLinuxQuitInProgress=!1/g) ?? []).length, 1); + assert.match(patched, /codexLinuxExplicitQuitTicket=!1/); + assert.match(patched, /codexLinuxPrepareForExplicitQuit=\(\)=>\{codexLinuxExplicitQuitTicket=!0,queueMicrotask/); + assert.equal((patched.match(/codexLinuxQuitCommitted=!1/g) ?? []).length, 1); }); -test("destroys the registered Linux tray before the app exits", () => { +test("destroys the registered Linux tray only after quit is committed", () => { const source = `${currentMainBundlePrefix}${trayBundleFixture()}`; const patched = applyPatchTwice( applyLinuxTrayPatch, @@ -2382,19 +2403,20 @@ test("destroys the registered Linux tray before the app exits", () => { assert.match(patched, /codexLinuxRegisterTray=e=>\(codexLinuxTray=e,e\)/); assert.match(patched, /codexLinuxDestroyTray=\(\)=>\{if\(process\.platform!==`linux`\)return;/); assert.match(patched, /codexLinuxTray=null;try\{e\?\.destroy\(\)\}catch\{\}/); - assert.match(patched, /codexLinuxMarkQuitInProgress=\(\)=>\{codexLinuxQuitInProgress=!0,codexLinuxDestroyTray\(\)\}/); - assert.match(patched, /c\.app\.on\(`before-quit`,\(\)=>codexLinuxDestroyTray\(\)\)/); + assert.match(patched, /codexLinuxCommitQuit=\(\)=>\{if\(codexLinuxQuitCommitted===!0\)return;/); + assert.match(patched, /c\.app\.on\(`will-quit`,\(\)=>\{process\.platform===`linux`&&codexLinuxCommitQuit\(\)\}\)/); assert.match(patched, /i=typeof codexLinuxRegisterTray===`function`\?codexLinuxRegisterTray\(new n\.Tray\(r\.defaultIcon\)\):new n\.Tray\(r\.defaultIcon\)/); assert.doesNotMatch(patched, /codexLinuxTrayQuitDelayMs/); const helperStart = patched.indexOf("let codexLinuxTray=null"); - const helperEnd = patched.indexOf(";c.app.on(`before-quit`", helperStart) + 1; + const helperEnd = patched.indexOf(";c.app.on(`will-quit`", helperStart) + 1; const helperSource = patched.slice(helperStart, helperEnd); const runDestroy = new Function( "process", - `${helperSource}let calls=0;codexLinuxRegisterTray({destroy(){calls+=1}});codexLinuxMarkQuitInProgress();codexLinuxMarkQuitInProgress();return calls;`, + "setTimeout", + `${helperSource}let calls=0;codexLinuxRegisterTray({destroy(){calls+=1}});codexLinuxCommitQuit();codexLinuxCommitQuit();return calls;`, ); - assert.equal(runDestroy({ platform: "linux" }), 1); + assert.equal(runDestroy({ platform: "linux" }, () => ({ unref() {} })), 1); }); test("bypasses the upstream before-quit confirmation after a Linux explicit quit", () => { @@ -2406,12 +2428,72 @@ test("bypasses the upstream before-quit confirmation after a Linux explicit quit assert.match( patched, - /if\(\(typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt\(\)\)\|\|e\|\|i\.canQuitWithoutPrompt\(\)\|\|r\|\|!s&&!c\)\{process\.platform===`linux`&&typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),g=!0,a\.markAppQuitting\(\);return\}/, + /if\(\(typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt\(\)\)\|\|e\|\|i\.canQuitWithoutPrompt\(\)\|\|r\|\|!s&&!c\)\{if\(process\.platform===`linux`&&typeof codexLinuxAcceptQuitAttempt===`function`\)\{codexLinuxAcceptQuitAttempt\(\(\)=>\{g=!0,a\.markAppQuitting\(\)\},o\);return\}g=!0,a\.markAppQuitting\(\);return\}/, ); assert.match( patched, - /process\.platform===`linux`&&typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),i\.markQuitApproved\(\),g=!0,a\.markAppQuitting\(\)/, + /i\.markQuitApproved\(\);if\(process\.platform===`linux`&&typeof codexLinuxAcceptQuitAttempt===`function`\)\{codexLinuxAcceptQuitAttempt\(\(\)=>\{g=!0,a\.markAppQuitting\(\)\},o\);return\}g=!0,a\.markAppQuitting\(\)/, + ); + assert.doesNotMatch( + patched, + /if\(\(typeof codexLinuxShouldBypassQuitPrompt[^}]+codexLinuxArmQuitWatchdog\(\)/, + ); + assert.doesNotMatch( + patched, + /markQuitApproved\(\),typeof codexLinuxArmQuitWatchdog/, + ); +}); + +test("bypasses a semantically matched before-quit guard without arming before commit", () => { + const source = `${currentMainBundlePrefix}${beforeQuitConfirmationBundleFixture() + .replace("if(e||", "if(U||")}`; + const patched = applyPatchTwice( + applyLinuxExplicitQuitPromptBypassPatch, + applyLinuxQuitGuardPatch(source), + ); + + assert.match( + patched, + /if\(\(typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt\(\)\)\|\|U\|\|i\.canQuitWithoutPrompt\(\)\|\|r\|\|!s&&!c\)\{if\(process\.platform===`linux`&&typeof codexLinuxAcceptQuitAttempt===`function`\)\{codexLinuxAcceptQuitAttempt\(\(\)=>\{g=!0,a\.markAppQuitting\(\)\},o\);return\}g=!0,a\.markAppQuitting\(\);return\}/, + ); + assert.doesNotMatch( + patched, + /if\(\(typeof codexLinuxShouldBypassQuitPrompt[^}]+codexLinuxArmQuitWatchdog\(\)/, + ); +}); + +test("lets an accepted Linux quit close the SQLite backfill progress window", () => { + const source = sqliteBackfillQuitCloseBundleFixture(); + const patched = applyPatchTwice(applyLinuxSqliteBackfillQuitClosePatch, source); + + assert.match( + patched, + /if\(!\(i\|\|o\|\|process\.platform===`linux`&&typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)\)/, ); + + const runClose = (quitInProgress) => { + const state = { onQuitCalls: 0, preventDefaultCalls: 0 }; + const window = new EventEmitter(); + vm.runInNewContext(patched, { + s: window, + i: false, + o: false, + e: () => { + state.onQuitCalls += 1; + }, + process: { platform: "linux" }, + codexLinuxIsQuitInProgress: () => quitInProgress, + }); + window.emit("close", { + preventDefault() { + state.preventDefaultCalls += 1; + }, + }); + return state; + }; + + assert.deepEqual(runClose(false), { onQuitCalls: 1, preventDefaultCalls: 1 }); + assert.deepEqual(runClose(true), { onQuitCalls: 0, preventDefaultCalls: 0 }); }); test("adds a bounded will-quit drain fallback for Linux explicit quit", () => { @@ -2423,7 +2505,8 @@ test("adds a bounded will-quit drain fallback for Linux explicit quit", () => { assert.match(patched, /codexLinuxExplicitQuitDrainTimeoutMs=3e3/); assert.match(patched, /\(\(\)=>\{let codexLinuxFinalizeQuit=\(\)=>\{d\(\),f\.dispose\(\),n\.app\.quit\(\)\},codexLinuxDrainPromise=Promise\.all\(\[u\.flush\(\),p\.flush\(\)\]\);/); - assert.match(patched, /if\(process\.platform===`linux`&&\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)\)\{Promise\.race\(\[codexLinuxDrainPromise,new Promise\(e=>setTimeout\(e,typeof codexLinuxExplicitQuitDrainTimeoutMs===`number`\?codexLinuxExplicitQuitDrainTimeoutMs:3e3\)\)\]\)\.finally\(codexLinuxFinalizeQuit\);return\}/); + assert.match(patched, /if\(process\.platform===`linux`\)\{typeof codexLinuxCommitQuit===`function`&&codexLinuxCommitQuit\(\);let codexLinuxLinuxFinalizeQuit=\(\)=>/); + assert.match(patched, /Promise\.race\(\[codexLinuxDrainPromise\.catch\(\(\)=>\{\}\),new Promise\(e=>setTimeout\(e,typeof codexLinuxExplicitQuitDrainTimeoutMs===`number`\?codexLinuxExplicitQuitDrainTimeoutMs:3e3\)\)\]\)\.finally\(codexLinuxLinuxFinalizeQuit\)/); assert.doesNotMatch(patched, /\\`number\\`/); assert.match(patched, /codexLinuxDrainPromise\.finally\(codexLinuxFinalizeQuit\)\}\)\(\)/); assert.doesNotThrow(() => new Function(patched)); @@ -2438,7 +2521,7 @@ test("marks Linux quit-in-progress for the tray quit path", () => { assert.match( patched, - /\{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\)\}\}/, + /\{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),n\.app\.quit\(\)\}\}/, ); }); @@ -2451,7 +2534,7 @@ test("marks Linux quit-in-progress for the quit-app IPC path", () => { assert.match( patched, - /if\(o\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\);return\}/, + /if\(o\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),n\.app\.quit\(\);return\}/, ); }); @@ -2462,7 +2545,7 @@ test("supports explicit tray quit patching when minified aliases drift", () => { assert.match( patched, - /\{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),x\.app\.quit\(\)\}\}/, + /\{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),x\.app\.quit\(\)\}\}/, ); }); @@ -2473,7 +2556,7 @@ test("supports explicit tray quit patching when upstream renames the quit label assert.match( patched, - /\{label:mH\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\)\}\}/, + /\{label:mH\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),n\.app\.quit\(\)\}\}/, ); }); @@ -2484,13 +2567,13 @@ test("supports explicit IPC quit patching when minified aliases drift", () => { assert.match( patched, - /if\(m\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),x\.app\.quit\(\);return\}/, + /if\(m\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),x\.app\.quit\(\);return\}/, ); }); test("patches remaining explicit quit handlers when another copy is already patched", () => { const quitMarkerExpression = - "typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),"; + "typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit(),"; const patchedTrayQuit = `{label:rB(this.appName),click:()=>{${quitMarkerExpression}n.app.quit()}}`; const unpatchedTrayQuit = "{label:rB(this.appName),click:()=>{n.app.quit()}}"; const patchedIpcQuit = `if(o.type===\`quit-app\`){${quitMarkerExpression}n.app.quit();return}`; @@ -2508,15 +2591,467 @@ test("patches remaining explicit quit handlers when another copy is already patc assert.equal((patchedTray.match(/codexLinuxPrepareForExplicitQuit\(\)/g) ?? []).length, 2); assert.match( patchedTray, - /function createSecondTray\(\)\{return \{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\)\}\}\}/, + /function createSecondTray\(\)\{return \{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),n\.app\.quit\(\)\}\}\}/, ); assert.equal((patchedIpc.match(/codexLinuxPrepareForExplicitQuit\(\)/g) ?? []).length, 2); assert.match( patchedIpc, - /function createSecondIpc\(\)\{if\(o\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\);return\}\}/, + /function createSecondIpc\(\)\{if\(o\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),n\.app\.quit\(\);return\}\}/, ); }); +function createLinuxQuitLifecycleHarness() { + const patched = applyPatchTwice(applyLinuxQuitGuardPatch, currentMainBundlePrefix); + const helperStart = patched.indexOf("let codexLinuxTray=null"); + const listener = "c.app.on(`will-quit`,()=>{process.platform===`linux`&&codexLinuxCommitQuit()});"; + const helperEnd = patched.indexOf(listener, helperStart) + listener.length; + const app = new EventEmitter(); + const timers = []; + const state = { appExitCalls: 0, appExitCodes: [], processExitCalls: 0 }; + app.exit = (code) => { + state.appExitCalls += 1; + state.appExitCodes.push(code); + }; + const context = { + c: { app }, + process: { + platform: "linux", + exit: () => { + state.processExitCalls += 1; + }, + }, + setTimeout: (callback, delay) => { + timers.push({ callback, delay }); + return { unref() {} }; + }, + queueMicrotask, + }; + vm.runInNewContext( + `${patched.slice(helperStart, helperEnd)}globalThis.lifecycle={register:codexLinuxRegisterTray,isTrayAlive:codexLinuxIsTrayAlive,prepare:codexLinuxPrepareForExplicitQuit,bypass:codexLinuxShouldBypassQuitPrompt,isQuitInProgress:codexLinuxIsQuitInProgress,commit:codexLinuxCommitQuit};`, + context, + ); + return { app, lifecycle: context.lifecycle, state, timers }; +} + +test("keeps ordinary cancelled quit attempts repeatable without destroying the tray", () => { + const { lifecycle } = createLinuxQuitLifecycleHarness(); + let destroyCalls = 0; + lifecycle.register({ + isDestroyed: () => false, + destroy: () => { + destroyCalls += 1; + }, + }); + + assert.equal(lifecycle.bypass(), false); + assert.equal(lifecycle.isQuitInProgress(), false); + assert.equal(lifecycle.isTrayAlive(), true); + assert.equal(lifecycle.bypass(), false); + assert.equal(lifecycle.isQuitInProgress(), false); + assert.equal(lifecycle.isTrayAlive(), true); + assert.equal(destroyCalls, 0); +}); + +test("a vetoed before-quit prompts again and leaves quit recoverable", () => { + const app = new EventEmitter(); + let promptCalls = 0; + let preventDefaultCalls = 0; + let destroyCalls = 0; + let hideCalls = 0; + const timers = []; + const electron = { + app: Object.assign(app, { getName: () => "Codex", exit() {} }), + dialog: { + showMessageBoxSync: () => { + promptCalls += 1; + return 1; + }, + }, + }; + const source = `${currentMainBundlePrefix}${beforeQuitConfirmationBundleFixture() + .replace("if(e||", "if(U||")}`; + const patched = applyLinuxExplicitQuitPromptBypassPatch( + applyLinuxQuitGuardPatch(source), + ); + const context = { + U: false, + n: electron, + BI: () => true, + t: { sr: () => [{ status: "ACTIVE" }] }, + i: { canQuitWithoutPrompt: () => false, markQuitApproved() {} }, + r: false, + g: false, + a: { markAppQuitting() {} }, + h: { H: () => ({ formatMessage: ({ defaultMessage }) => defaultMessage }) }, + x6: "desktop.quitConfirmation.title", + S6: "Quit Codex?", + K6: () => "detail", + process: { platform: "linux", exit() {} }, + setTimeout: (callback) => { + timers.push(callback); + return { unref() {} }; + }, + queueMicrotask, + require: (name) => name === "electron" ? electron : {}, + }; + vm.runInNewContext( + `${patched};globalThis.lifecycle={register:codexLinuxRegisterTray,isTrayAlive:codexLinuxIsTrayAlive,isQuitInProgress:codexLinuxIsQuitInProgress};`, + context, + ); + context.lifecycle.register({ + isDestroyed: () => false, + destroy() { + destroyCalls += 1; + }, + }); + const event = { preventDefault: () => { preventDefaultCalls += 1; } }; + + app.emit("before-quit", event); + app.emit("before-quit", event); + + assert.equal(promptCalls, 2); + assert.equal(preventDefaultCalls, 2); + assert.equal(context.lifecycle.isTrayAlive(), true); + assert.equal(context.lifecycle.isQuitInProgress(), false); + assert.equal(timers.length, 0); + assert.equal(destroyCalls, 0); + + const closeSource = captureWarns(() => applyLinuxTrayPatch( + "let n=require(`electron`);v&&k.on(`close`,e=>{this.persistPrimaryWindowBounds(k);let t=this.getPrimaryWindows().some(e=>e!==k);if(process.platform===`win32`&&!this.isAppQuitting&&this.options.canHideLastWindowToTray?.()===!0&&!t){e.preventDefault(),k.hide();return}});", + null, + )).value; + const window = new EventEmitter(); + window.hide = () => { + hideCalls += 1; + }; + const closeContext = { + process: { platform: "linux" }, + require: () => electron, + v: true, + k: window, + persistPrimaryWindowBounds() {}, + getPrimaryWindows: () => [], + isAppQuitting: false, + options: { canHideLastWindowToTray: () => true }, + codexLinuxIsQuitInProgress: context.lifecycle.isQuitInProgress, + codexLinuxIsTrayAlive: context.lifecycle.isTrayAlive, + }; + vm.runInNewContext(closeSource, closeContext); + window.emit("close", event); + assert.equal(hideCalls, 1); + assert.equal(preventDefaultCalls, 3); +}); + +test("a later before-quit veto preserves upstream state before a successful retry", async () => { + const app = new EventEmitter(); + const timers = []; + const state = { + appExitCalls: 0, + beforeQuitVetoes: 0, + destroyCalls: 0, + hideCalls: 0, + isAppQuitting: false, + markAppQuittingCalls: 0, + preventDefaultCalls: 0, + processExitCalls: 0, + quickChatDisposeCalls: 0, + }; + app.exit = () => { + state.appExitCalls += 1; + }; + const electron = { + app: Object.assign(app, { getName: () => "Codex" }), + dialog: { showMessageBoxSync: () => 1 }, + }; + const source = `${currentMainBundlePrefix}${beforeQuitConfirmationBundleFixture() + .replace("if(e||", "if(U||")}`; + const patched = applyPatchTwice( + applyLinuxExplicitQuitPromptBypassPatch, + applyLinuxQuitGuardPatch(source), + ); + const context = { + U: false, + n: electron, + BI: () => true, + t: { sr: () => [{ status: "ACTIVE" }] }, + i: { canQuitWithoutPrompt: () => false, markQuitApproved() {} }, + r: false, + g: false, + a: { + markAppQuitting() { + state.markAppQuittingCalls += 1; + state.isAppQuitting = true; + state.quickChatDisposeCalls += 1; + }, + }, + h: { H: () => ({ formatMessage: ({ defaultMessage }) => defaultMessage }) }, + x6: "desktop.quitConfirmation.title", + S6: "Quit Codex?", + K6: () => "detail", + process: { + platform: "linux", + exit: () => { + state.processExitCalls += 1; + }, + }, + setTimeout: (callback, delay) => { + timers.push({ callback, delay }); + return { unref() {} }; + }, + queueMicrotask, + require: (name) => name === "electron" ? electron : {}, + }; + vm.runInNewContext( + `${patched};globalThis.lifecycle={register:codexLinuxRegisterTray,isTrayAlive:codexLinuxIsTrayAlive,prepare:codexLinuxPrepareForExplicitQuit,isQuitInProgress:codexLinuxIsQuitInProgress};`, + context, + ); + let trayDestroyed = false; + context.lifecycle.register({ + isDestroyed: () => trayDestroyed, + destroy() { + trayDestroyed = true; + state.destroyCalls += 1; + }, + }); + + let vetoNextQuit = true; + app.on("before-quit", (event) => { + if (!vetoNextQuit) return; + vetoNextQuit = false; + state.beforeQuitVetoes += 1; + event.preventDefault(); + }); + + context.lifecycle.prepare(); + const vetoedEvent = { + defaultPrevented: false, + preventDefault() { + this.defaultPrevented = true; + }, + }; + app.emit("before-quit", vetoedEvent); + await Promise.resolve(); + + assert.equal(state.beforeQuitVetoes, 1); + assert.equal(state.markAppQuittingCalls, 0); + assert.equal(state.isAppQuitting, false); + assert.equal(state.quickChatDisposeCalls, 0); + assert.equal(state.appExitCalls, 0); + assert.equal(state.processExitCalls, 0); + assert.equal(state.destroyCalls, 0); + assert.equal(context.lifecycle.isTrayAlive(), true); + assert.equal(context.lifecycle.isQuitInProgress(), false); + assert.equal(timers.length, 0); + + const closeSource = captureWarns(() => applyLinuxTrayPatch( + "let n=require(`electron`);v&&k.on(`close`,e=>{this.persistPrimaryWindowBounds(k);let t=this.getPrimaryWindows().some(e=>e!==k);if(process.platform===`win32`&&!this.isAppQuitting&&this.options.canHideLastWindowToTray?.()===!0&&!t){e.preventDefault(),k.hide();return}});", + null, + )).value; + const window = new EventEmitter(); + window.hide = () => { + state.hideCalls += 1; + }; + vm.runInNewContext(closeSource, { + process: { platform: "linux" }, + require: () => electron, + v: true, + k: window, + persistPrimaryWindowBounds() {}, + getPrimaryWindows: () => [], + isAppQuitting: state.isAppQuitting, + options: { canHideLastWindowToTray: () => true }, + codexLinuxIsQuitInProgress: context.lifecycle.isQuitInProgress, + codexLinuxIsTrayAlive: context.lifecycle.isTrayAlive, + }); + window.emit("close", { + preventDefault() { + state.preventDefaultCalls += 1; + }, + }); + + assert.equal(state.hideCalls, 1); + assert.equal(state.preventDefaultCalls, 1); + assert.equal(context.lifecycle.isTrayAlive(), true); + + context.lifecycle.prepare(); + app.emit("before-quit", { defaultPrevented: false, preventDefault() {} }); + await Promise.resolve(); + + assert.equal(state.markAppQuittingCalls, 0); + assert.equal(state.isAppQuitting, false); + assert.equal(state.quickChatDisposeCalls, 0); + assert.equal(context.lifecycle.isQuitInProgress(), true); + + window.emit("close", { + preventDefault() { + state.preventDefaultCalls += 1; + }, + }); + + assert.equal(state.hideCalls, 1); + assert.equal(state.preventDefaultCalls, 1); + + app.emit("will-quit", {}); + + assert.equal(state.markAppQuittingCalls, 1); + assert.equal(state.isAppQuitting, true); + assert.equal(state.quickChatDisposeCalls, 1); + assert.equal(state.destroyCalls, 1); + assert.equal(context.lifecycle.isTrayAlive(), false); + assert.equal(context.lifecycle.isQuitInProgress(), true); + assert.equal(timers.length, 1); +}); + +test("warns when the localized before-quit bypass guard drifts despite an intact accepted prompt", () => { + const source = beforeQuitConfirmationBundleFixture().replace( + "if(e||i.canQuitWithoutPrompt()", + "if(Boolean(e)||i.canQuitWithoutPrompt()", + ); + const { value, warnings } = captureWarns(() => + applyLinuxExplicitQuitPromptBypassPatch(source), + ); + + assert.equal(value, source); + assert.doesNotMatch(value, /codexLinuxShouldBypassQuitPrompt\(\)/); + assert.match(value, /i\.markQuitApproved\(\),g=!0,a\.markAppQuitting\(\)/); + assert.doesNotMatch(value, /markQuitApproved\(\),typeof codexLinuxArmQuitWatchdog/); + assert.deepEqual(warnings, [ + "WARN: Could not find before-quit confirmation guard — skipping Linux explicit quit prompt bypass patch", + ]); +}); + +test("warns when the localized before-quit anchor is renamed", () => { + const source = beforeQuitConfirmationBundleFixture().replaceAll( + ".canQuitWithoutPrompt()", + ".canQuitImmediately()", + ); + const { value, warnings } = captureWarns(() => + applyLinuxExplicitQuitPromptBypassPatch(source), + ); + + assert.equal(value, source); + assert.doesNotMatch(value, /codexLinuxShouldBypassQuitPrompt/); + assert.deepEqual(warnings, [ + "WARN: Could not find before-quit confirmation guard — skipping Linux explicit quit prompt bypass patch", + ]); +}); + +test("patches the intact localized before-quit prompt without warnings", () => { + const { value, warnings } = captureWarns(() => + applyPatchTwice( + applyLinuxExplicitQuitPromptBypassPatch, + beforeQuitConfirmationBundleFixture(), + ), + ); + + assert.match(value, /codexLinuxShouldBypassQuitPrompt\(\)/); + assert.match(value, /desktop\.quitConfirmation\.quit/); + assert.match(value, /desktop\.quitConfirmation\.cancel/); + assert.deepEqual(warnings, []); +}); + +test("expires an unconsumed explicit quit ticket before a later quit", async () => { + const { lifecycle } = createLinuxQuitLifecycleHarness(); + + lifecycle.prepare(); + await Promise.resolve(); + + assert.equal(lifecycle.bypass(), false); + assert.equal(lifecycle.isQuitInProgress(), false); +}); + +test("consumes explicit tray and IPC quit approvals as one-shot tickets", () => { + for (const quitPath of ["tray", "ipc"]) { + const { lifecycle } = createLinuxQuitLifecycleHarness(); + lifecycle.prepare(); + assert.equal(lifecycle.bypass(), true, quitPath); + assert.equal(lifecycle.bypass(), false, quitPath); + assert.equal(lifecycle.isQuitInProgress(), false, quitPath); + } +}); + +test("commits will-quit, bypasses re-entrant prompts, and arms hard exit", () => { + const { app, lifecycle, state, timers } = createLinuxQuitLifecycleHarness(); + let destroyCalls = 0; + lifecycle.register({ + isDestroyed: () => false, + destroy: () => { + destroyCalls += 1; + }, + }); + + app.emit("will-quit"); + assert.equal(lifecycle.isQuitInProgress(), true); + assert.equal(lifecycle.bypass(), true); + assert.equal(destroyCalls, 1); + assert.equal(timers.length, 1); + assert.equal(timers[0].delay, 8000); + timers[0].callback(); + assert.equal(state.appExitCalls, 1); + assert.deepEqual(state.appExitCodes, [0]); + assert.equal(state.processExitCalls, 1); +}); + +test("reports null and destroyed registered trays as not alive", () => { + const { lifecycle } = createLinuxQuitLifecycleHarness(); + lifecycle.register(null); + assert.equal(lifecycle.isTrayAlive(), false); + lifecycle.register({ isDestroyed: () => true }); + assert.equal(lifecycle.isTrayAlive(), false); + lifecycle.register({ isDestroyed: () => { throw new Error("destroyed"); } }); + assert.equal(lifecycle.isTrayAlive(), false); +}); + +test("Linux drain timeout reaches app.exit even when cleanup fails", async () => { + const patched = applyPatchTwice( + applyLinuxWillQuitDrainTimeoutPatch, + willQuitDrainBundleFixture(), + ); + const app = new EventEmitter(); + let appExitCalls = 0; + let processExitCalls = 0; + let commitCalls = 0; + app.exit = () => { + appExitCalls += 1; + }; + const never = new Promise(() => {}); + const context = { + n: { app }, + g: false, + h: false, + i: { shouldSkipDrainBeforeQuit: () => false }, + mB() {}, + c: { dispose() {} }, + l: { dispose() {} }, + d: () => { throw new Error("flush cleanup failed"); }, + f: { dispose: () => { throw new Error("dispose cleanup failed"); } }, + u: { flush: () => never }, + p: { flush: () => never }, + codexLinuxCommitQuit: () => { + commitCalls += 1; + }, + codexLinuxExplicitQuitDrainTimeoutMs: 0, + process: { + platform: "linux", + exit: () => { + processExitCalls += 1; + }, + }, + setTimeout: (callback) => { + queueMicrotask(callback); + return 1; + }, + }; + vm.runInNewContext(patched, context); + + app.emit("will-quit", { preventDefault() {} }); + await new Promise((resolve) => setImmediate(resolve)); + + assert.equal(commitCalls, 1); + assert.equal(appExitCalls, 1); + assert.equal(processExitCalls, 1); +}); + test("uses the frameless native Codex titlebar for primary Linux windows", () => { const source = [ "function A2(e){return e===`avatarOverlay`}", @@ -3604,9 +4139,9 @@ test("adds Linux tray support including the platform guard", () => { ); assert.match( patched, - /\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&!\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)/, + /\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&\(typeof codexLinuxIsQuitInProgress!==`function`\|\|!codexLinuxIsQuitInProgress\(\)\)&&\(process\.platform!==`linux`\|\|typeof codexLinuxIsTrayAlive===`function`&&codexLinuxIsTrayAlive\(\)===!0\)/, ); - assert.match(patched, /setLinuxTrayContextMenu\(\)\{let e=n\.Menu\.buildFromTemplate/); + assert.match(patched, /setLinuxTrayContextMenu\(\)\{if\(this\.tray==null\|\|typeof this\.tray\.isDestroyed===`function`&&this\.tray\.isDestroyed\(\)\)return null;let e=n\.Menu\.buildFromTemplate/); assert.match( patched, /process\.platform===`linux`&&\(codexLinuxSetTrayController\(this\),this\.setLinuxTrayContextMenu\(\)\),this\.tray\.on\(`click`/, @@ -3631,6 +4166,86 @@ test("adds Linux tray support including the platform guard", () => { assert.doesNotMatch(patched, /process\.platform===`linux`&&codexLinuxIsTrayEnabled\(\)/); }); +test("moves the upstream tray teardown to the Linux quit commit point", () => { + const source = `${mainBundlePrefix}${trayBundleFixture()}`; + const patched = applyPatchTwice(applyLinuxTrayPatch, source, null); + + assert.match( + patched, + /process\.platform===`linux`\?`will-quit`:`before-quit`/, + ); + assert.match( + patched, + /ire\|\|=\(n\.app\.on\(process\.platform===`linux`\?`will-quit`:`before-quit`,\(\)=>\{try\{G9\(\)\}catch\{\}\}\),!0\)/, + ); + assert.doesNotMatch( + patched, + /n\.app\.on\(`before-quit`,\(\)=>\{G9\(\)\}\)/, + ); + assert.equal(applyLinuxTrayPatch(patched, null), patched); +}); + +test("keeps the upstream tray quit teardown patch fail-soft when its needle drifts", () => { + const emittedListener = + "ire||=(n.app.on(process.platform===`linux`?`will-quit`:`before-quit`,()=>{try{G9()}catch{}}),!0)"; + const patched = captureWarns(() => + applyPatchTwice(applyLinuxTrayPatch, `${mainBundlePrefix}${trayBundleFixture()}`, null), + ).value; + const sourceWithoutNeedle = patched.replace(emittedListener, "ire||=!0"); + + const { value, warnings } = captureWarns(() => + applyLinuxTrayPatch(sourceWithoutNeedle, null), + ); + + assert.equal(value, sourceWithoutNeedle); + assert.deepEqual(warnings, [ + "WARN: Could not find upstream tray before-quit listener — skipping Linux upstream tray quit teardown patch", + ]); +}); + +test("hides the last Linux window only while a live tray is registered", () => { + const source = [ + "let n=require(`electron`);", + "v&&k.on(`close`,e=>{this.persistPrimaryWindowBounds(k);let t=this.getPrimaryWindows().some(e=>e!==k);if(process.platform===`win32`&&!this.isAppQuitting&&this.options.canHideLastWindowToTray?.()===!0&&!t){e.preventDefault(),k.hide();return}});", + ].join(""); + const patched = applyPatchTwice(applyLinuxTrayPatch, source, null); + const runClose = (trayAlive, quitInProgress, trayEnabled = true) => { + const window = new EventEmitter(); + let prevented = 0; + let hidden = 0; + window.hide = () => { + hidden += 1; + }; + const context = { + require: () => ({}), + process: { platform: "linux" }, + v: true, + k: window, + persistPrimaryWindowBounds() {}, + getPrimaryWindows: () => [window], + isAppQuitting: false, + options: { canHideLastWindowToTray: () => trayEnabled }, + codexLinuxIsQuitInProgress: () => quitInProgress, + }; + if (trayAlive !== undefined) { + context.codexLinuxIsTrayAlive = () => trayAlive; + } + vm.runInNewContext(patched, context); + window.emit("close", { + preventDefault: () => { + prevented += 1; + }, + }); + return { hidden, prevented }; + }; + + assert.deepEqual(runClose(true, false), { hidden: 1, prevented: 1 }); + assert.deepEqual(runClose(false, false), { hidden: 0, prevented: 0 }); + assert.deepEqual(runClose(undefined, false), { hidden: 0, prevented: 0 }); + assert.deepEqual(runClose(true, true), { hidden: 0, prevented: 0 }); + assert.deepEqual(runClose(true, false, false), { hidden: 0, prevented: 0 }); +}); + test("refreshes only the live Linux tray controller after session recovery", () => { const source = `${mainBundlePrefix}${trayBundleFixture() .replace( @@ -3670,6 +4285,9 @@ test("refreshes only the live Linux tray controller after session recovery", () this.menuSetCount += 1; }, popUpContextMenu() {}, + isDestroyed() { + return this.destroyed; + }, destroy() { this.destroyed = true; }, @@ -3961,7 +4579,7 @@ test("adds Linux tray support for current minified window and startup identifier assert.match( patched, - /\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&!\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)/, + /\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&\(typeof codexLinuxIsQuitInProgress!==`function`\|\|!codexLinuxIsQuitInProgress\(\)\)&&\(process\.platform!==`linux`\|\|typeof codexLinuxIsTrayAlive===`function`&&codexLinuxIsTrayAlive\(\)===!0\)/, ); assert.match(patched, /e\.preventDefault\(\),j\.hide\(\);return/); assert.match( @@ -4054,7 +4672,7 @@ test("scopes close-to-tray already-patched detection to the handler", () => { assert.match( patched, - /if\(\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&!\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)&&this\.options\.canHideLastWindowToTray\?\.\(\)===!0&&!t\)\{e\.preventDefault\(\),j\.hide\(\);return\}/, + /if\(\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&\(typeof codexLinuxIsQuitInProgress!==`function`\|\|!codexLinuxIsQuitInProgress\(\)\)&&\(process\.platform!==`linux`\|\|typeof codexLinuxIsTrayAlive===`function`&&codexLinuxIsTrayAlive\(\)===!0\)&&this\.options\.canHideLastWindowToTray\?\.\(\)===!0&&!t\)\{e\.preventDefault\(\),j\.hide\(\);return\}/, ); }); @@ -4066,12 +4684,12 @@ test("adds Linux single-instance lock and second-instance handoff", () => { /process\.platform===`linux`&&process\.env\.CODEX_LINUX_MULTI_LAUNCH!==`1`&&!n\.app\.requestSingleInstanceLock\(\)/, ); assert.match(patched, /n\.app\.quit\(\);return/); - assert.match(patched, /codexLinuxBeforeQuitHandler=\(\)=>\{typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\)\}/); - assert.match(patched, /n\.app\.on\(`before-quit`,codexLinuxBeforeQuitHandler\)/); - assert.match(patched, /n\.app\.off\(`before-quit`,codexLinuxBeforeQuitHandler\)/); assert.match(patched, /codexLinuxSecondInstanceHandler/); + assert.match(patched, /\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)\?void 0:R\.deepLinks\.queueProcessArgs\(t\)\|\|ie\(\)/); assert.match(patched, /n\.app\.on\(`second-instance`,codexLinuxSecondInstanceHandler\)/); assert.match(patched, /n\.app\.off\(`second-instance`,codexLinuxSecondInstanceHandler\)/); + assert.doesNotMatch(patched, /codexLinuxBeforeQuitHandler/); + assert.doesNotMatch(patched, /app\.(?:on|off)\(`before-quit`/); }); test("forces the bootstrap single-instance lock on Linux even when upstream disables it", () => { @@ -4349,6 +4967,33 @@ test("adds Linux launch actions through current setSecondInstanceArgsHandler bun ); }); +test("keeps quit lifecycle declarations module-scoped when launch actions share a bundle", () => { + const launchBody = currentLaunchActionBundleFixture().slice( + currentLaunchActionBundleFixture().indexOf("async function CN"), + ); + const patched = applyLinuxLaunchActionArgsPatch( + applyLinuxQuitGuardPatch(`${currentMainBundlePrefix}${launchBody}`), + ); + + assert.equal((patched.match(/codexLinuxQuitCommitted=!1/g) ?? []).length, 1); + assert.ok(patched.indexOf("codexLinuxQuitCommitted=!1") < patched.indexOf("async function CN")); + assert.doesNotMatch(patched.slice(patched.indexOf("async function CN")), /codexLinuxQuitCommitted=!1/); +}); + +test("standalone launch-action output stays strict-mode parseable and lifecycle-free", () => { + const patched = applyPatchTwice( + applyLinuxLaunchActionArgsPatch, + currentLaunchActionBundleFixture(), + ); + + assert.doesNotThrow(() => new Function(`"use strict";${patched}`)); + assert.match(patched, /let codexLinuxGetSetting=e=>/); + assert.doesNotMatch( + patched, + /codexLinuxQuitCommitted|codexLinuxExplicitQuitTicket|codexLinuxPrepareForExplicitQuit|codexLinuxShouldBypassQuitPrompt|codexLinuxBeforeQuitHandler/, + ); +}); + test("uses collision-safe modules for launch-action socket in shadowed startup scopes", () => { const source = currentLaunchActionBundleFixture().replace( "async function CN(){let{setSecondInstanceArgsHandler:l}=t.y(),g={reportNonFatal(){}}", @@ -4375,16 +5020,10 @@ test("adds Linux launch actions when captured window identifiers contain dollar assert.match(patched, /codexLinuxHandleLaunchActionArgs/); assert.match(patched, /z\.navigateToRoute\(r\$,e\),ae\(r\$\)/); - assert.match(patched, /codexLinuxQuitInProgress=!1/); - assert.match(patched, /codexLinuxExplicitQuitApproved=!1/); - assert.match(patched, /codexLinuxMarkQuitInProgress=\(\)=>\{codexLinuxQuitInProgress=!0\}/); - assert.match(patched, /codexLinuxPrepareForExplicitQuit=\(\)=>\{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress\(\)\}/); - assert.match(patched, /codexLinuxShouldBypassQuitPrompt=\(\)=>codexLinuxExplicitQuitApproved===!0/); - assert.match(patched, /codexLinuxIsQuitInProgress=\(\)=>codexLinuxQuitInProgress===!0/); + assert.doesNotMatch(patched, /codexLinuxQuitCommitted|codexLinuxExplicitQuitTicket/); + assert.doesNotMatch(patched, /codexLinuxPrepareForExplicitQuit|codexLinuxShouldBypassQuitPrompt|codexLinuxBeforeQuitHandler/); assert.match(patched, /codexLinuxGetSetting=e=>/); assert.match(patched, /codexLinuxHandleLaunchActionArgs=async e=>/); - assert.match(patched, /codexLinuxHandleLaunchActionArgs=async e=>\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)\?!0:/); - assert.match(patched, /codexLinuxHandleLaunchActionArgsFallback=\(e,t\)=>\{if\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)return;/); assert.match(patched, /codexLinuxStartLaunchActionSocket=\(\)=>/); assert.match(patched, /codexLinuxDefaultLaunchActionSocket=\(\)=>/); assert.match(patched, /codexLinuxPrewarmHotkeyWindow=\(\)=>/); @@ -8262,11 +8901,15 @@ test("patchMainBundleSource keeps non-icon patches active without an icon asset" const patched = applyPatchTwice(patchMainBundleSource, source, null); - assert.match(patched, /codexLinuxQuitInProgress=!1/); - assert.match(patched, /codexLinuxExplicitQuitApproved=!1/); - assert.match(patched, /codexLinuxIsQuitInProgress=\(\)=>codexLinuxQuitInProgress===!0/); - assert.match(patched, /codexLinuxShouldBypassQuitPrompt=\(\)=>codexLinuxExplicitQuitApproved===!0/); - assert.match(patched, /n\.app\.on\(`before-quit`,codexLinuxBeforeQuitHandler\)/); + assert.match(patched, /codexLinuxQuitCommitted=!1/); + assert.match(patched, /codexLinuxQuitAttempting=!1/); + assert.match(patched, /codexLinuxExplicitQuitTicket=!1/); + assert.match( + patched, + /codexLinuxIsQuitInProgress=\(\)=>codexLinuxQuitAttempting===!0\|\|codexLinuxQuitCommitted===!0/, + ); + assert.match(patched, /codexLinuxShouldBypassQuitPrompt=\(\)=>\{if\(codexLinuxQuitCommitted===!0\)return!0;/); + assert.doesNotMatch(patched, /codexLinuxBeforeQuitHandler/); assert.match(patched, /process\.platform===`linux`&&k\.removeMenu\(\)/); assert.match(patched, /linux:\{label:`File Manager`/); assert.match( @@ -8296,11 +8939,11 @@ test("patchMainBundleSource stays idempotent after wrapping the Electron require assert.equal(patchMainBundleSource(patched, "app-test.png"), patched); assert.match( patched, - /setLinuxTrayContextMenu\(\)\{let e=c\.Menu\.buildFromTemplate/, + /setLinuxTrayContextMenu\(\)\{if\(this\.tray==null\|\|typeof this\.tray\.isDestroyed===`function`&&this\.tray\.isDestroyed\(\)\)return null;let e=c\.Menu\.buildFromTemplate/, ); assert.doesNotMatch( patched, - /setLinuxTrayContextMenu\(\)\{let e=n\.Menu\.buildFromTemplate/, + /setLinuxTrayContextMenu\(\).*let e=n\.Menu\.buildFromTemplate/, ); }); diff --git a/scripts/patches/core/all-linux/main-process/lifecycle/patch.js b/scripts/patches/core/all-linux/main-process/lifecycle/patch.js index db9848ede..4ec297744 100644 --- a/scripts/patches/core/all-linux/main-process/lifecycle/patch.js +++ b/scripts/patches/core/all-linux/main-process/lifecycle/patch.js @@ -6,6 +6,7 @@ const { const { applyLinuxQuitGuardPatch, applyLinuxExplicitQuitPromptBypassPatch, + applyLinuxSqliteBackfillQuitClosePatch, applyLinuxWillQuitDrainTimeoutPatch, applyLinuxExplicitTrayQuitPatch, applyLinuxExplicitIpcQuitPatch, @@ -26,6 +27,13 @@ module.exports = [ ciPolicy: "required-upstream", apply: applyLinuxExplicitQuitPromptBypassPatch, }), + mainBundlePatch({ + id: "linux-sqlite-backfill-quit-close", + phase: "main-bundle", + order: 15, + ciPolicy: "required-upstream", + apply: applyLinuxSqliteBackfillQuitClosePatch, + }), mainBundlePatch({ id: "linux-explicit-quit-drain-timeout", phase: "main-bundle", diff --git a/scripts/patches/impl/launch-actions.js b/scripts/patches/impl/launch-actions.js index f956b81cb..48fbdc249 100644 --- a/scripts/patches/impl/launch-actions.js +++ b/scripts/patches/impl/launch-actions.js @@ -15,9 +15,6 @@ const { // Launch-action patches keep second launches, hotkey windows, and persisted // Linux settings coordinated with the generated launcher. -const linuxQuitStateHelpers = - "let codexLinuxQuitInProgress=!1,codexLinuxExplicitQuitApproved=!1,codexLinuxMarkQuitInProgress=()=>{codexLinuxQuitInProgress=!0},codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress()},codexLinuxShouldBypassQuitPrompt=()=>codexLinuxExplicitQuitApproved===!0,codexLinuxIsQuitInProgress=()=>codexLinuxQuitInProgress===!0,"; - function persistedLinuxSettingsKeysSource() { return `[${Object.values(linuxSettingsKeys).map((key) => `\`${key}\``).join(",")}]`; } @@ -117,19 +114,13 @@ function buildSemanticLinuxLaunchActionPatch({ globalStateExpr, reporterVar, disposableVar, - appVar, freshWindowExpr, }) { const notificationPrefix = notificationVar == null ? "" : `${notificationVar}.desktopNotificationManager.dismissByNavigationPath(e),`; - const quitState = linuxQuitStateHelpers; - const directHandler = appVar == null - ? "" - : `,codexLinuxSecondInstanceHandler=(e,t)=>{codexLinuxHandleLaunchActionArgsFallback(t,()=>{${fallbackFn}()})},codexLinuxBeforeQuitHandler=()=>{typeof codexLinuxMarkQuitInProgress===\`function\`&&codexLinuxMarkQuitInProgress()}`; - const startup = appVar == null - ? `process.platform===\`linux\`&&codexLinuxStartLaunchActionSocket();${setterVar}(e=>{codexLinuxHandleLaunchActionArgsFallback(e,()=>{${fallbackFn}()})});` - : `process.platform===\`linux\`&&(${appVar}.app.on(\`before-quit\`,codexLinuxBeforeQuitHandler),${disposableVar}.add(()=>{${appVar}.app.off(\`before-quit\`,codexLinuxBeforeQuitHandler)}),codexLinuxStartLaunchActionSocket(),${appVar}.app.on(\`second-instance\`,codexLinuxSecondInstanceHandler),${disposableVar}.add(()=>{${appVar}.app.off(\`second-instance\`,codexLinuxSecondInstanceHandler)}));${setterVar}(e=>{codexLinuxHandleLaunchActionArgsFallback(e,()=>{${fallbackFn}()})});`; + const quitState = "let "; + const startup = `process.platform===\`linux\`&&codexLinuxStartLaunchActionSocket();${setterVar}(e=>{codexLinuxHandleLaunchActionArgsFallback(e,()=>{${fallbackFn}()})});`; const ensureHostWindowCall = hostExpr == null ? `${windowManagerVar}.ensureHostWindow()` : `${windowManagerVar}.ensureHostWindow(${hostExpr})`; const createFreshWindow = freshWindowExpr ?? ((pathExpr) => `${windowManagerVar}.${createFreshWindowMethod}(${pathExpr})`); @@ -137,7 +128,7 @@ function buildSemanticLinuxLaunchActionPatch({ "codexLinuxDefaultLaunchActionSocket=()=>{let e=codexLinuxLaunchActionAppId(),t=codexLinuxLaunchActionInstanceId(),n=process.env.XDG_RUNTIME_DIR?.trim(),r=require(`node:path`);if(n&&n.length>0)return t?r.join(n,e,`instances`,t,`launch-action.sock`):r.join(n,e,`launch-action.sock`);let i=process.env.XDG_STATE_HOME?.trim(),a=process.env.HOME?.trim();if((!i||i.length===0)&&a&&a.length>0)i=r.join(a,`.local`,`state`);if(!i||i.length===0)return null;return t?r.join(i,e,`instances`,t,`launch-action.sock`):r.join(i,e,`launch-action.sock`)}"; const startSocket = `codexLinuxStartLaunchActionSocket=()=>{if(process.platform!==\`linux\`)return;try{let e=process.env.CODEX_DESKTOP_LAUNCH_ACTION_SOCKET?.trim()||codexLinuxDefaultLaunchActionSocket();if(!e||!codexLinuxIsWarmStartEnabled())return;let n=require(\`node:path\`),r=require(\`node:fs\`),i=require(\`node:net\`);r.mkdirSync(n.dirname(e),{recursive:!0,mode:448}),r.rmSync(e,{force:!0});let a=i.createServer(t=>{let n=\`\`,r=!1,i=()=>{if(r)return;r=!0;let i=[];try{let e=JSON.parse(n.trim());Array.isArray(e.argv)&&(i=e.argv.filter(e=>typeof e===\`string\`))}catch(e){t.end?.(\`error\\n\`);return}codexLinuxHandleLaunchActionArgs(i).then(e=>e?void 0:${fallbackFn}()).then(()=>{t.end?.(\`ok\\n\`)}).catch(e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to handle Linux launch action socket\`,{kind:\`linux-launch-action-socket-failed\`}),t.end?.(\`error\\n\`)})};t.on(\`error\`,e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed Linux launch action socket client\`,{kind:\`linux-launch-action-socket-client-error\`})}),t.setEncoding?.(\`utf8\`),t.on(\`data\`,e=>{n+=e,n.includes(\`\\n\`)?i():n.length>65536&&t.destroy()}),t.on(\`end\`,i)});a.on(\`error\`,e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed Linux launch action socket\`,{kind:\`linux-launch-action-socket-error\`})}),a.listen(e),${disposableVar}.add(()=>{a.close(),r.rmSync(e,{force:!0})})}catch(e){${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to start Linux launch action socket\`,{kind:\`linux-launch-action-socket-start-failed\`})}}`; - return `${quitState}codexLinuxGetSetting=e=>process.platform!==\`linux\`||${globalStateExpr}.get(e)!==!1,codexLinuxIsTrayEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.systemTray}\`),codexLinuxIsWarmStartEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.warmStart}\`),codexLinuxIsPromptWindowEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.promptWindow}\`),codexLinuxLaunchActionAppId=()=>{let e=process.env.CODEX_LINUX_APP_ID||process.env.CODEX_APP_ID||\`codex-desktop\`;return/^[A-Za-z0-9._-]+$/.test(e)?e:\`codex-desktop\`},codexLinuxLaunchActionInstanceId=()=>{let e=process.env.CODEX_LINUX_INSTANCE_ID?.trim();return e&&/^[A-Za-z0-9._-]+$/.test(e)?e:null},${defaultSocket},${openerFn}=async(e,t)=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let ${currentWindowVar}=${getPrimaryWindowCall},${createdWindowVar}=${currentWindowVar}??await ${createFreshWindow("e")};${createdWindowVar}!=null&&(${notificationPrefix}${currentWindowVar}!=null&&t.navigateExistingWindow&&${routeVar}.navigateToRoute(${createdWindowVar},e),${focusFn}(${createdWindowVar}))},codexLinuxGetHotkeyWindowController=()=>typeof ${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController===\`function\`?${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController():${windowManagerVar}.hotkeyWindowLifecycleManager,codexLinuxShowHotkeyWindow=async()=>{let e=codexLinuxGetHotkeyWindowController();typeof e.openHome===\`function\`?await e.openHome():typeof e.show===\`function\`?await e.show():await ${ensureHostWindowCall}},codexLinuxOpenQuickChat=async()=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let e=${getPrimaryWindowCall},t=e??await ${createFreshWindow("`/`")};t!=null&&(${windowManagerVar}.windowManager.sendMessageToWindow(t,{type:\`new-quick-chat\`}),${focusFn}(t))},codexLinuxHasDeepLink=e=>Array.isArray(e)&&e.some(e=>typeof e===\`string\`&&(e.startsWith(\`codex://\`)||e.startsWith(\`codex-browser-sidebar://\`))),codexLinuxHandleLaunchActionArgs=async e=>(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())?!0:codexLinuxHasDeepLink(e)&&${deepLinksVar}.deepLinks.queueProcessArgs(e)?!0:Array.isArray(e)&&(e.includes(\`--prompt-chat\`)||e.includes(\`--hotkey-window\`))?(codexLinuxIsPromptWindowEnabled()?(await codexLinuxShowHotkeyWindow(),!0):!1):Array.isArray(e)&&e.includes(\`--quick-chat\`)?(await codexLinuxOpenQuickChat(),!0):Array.isArray(e)&&e.includes(\`--new-chat\`)?(await ${openerFn}(\`/\`,{navigateExistingWindow:!0}),!0):!1,codexLinuxHandleLaunchActionArgsFallback=(e,t)=>{if(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())return;codexLinuxHandleLaunchActionArgs(e).then(e=>{e||t()}).catch(e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to handle Linux launch action\`,{kind:\`linux-launch-action-failed\`}),t()})},codexLinuxPrewarmHotkeyWindow=()=>{if(!codexLinuxIsPromptWindowEnabled())return;try{let e=codexLinuxGetHotkeyWindowController();typeof e.prewarm===\`function\`&&e.prewarm()}catch(e){${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to prewarm Linux hotkey window\`,{kind:\`linux-hotkey-window-prewarm-failed\`})}},${startSocket}${directHandler};${startup}`; + return `${quitState}codexLinuxGetSetting=e=>process.platform!==\`linux\`||${globalStateExpr}.get(e)!==!1,codexLinuxIsTrayEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.systemTray}\`),codexLinuxIsWarmStartEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.warmStart}\`),codexLinuxIsPromptWindowEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.promptWindow}\`),codexLinuxLaunchActionAppId=()=>{let e=process.env.CODEX_LINUX_APP_ID||process.env.CODEX_APP_ID||\`codex-desktop\`;return/^[A-Za-z0-9._-]+$/.test(e)?e:\`codex-desktop\`},codexLinuxLaunchActionInstanceId=()=>{let e=process.env.CODEX_LINUX_INSTANCE_ID?.trim();return e&&/^[A-Za-z0-9._-]+$/.test(e)?e:null},${defaultSocket},${openerFn}=async(e,t)=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let ${currentWindowVar}=${getPrimaryWindowCall},${createdWindowVar}=${currentWindowVar}??await ${createFreshWindow("e")};${createdWindowVar}!=null&&(${notificationPrefix}${currentWindowVar}!=null&&t.navigateExistingWindow&&${routeVar}.navigateToRoute(${createdWindowVar},e),${focusFn}(${createdWindowVar}))},codexLinuxGetHotkeyWindowController=()=>typeof ${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController===\`function\`?${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController():${windowManagerVar}.hotkeyWindowLifecycleManager,codexLinuxShowHotkeyWindow=async()=>{let e=codexLinuxGetHotkeyWindowController();typeof e.openHome===\`function\`?await e.openHome():typeof e.show===\`function\`?await e.show():await ${ensureHostWindowCall}},codexLinuxOpenQuickChat=async()=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let e=${getPrimaryWindowCall},t=e??await ${createFreshWindow("`/`")};t!=null&&(${windowManagerVar}.windowManager.sendMessageToWindow(t,{type:\`new-quick-chat\`}),${focusFn}(t))},codexLinuxHasDeepLink=e=>Array.isArray(e)&&e.some(e=>typeof e===\`string\`&&(e.startsWith(\`codex://\`)||e.startsWith(\`codex-browser-sidebar://\`))),codexLinuxHandleLaunchActionArgs=async e=>(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())?!0:codexLinuxHasDeepLink(e)&&${deepLinksVar}.deepLinks.queueProcessArgs(e)?!0:Array.isArray(e)&&(e.includes(\`--prompt-chat\`)||e.includes(\`--hotkey-window\`))?(codexLinuxIsPromptWindowEnabled()?(await codexLinuxShowHotkeyWindow(),!0):!1):Array.isArray(e)&&e.includes(\`--quick-chat\`)?(await codexLinuxOpenQuickChat(),!0):Array.isArray(e)&&e.includes(\`--new-chat\`)?(await ${openerFn}(\`/\`,{navigateExistingWindow:!0}),!0):!1,codexLinuxHandleLaunchActionArgsFallback=(e,t)=>{if(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())return;codexLinuxHandleLaunchActionArgs(e).then(e=>{e||t()}).catch(e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to handle Linux launch action\`,{kind:\`linux-launch-action-failed\`}),t()})},codexLinuxPrewarmHotkeyWindow=()=>{if(!codexLinuxIsPromptWindowEnabled())return;try{let e=codexLinuxGetHotkeyWindowController();typeof e.prewarm===\`function\`&&e.prewarm()}catch(e){${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to prewarm Linux hotkey window\`,{kind:\`linux-hotkey-window-prewarm-failed\`})}},${startSocket};${startup}`; } function applyCurrentSemanticLinuxLaunchActionArgsPatch(currentSource) { @@ -227,7 +218,6 @@ function applyCurrentSemanticLinuxLaunchActionArgsPatch(currentSource) { globalStateExpr, reporterVar, disposableVar, - appVar: null, freshWindowExpr, }); const suffix = separator === "," ? "let " : ""; @@ -241,20 +231,11 @@ function applyLinuxLaunchActionArgsPatch(currentSource) { let patchedSource = currentSource; if ( - patchedSource.includes("codexLinuxQuitInProgress=!1") && - patchedSource.includes("codexLinuxExplicitQuitApproved=!1") && - patchedSource.includes("codexLinuxMarkQuitInProgress=()=>{codexLinuxQuitInProgress=!0}") && - patchedSource.includes("codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress()}") && - patchedSource.includes("codexLinuxShouldBypassQuitPrompt=()=>codexLinuxExplicitQuitApproved===!0") && - patchedSource.includes("codexLinuxIsQuitInProgress=()=>codexLinuxQuitInProgress===!0") && patchedSource.includes("codexLinuxGetSetting=e=>") && patchedSource.includes("codexLinuxGetHotkeyWindowController=()=>") && patchedSource.includes("codexLinuxPrewarmHotkeyWindow=()=>") && patchedSource.includes("codexLinuxStartLaunchActionSocket=()=>") && - ( - patchedSource.includes("n.app.on(`before-quit`,codexLinuxBeforeQuitHandler)") || - /process\.platform===`linux`&&codexLinuxStartLaunchActionSocket\(\);[A-Za-z_$][\w$]*\(e=>\{codexLinuxHandleLaunchActionArgsFallback\(e,\(\)=>\{[A-Za-z_$][\w$]*\(\)\}\)\}\)/.test(patchedSource) - ) && + /process\.platform===`linux`&&codexLinuxStartLaunchActionSocket\(\);[A-Za-z_$][\w$]*\(e=>\{codexLinuxHandleLaunchActionArgsFallback\(e,\(\)=>\{[A-Za-z_$][\w$]*\(\)\}\)\}\)/.test(patchedSource) && !patchedSource.includes("codexLinuxOpenNewChat") ) { return patchedSource; diff --git a/scripts/patches/impl/main-process/quit-lifecycle.js b/scripts/patches/impl/main-process/quit-lifecycle.js index 1e6bf23e2..21a39f1d8 100644 --- a/scripts/patches/impl/main-process/quit-lifecycle.js +++ b/scripts/patches/impl/main-process/quit-lifecycle.js @@ -1,7 +1,10 @@ "use strict"; function applyLinuxQuitGuardPatch(currentSource) { - if (currentSource.includes("codexLinuxExplicitQuitApproved=!1")) { + if ( + currentSource.includes("codexLinuxArmQuitWatchdog=()=>") && + /\.app\.on\(`will-quit`,\(\)=>\{process\.platform===`linux`&&codexLinuxCommitQuit\(\)\}\)/.test(currentSource) + ) { return currentSource; } @@ -12,7 +15,7 @@ function applyLinuxQuitGuardPatch(currentSource) { const matchedPrefix = currentBundlerQuitGuardMatch[0]; const electronVar = currentBundlerQuitGuardMatch[1]; const quitGuardSuffix = - `let codexLinuxTray=null,codexLinuxRegisterTray=e=>(codexLinuxTray=e,e),codexLinuxDestroyTray=()=>{if(process.platform!==\`linux\`)return;let e=codexLinuxTray;codexLinuxTray=null;try{e?.destroy()}catch{}},codexLinuxQuitInProgress=!1,codexLinuxExplicitQuitApproved=!1,codexLinuxExplicitQuitDrainTimeoutMs=3e3,codexLinuxMarkQuitInProgress=()=>{codexLinuxQuitInProgress=!0,codexLinuxDestroyTray()},codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress()},codexLinuxShouldBypassQuitPrompt=()=>codexLinuxExplicitQuitApproved===!0,codexLinuxIsQuitInProgress=()=>codexLinuxQuitInProgress===!0;${electronVar}.app.on(\`before-quit\`,()=>codexLinuxDestroyTray());`; + `let codexLinuxTray=null,codexLinuxRegisterTray=e=>(codexLinuxTray=e,e),codexLinuxIsTrayAlive=()=>{let e=codexLinuxTray;if(e==null)return!1;try{return typeof e.isDestroyed===\`function\`?!e.isDestroyed():!0}catch{return!1}},codexLinuxDestroyTray=()=>{if(process.platform!==\`linux\`)return;let e=codexLinuxTray;codexLinuxTray=null;try{e?.destroy()}catch{}},codexLinuxQuitAttempting=!1,codexLinuxQuitCommitted=!1,codexLinuxQuitCommitCallback=null,codexLinuxExplicitQuitTicket=!1,codexLinuxQuitWatchdogTimer=null,codexLinuxExplicitQuitDrainTimeoutMs=3e3,codexLinuxQuitHardExitTimeoutMs=8e3,codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitTicket=!0,queueMicrotask(()=>{codexLinuxExplicitQuitTicket=!1})},codexLinuxCancelQuitAttempt=()=>{if(codexLinuxQuitCommitted===!0)return;codexLinuxQuitAttempting=!1,codexLinuxQuitCommitCallback=null},codexLinuxAcceptQuitAttempt=(e,t)=>{if(process.platform!==\`linux\`){e();return}codexLinuxQuitAttempting=!0,codexLinuxQuitCommitCallback=e,queueMicrotask(()=>{t?.defaultPrevented&&codexLinuxCancelQuitAttempt()})},codexLinuxShouldBypassQuitPrompt=()=>{if(codexLinuxQuitCommitted===!0)return!0;if(codexLinuxExplicitQuitTicket!==!0)return!1;codexLinuxExplicitQuitTicket=!1;return!0},codexLinuxIsQuitInProgress=()=>codexLinuxQuitAttempting===!0||codexLinuxQuitCommitted===!0,codexLinuxForceExit=()=>{try{codexLinuxDestroyTray()}catch{}try{${electronVar}.app.exit(0)}catch{}try{process.exit(0)}catch{}},codexLinuxArmQuitWatchdog=()=>{if(process.platform!==\`linux\`)return;if(codexLinuxQuitWatchdogTimer!=null)return;let e=setTimeout(codexLinuxForceExit,typeof codexLinuxQuitHardExitTimeoutMs===\`number\`?codexLinuxQuitHardExitTimeoutMs:8e3);e.unref?.(),codexLinuxQuitWatchdogTimer=e},codexLinuxCommitQuit=()=>{if(codexLinuxQuitCommitted===!0)return;codexLinuxQuitCommitted=!0,codexLinuxQuitAttempting=!1,codexLinuxExplicitQuitTicket=!1;let e=codexLinuxQuitCommitCallback;codexLinuxQuitCommitCallback=null;try{e?.()}catch{}codexLinuxDestroyTray(),codexLinuxArmQuitWatchdog()};${electronVar}.app.on(\`will-quit\`,()=>{process.platform===\`linux\`&&codexLinuxCommitQuit()});`; return currentSource.replace(matchedPrefix, `${matchedPrefix}${quitGuardSuffix}`); } @@ -24,14 +27,12 @@ function applyLinuxQuitGuardPatch(currentSource) { } function linuxExplicitQuitExpression() { - return "typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),"; + return "typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit(),"; } function applyLinuxWillQuitDrainTimeoutPatch(currentSource) { let patchedSource = currentSource; - const explicitQuitDrainGuard = - "process.platform===`linux`&&(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())"; let patchedAny = false; const drainRegex = @@ -40,13 +41,13 @@ function applyLinuxWillQuitDrainTimeoutPatch(currentSource) { drainRegex, (_match, firstDrainVar, secondDrainVar, flushDisposeVar, disposablesVar, electronVar) => { patchedAny = true; - return `(()=>{let codexLinuxFinalizeQuit=()=>{${flushDisposeVar}(),${disposablesVar}.dispose(),${electronVar}.app.quit()},codexLinuxDrainPromise=Promise.all([${firstDrainVar}.flush(),${secondDrainVar}.flush()]);if(${explicitQuitDrainGuard}){Promise.race([codexLinuxDrainPromise,new Promise(e=>setTimeout(e,typeof codexLinuxExplicitQuitDrainTimeoutMs===\`number\`?codexLinuxExplicitQuitDrainTimeoutMs:3e3))]).finally(codexLinuxFinalizeQuit);return}codexLinuxDrainPromise.finally(codexLinuxFinalizeQuit)})()`; + return `(()=>{let codexLinuxFinalizeQuit=()=>{${flushDisposeVar}(),${disposablesVar}.dispose(),${electronVar}.app.quit()},codexLinuxDrainPromise=Promise.all([${firstDrainVar}.flush(),${secondDrainVar}.flush()]);if(process.platform===\`linux\`){typeof codexLinuxCommitQuit===\`function\`&&codexLinuxCommitQuit();let codexLinuxLinuxFinalizeQuit=()=>{try{${flushDisposeVar}()}catch{}try{${disposablesVar}.dispose()}catch{}try{${electronVar}.app.exit(0)}catch{}process.exit(0)};Promise.race([codexLinuxDrainPromise.catch(()=>{}),new Promise(e=>setTimeout(e,typeof codexLinuxExplicitQuitDrainTimeoutMs===\`number\`?codexLinuxExplicitQuitDrainTimeoutMs:3e3))]).finally(codexLinuxLinuxFinalizeQuit);return}codexLinuxDrainPromise.finally(codexLinuxFinalizeQuit)})()`; }, ); if ( !patchedAny && - !patchedSource.includes("codexLinuxDrainPromise=Promise.all(") && + !patchedSource.includes("codexLinuxLinuxFinalizeQuit=()=>") && patchedSource.includes("n.app.on(`will-quit`,") && patchedSource.includes(".flush()") ) { @@ -62,54 +63,79 @@ function applyLinuxExplicitQuitPromptBypassPatch(currentSource) { const promptBypassExpression = "(typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt())||"; const promptBypassGuard = `if(${promptBypassExpression}`; - const quitMarkerExpression = - "process.platform===`linux`&&typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),"; const beforeQuitNeedle = "if(e||i.canQuitWithoutPrompt()||r||!s&&!c){g=!0,a.markAppQuitting();return}"; const beforeQuitPatch = - `if(${promptBypassExpression}e||i.canQuitWithoutPrompt()||r||!s&&!c){${quitMarkerExpression}g=!0,a.markAppQuitting();return}`; + `if(${promptBypassExpression}e||i.canQuitWithoutPrompt()||r||!s&&!c){if(process.platform===\`linux\`&&typeof codexLinuxAcceptQuitAttempt===\`function\`){codexLinuxAcceptQuitAttempt(()=>{g=!0,a.markAppQuitting()},o);return}g=!0,a.markAppQuitting();return}`; const beforeQuitRegex = /if\(([A-Za-z_$][\w$]*)\|\|([A-Za-z_$][\w$]*)\.canQuitWithoutPrompt\(\)\|\|([A-Za-z_$][\w$]*)\|\|!([A-Za-z_$][\w$]*)&&!([A-Za-z_$][\w$]*)\)\{([A-Za-z_$][\w$]*)=!0,([A-Za-z_$][\w$]*)\.markAppQuitting\(\);return\}/g; const acceptedPromptRegex = /([A-Za-z_$][\w$]*)\.markQuitApproved\(\),([A-Za-z_$][\w$]*)=!0,([A-Za-z_$][\w$]*)\.markAppQuitting\(\)/g; - let patchedAny = false; + + const beforeQuitEventAt = (source, offset) => { + const prefix = source.slice(Math.max(0, offset - 1600), offset); + const matches = [...prefix.matchAll(/\.app\.on\(`before-quit`,([A-Za-z_$][\w$]*)=>\{/g)]; + return matches.at(-1)?.[1] ?? null; + }; if (patchedSource.includes(beforeQuitNeedle)) { - patchedAny = true; patchedSource = patchedSource.split(beforeQuitNeedle).join(beforeQuitPatch); } patchedSource = patchedSource.replace( beforeQuitRegex, - (_match, updateInstallVar, quitControllerVar, appQuittingVar, activeConversationVar, automationVar, quittingStateVar, appQuittingControllerVar) => { - patchedAny = true; - return `if(${promptBypassExpression}${updateInstallVar}||${quitControllerVar}.canQuitWithoutPrompt()||${appQuittingVar}||!${activeConversationVar}&&!${automationVar}){${quitMarkerExpression}${quittingStateVar}=!0,${appQuittingControllerVar}.markAppQuitting();return}`; - }, - ); - patchedSource = patchedSource.replace( - acceptedPromptRegex, - (match, quitControllerVar, quittingStateVar, appQuittingControllerVar, offset, source) => { - const prefix = source.slice(Math.max(0, offset - 120), offset); - if (prefix.includes("codexLinuxMarkQuitInProgress()")) { - return match; - } - patchedAny = true; - return `${quitMarkerExpression}${quitControllerVar}.markQuitApproved(),${quittingStateVar}=!0,${appQuittingControllerVar}.markAppQuitting()`; + (match, updateInstallVar, quitControllerVar, appQuittingVar, activeConversationVar, automationVar, quittingStateVar, appQuittingControllerVar, offset, source) => { + const eventVar = beforeQuitEventAt(source, offset); + if (eventVar == null) return match; + return `if(${promptBypassExpression}${updateInstallVar}||${quitControllerVar}.canQuitWithoutPrompt()||${appQuittingVar}||!${activeConversationVar}&&!${automationVar}){if(process.platform===\`linux\`&&typeof codexLinuxAcceptQuitAttempt===\`function\`){codexLinuxAcceptQuitAttempt(()=>{${quittingStateVar}=!0,${appQuittingControllerVar}.markAppQuitting()},${eventVar});return}${quittingStateVar}=!0,${appQuittingControllerVar}.markAppQuitting();return}`; }, ); + if (patchedSource.includes(promptBypassGuard)) { + patchedSource = patchedSource.replace( + acceptedPromptRegex, + (match, quitControllerVar, quittingStateVar, appQuittingControllerVar, offset, source) => { + const eventVar = beforeQuitEventAt(source, offset); + if (eventVar == null) return match; + return `${quitControllerVar}.markQuitApproved();if(process.platform===\`linux\`&&typeof codexLinuxAcceptQuitAttempt===\`function\`){codexLinuxAcceptQuitAttempt(()=>{${quittingStateVar}=!0,${appQuittingControllerVar}.markAppQuitting()},${eventVar});return}${quittingStateVar}=!0,${appQuittingControllerVar}.markAppQuitting()`; + }, + ); + } - if ( - !patchedAny && - !patchedSource.includes(promptBypassGuard) && - patchedSource.includes("showMessageBoxSync({type:`warning`,buttons:[`Quit`,`Cancel`]") && - patchedSource.includes(".canQuitWithoutPrompt()") - ) { + const hasQuitConfirmationSite = + patchedSource.includes(".canQuitWithoutPrompt()") || + (patchedSource.includes("showMessageBoxSync") && + patchedSource.includes(".markQuitApproved()")); + + if (!patchedSource.includes(promptBypassGuard) && hasQuitConfirmationSite) { console.warn("WARN: Could not find before-quit confirmation guard — skipping Linux explicit quit prompt bypass patch"); } return patchedSource; } +function applyLinuxSqliteBackfillQuitClosePatch(currentSource) { + const patchedCloseGuard = + /if\(!\([A-Za-z_$][\w$]*\|\|[A-Za-z_$][\w$]*\|\|process\.platform===`linux`&&typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)\)/; + if (patchedCloseGuard.test(currentSource)) { + return currentSource; + } + + const closeGuardRegex = + /([A-Za-z_$][\w$]*)\.on\(`close`,([A-Za-z_$][\w$]*)=>\{if\(!\(([A-Za-z_$][\w$]*)\|\|([A-Za-z_$][\w$]*)\)\)\{\2\.preventDefault\(\),\4=!0;try\{([A-Za-z_$][\w$]*)\(\)\}finally\{\4=!1\}\}\}\)/; + if (closeGuardRegex.test(currentSource)) { + return currentSource.replace( + closeGuardRegex, + (_match, windowVar, eventVar, disposedVar, reentrantVar, onQuitVar) => + `${windowVar}.on(\`close\`,${eventVar}=>{if(!(${disposedVar}||${reentrantVar}||process.platform===\`linux\`&&typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())){${eventVar}.preventDefault(),${reentrantVar}=!0;try{${onQuitVar}()}finally{${reentrantVar}=!1}}})`, + ); + } + + if (currentSource.includes("sqliteBackfillProgress") || currentSource.includes("onBackfillWait")) { + console.warn("WARN: Could not find SQLite backfill quit close guard — skipping Linux quit-attempt bypass patch"); + } + return currentSource; +} + function applyLinuxExplicitTrayQuitPatch(currentSource) { let patchedSource = currentSource; @@ -119,7 +145,7 @@ function applyLinuxExplicitTrayQuitPatch(currentSource) { const trayQuitPatch = `{label:rB(this.appName),click:()=>{${quitMarkerExpression}n.app.quit()}}`; const patchedTrayQuitRegex = - /\{label:[^{}]+,click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),[A-Za-z_$][\w$]*\.app\.quit\(\)\}\}/; + /\{label:[^{}]+,click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),[A-Za-z_$][\w$]*\.app\.quit\(\)\}\}/; const trayQuitRegex = /\{label:rB\(([^)]+)\),click:\(\)=>\{([A-Za-z_$][\w$]*)\.app\.quit\(\)\}\}/g; const genericTrayQuitRegex = @@ -165,7 +191,7 @@ function applyLinuxExplicitIpcQuitPatch(currentSource) { const quitAppRegex = /if\(([A-Za-z_$][\w$]*)\.type===`quit-app`\)\{([A-Za-z_$][\w$]*)\.app\.quit\(\);return\}/g; const patchedQuitAppRegex = - /if\([A-Za-z_$][\w$]*\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),[A-Za-z_$][\w$]*\.app\.quit\(\);return\}/; + /if\([A-Za-z_$][\w$]*\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),[A-Za-z_$][\w$]*\.app\.quit\(\);return\}/; let patchedAny = false; if (patchedSource.includes(quitAppNeedle)) { patchedAny = true; @@ -190,5 +216,6 @@ module.exports = { applyLinuxExplicitQuitPromptBypassPatch, applyLinuxExplicitTrayQuitPatch, applyLinuxQuitGuardPatch, + applyLinuxSqliteBackfillQuitClosePatch, applyLinuxWillQuitDrainTimeoutPatch, }; diff --git a/scripts/patches/impl/main-process/tray.js b/scripts/patches/impl/main-process/tray.js index 5604dcb02..acf69c75e 100644 --- a/scripts/patches/impl/main-process/tray.js +++ b/scripts/patches/impl/main-process/tray.js @@ -171,6 +171,24 @@ function applyLinuxTrayPatch(currentSource, iconPathExpression) { console.warn("WARN: Could not find tray platform guard — skipping Linux tray guard patch"); } + const upstreamTrayQuitTeardownPatchMarker = + "process.platform===`linux`?`will-quit`:`before-quit`"; + const upstreamTrayQuitTeardownRegex = + /([A-Za-z_$][\w$]*)\|\|=\(([A-Za-z_$][\w$]*)\.app\.on\(`before-quit`,\(\)=>\{([A-Za-z_$][\w$]*)\(\)\}\),!0\)/; + if (patchedSource.includes(upstreamTrayQuitTeardownPatchMarker)) { + // Already patched. + } else if (upstreamTrayQuitTeardownRegex.test(patchedSource)) { + patchedSource = patchedSource.replace( + upstreamTrayQuitTeardownRegex, + (_match, onceFlag, electronAlias, teardownFn) => + `${onceFlag}||=(${electronAlias}.app.on(process.platform===\`linux\`?\`will-quit\`:\`before-quit\`,()=>{try{${teardownFn}()}catch{}}),!0)`, + ); + } else { + console.warn( + "WARN: Could not find upstream tray before-quit listener — skipping Linux upstream tray quit teardown patch", + ); + } + if (iconPathExpression != null) { const linuxIconFallback = `if(process.platform===\`linux\`){let __codexLinuxTrayIcon=${electronVar}.nativeImage.createFromPath(${packagedTrayIconPathExpression});if(!__codexLinuxTrayIcon.isEmpty())return{defaultIcon:__codexLinuxTrayIcon,chronicleRunningIcon:null};let __codexLinuxAppIcon=${electronVar}.nativeImage.createFromPath(${packagedAppIconPathExpression});if(!__codexLinuxAppIcon.isEmpty())return{defaultIcon:__codexLinuxAppIcon,chronicleRunningIcon:null};let __codexLinuxUpstreamTrayIcon=${electronVar}.nativeImage.createFromPath(${iconPathExpression});if(!__codexLinuxUpstreamTrayIcon.isEmpty())return{defaultIcon:__codexLinuxUpstreamTrayIcon,chronicleRunningIcon:null}}`; @@ -193,7 +211,7 @@ function applyLinuxTrayPatch(currentSource, iconPathExpression) { } const patchedCloseToTrayRegex = - /if\(\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&!\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)&&this\.options\.canHideLastWindowToTray\?\.\(\)===!0&&![A-Za-z_$][\w$]*\)\{[A-Za-z_$][\w$]*\.preventDefault\(\),[A-Za-z_$][\w$]*\.hide\(\);return\}/; + /if\(\(process\.platform===`win32`\|\|process\.platform===`linux`\)&&!this\.isAppQuitting&&\(typeof codexLinuxIsQuitInProgress!==`function`\|\|!codexLinuxIsQuitInProgress\(\)\)&&\(process\.platform!==`linux`\|\|typeof codexLinuxIsTrayAlive===`function`&&codexLinuxIsTrayAlive\(\)===!0\)&&this\.options\.canHideLastWindowToTray\?\.\(\)===!0&&![A-Za-z_$][\w$]*\)\{[A-Za-z_$][\w$]*\.preventDefault\(\),[A-Za-z_$][\w$]*\.hide\(\);return\}/; if (patchedCloseToTrayRegex.test(patchedSource)) { // Already patched with a newer minifier's window variable. } else { @@ -204,7 +222,7 @@ function applyLinuxTrayPatch(currentSource, iconPathExpression) { const [, gateMethodName, hasOtherWindowVar, eventVar, windowVar] = closeToTrayMatch; patchedSource = patchedSource.replace( closeToTrayRegex, - `if((process.platform===\`win32\`||process.platform===\`linux\`)&&!this.isAppQuitting&&!(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())&&this.options.${gateMethodName}?.()===!0&&!${hasOtherWindowVar}){${eventVar}.preventDefault(),${windowVar}.hide();return}`, + `if((process.platform===\`win32\`||process.platform===\`linux\`)&&!this.isAppQuitting&&(typeof codexLinuxIsQuitInProgress!==\`function\`||!codexLinuxIsQuitInProgress())&&(process.platform!==\`linux\`||typeof codexLinuxIsTrayAlive===\`function\`&&codexLinuxIsTrayAlive()===!0)&&this.options.${gateMethodName}?.()===!0&&!${hasOtherWindowVar}){${eventVar}.preventDefault(),${windowVar}.hide();return}`, ); } else { console.warn("WARN: Could not find close-to-tray condition — skipping Linux close-to-tray patch"); @@ -214,16 +232,18 @@ function applyLinuxTrayPatch(currentSource, iconPathExpression) { const trayContextMethodNeedle = "trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};constructor("; const trayContextMethodPatch = - `trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};setLinuxTrayContextMenu(){let e=${electronVar}.Menu.buildFromTemplate(this.getNativeTrayMenuItems());this.tray.setContextMenu?.(e);return e}constructor(`; + `trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};setLinuxTrayContextMenu(){if(this.tray==null||typeof this.tray.isDestroyed===\`function\`&&this.tray.isDestroyed())return null;let e=${electronVar}.Menu.buildFromTemplate(this.getNativeTrayMenuItems());this.tray.setContextMenu?.(e);return e}constructor(`; const trayControllerClassPrefix = "nativeTrayClickSuppressionReason=null;clearNativeTrayClickSuppressionTimeout=null;chronicleTrayIconRefreshInterval=null;chronicleTrayIconState=`default`;isNativeTrayMenuOpen=!1;trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};"; const trayRecoveryHelpers = `let codexLinuxTrayController=null,codexLinuxTrayRecoveryRegistered=!1,codexLinuxTrayRecoveryHandler=()=>{let e=codexLinuxTrayController;e?.setLinuxTrayContextMenu?.()},codexLinuxStopTrayRecovery=()=>{codexLinuxTrayRecoveryRegistered&&(${electronVar}.powerMonitor.removeListener?.(\`unlock-screen\`,codexLinuxTrayRecoveryHandler),${electronVar}.powerMonitor.removeListener?.(\`resume\`,codexLinuxTrayRecoveryHandler),${electronVar}.app.removeListener?.(\`will-quit\`,codexLinuxTrayQuitHandler),codexLinuxTrayRecoveryRegistered=!1),codexLinuxTrayController=null},codexLinuxTrayQuitHandler=()=>{codexLinuxStopTrayRecovery()},codexLinuxStartTrayRecovery=()=>{codexLinuxTrayRecoveryRegistered||(${electronVar}.powerMonitor.on(\`unlock-screen\`,codexLinuxTrayRecoveryHandler),${electronVar}.powerMonitor.on(\`resume\`,codexLinuxTrayRecoveryHandler),${electronVar}.app.once(\`will-quit\`,codexLinuxTrayQuitHandler),codexLinuxTrayRecoveryRegistered=!0)},codexLinuxSetTrayController=e=>(codexLinuxTrayController=e,codexLinuxStartTrayRecovery(),e);`; if (patchedSource.includes("setLinuxTrayContextMenu(){")) { - patchedSource = patchedSource.replace( - /setLinuxTrayContextMenu\(\)\{let e=[A-Za-z_$][\w$]*\.Menu\.buildFromTemplate\(this\.getNativeTrayMenuItems\(\)\);/, - `setLinuxTrayContextMenu(){let e=${electronVar}.Menu.buildFromTemplate(this.getNativeTrayMenuItems());`, - ); + if (!patchedSource.includes("setLinuxTrayContextMenu(){if(this.tray==null||typeof this.tray.isDestroyed===`function`&&this.tray.isDestroyed())return null;")) { + patchedSource = patchedSource.replace( + /setLinuxTrayContextMenu\(\)\{let e=[A-Za-z_$][\w$]*\.Menu\.buildFromTemplate\(this\.getNativeTrayMenuItems\(\)\);/, + `setLinuxTrayContextMenu(){if(this.tray==null||typeof this.tray.isDestroyed===\`function\`&&this.tray.isDestroyed())return null;let e=${electronVar}.Menu.buildFromTemplate(this.getNativeTrayMenuItems());`, + ); + } } else if (patchedSource.includes(trayContextMethodNeedle)) { patchedSource = patchedSource.replace(trayContextMethodNeedle, trayContextMethodPatch); } else { @@ -531,17 +551,13 @@ function applyLinuxSingleInstancePatch(currentSource) { const secondInstanceHandlerNeedle = "l(e=>{R.deepLinks.queueProcessArgs(e)||ie()});let ae="; - const secondInstanceHandlerExistingPatch = - "let codexLinuxSecondInstanceHandler=(e,t)=>{R.deepLinks.queueProcessArgs(t)||ie()};process.platform===`linux`&&(n.app.on(`second-instance`,codexLinuxSecondInstanceHandler),k.add(()=>{n.app.off(`second-instance`,codexLinuxSecondInstanceHandler)})),l(e=>{R.deepLinks.queueProcessArgs(e)||ie()});let ae="; const secondInstanceHandlerPatch = - "let codexLinuxSecondInstanceHandler=(e,t)=>{(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())?void 0:R.deepLinks.queueProcessArgs(t)||ie()},codexLinuxBeforeQuitHandler=()=>{typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress()};process.platform===`linux`&&(n.app.on(`before-quit`,codexLinuxBeforeQuitHandler),k.add(()=>{n.app.off(`before-quit`,codexLinuxBeforeQuitHandler)}),n.app.on(`second-instance`,codexLinuxSecondInstanceHandler),k.add(()=>{n.app.off(`second-instance`,codexLinuxSecondInstanceHandler)})),l(e=>{R.deepLinks.queueProcessArgs(e)||ie()});let ae="; + "let codexLinuxSecondInstanceHandler=(e,t)=>{(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())?void 0:R.deepLinks.queueProcessArgs(t)||ie()};process.platform===`linux`&&(n.app.on(`second-instance`,codexLinuxSecondInstanceHandler),k.add(()=>{n.app.off(`second-instance`,codexLinuxSecondInstanceHandler)})),l(e=>{R.deepLinks.queueProcessArgs(e)||ie()});let ae="; if ( - patchedSource.includes("codexLinuxBeforeQuitHandler=()=>{typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress()}") && - patchedSource.includes("(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())?void 0:R.deepLinks.queueProcessArgs(t)||ie()") + patchedSource.includes("(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())?void 0:R.deepLinks.queueProcessArgs(t)||ie()") && + patchedSource.includes("n.app.on(`second-instance`,codexLinuxSecondInstanceHandler)") ) { // Already patched. - } else if (patchedSource.includes(secondInstanceHandlerExistingPatch)) { - patchedSource = patchedSource.replace(secondInstanceHandlerExistingPatch, secondInstanceHandlerPatch); } else if (patchedSource.includes(secondInstanceHandlerNeedle)) { patchedSource = patchedSource.replace(secondInstanceHandlerNeedle, secondInstanceHandlerPatch); } else if (patchedSource.includes("setSecondInstanceArgsHandler")) { diff --git a/tests/scripts_smoke.sh b/tests/scripts_smoke.sh index 3d57a5dad..9bd98b9b4 100755 --- a/tests/scripts_smoke.sh +++ b/tests/scripts_smoke.sh @@ -7599,7 +7599,9 @@ async function la(e){let t=ua(e);if(t&&(0,a.statSync)(t).isFile()){n.shell.showI function ua(e){return e} var Ua=Mi({id:`systemDefault`,label:`System Default App`,icon:`apps/file-explorer.png`,kind:`systemDefault`,hidden:!0,darwin:{icon:`apps/finder.png`,detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},win32:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},linux:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)}}); async function Wa(e){return e} -async function Hw(e){return process.platform!==`win32`&&process.platform!==`darwin`?null:(zw=!0,Lw??Rw??(Rw=(async()=>{let r=await Ww(e.buildFlavor,e.appBrand,e.repoRoot),i=new n.Tray(r.defaultIcon);return i})()))} +var ire=!1; +function G9(){zw=!1} +async function Hw(e){return process.platform!==`win32`&&process.platform!==`darwin`?null:(zw=!0,Lw??Rw??(ire||=(n.app.on(`before-quit`,()=>{G9()}),!0),Rw=(async()=>{let r=await Ww(e.buildFlavor,e.appBrand,e.repoRoot),i=new n.Tray(r.defaultIcon);return i})()))} async function Ww(e,t,i){if(process.platform===`darwin`){return null}let r=K9(e,t,i);return r==null?{defaultIcon:await n.app.getFileIcon(process.execPath,{size:`small`}),chronicleRunningIcon:null}:{defaultIcon:r,chronicleRunningIcon:null}} function K9(e,t,r){let a=[(0,i.join)(r,`electron`,`src`,`icons`,`tray.png`)];for(let e of a){let t=n.nativeImage.createFromPath(e);if(!t.isEmpty())return t}return null} var pb=class{nativeTrayClickSuppressionReason=null;clearNativeTrayClickSuppressionTimeout=null;chronicleTrayIconRefreshInterval=null;chronicleTrayIconState=`default`;isNativeTrayMenuOpen=!1;trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};constructor(){this.tray={on(){},setContextMenu(){},popUpContextMenu(){}};this.onTrayButtonClick=()=>{};this.tray.on(`click`,()=>{this.onTrayButtonClick()}),this.tray.on(`right-click`,()=>{this.openNativeTrayMenu()})}async handleMessage(e){switch(e.type){case`tray-menu-threads-changed`:this.trayMenuThreads=e.trayMenuThreads;return}}openNativeTrayMenu(){this.updateChronicleTrayIcon();let e=n.Menu.buildFromTemplate(this.getNativeTrayMenuItems());e.once(`menu-will-show`,()=>{this.isNativeTrayMenuOpen=!0}),e.once(`menu-will-close`,()=>{this.isNativeTrayMenuOpen=!1,this.handleNativeTrayMenuClosed()}),this.tray.popUpContextMenu(e)}updateChronicleTrayIcon(){}getNativeTrayMenuItems(){return[]}} @@ -7616,8 +7618,9 @@ JS assert_contains "$extracted/.vite/build/main-test.js" 'process.platform!==`win32`&&process.platform!==`darwin`&&process.platform!==`linux`?null:' assert_contains "$extracted/.vite/build/main-test.js" 'nativeImage.createFromPath(process.resourcesPath+`/../content/webview/assets/app-test.png`)' assert_contains "$extracted/.vite/build/main-test.js" '(process.platform===`win32`||process.platform===`linux`)&&!this.isAppQuitting' - assert_contains "$extracted/.vite/build/main-test.js" '!this.isAppQuitting&&!(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())' - assert_contains "$extracted/.vite/build/main-test.js" 'setLinuxTrayContextMenu(){let e=n.Menu.buildFromTemplate(this.getNativeTrayMenuItems())' + assert_contains "$extracted/.vite/build/main-test.js" '!this.isAppQuitting&&(typeof codexLinuxIsQuitInProgress!==`function`||!codexLinuxIsQuitInProgress())' + assert_contains "$extracted/.vite/build/main-test.js" '(process.platform!==`linux`||typeof codexLinuxIsTrayAlive===`function`&&codexLinuxIsTrayAlive()===!0)' + assert_contains "$extracted/.vite/build/main-test.js" 'setLinuxTrayContextMenu(){if(this.tray==null||typeof this.tray.isDestroyed===`function`&&this.tray.isDestroyed())return null;let e=n.Menu.buildFromTemplate(this.getNativeTrayMenuItems())' assert_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&(codexLinuxSetTrayController(this),this.setLinuxTrayContextMenu()),this.tray.on(`click`' assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxTrayRecoveryHandler=()=>{let e=codexLinuxTrayController;e?.setLinuxTrayContextMenu?.()}' assert_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`?this.openNativeTrayMenu():this.onTrayButtonClick()' @@ -7626,6 +7629,8 @@ JS assert_contains "$extracted/.vite/build/main-test.js" 'if(process.platform===`linux`)return;e.once(`menu-will-show`' assert_contains "$extracted/.vite/build/main-test.js" 'this.trayMenuThreads=e.trayMenuThreads,process.platform===`linux`&&!(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())&&this.setLinuxTrayContextMenu?.()' assert_contains "$extracted/.vite/build/main-test.js" '(E||process.platform===`linux`&&(typeof codexLinuxIsTrayEnabled!==`function`||codexLinuxIsTrayEnabled()))&&oe();' + assert_contains "$extracted/.vite/build/main-test.js" 'ire||=(n.app.on(process.platform===`linux`?`will-quit`:`before-quit`,()=>{try{G9()}catch{}}),!0)' + assert_not_contains "$extracted/.vite/build/main-test.js" 'n.app.on(`before-quit`,()=>{G9()})' assert_not_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&this.tray.setContextMenu?.(e),this.tray.popUpContextMenu(e)' assert_not_contains "$output_log" 'WARN: Could not find tray' @@ -7638,7 +7643,7 @@ if (!closeSnippet) { throw new Error("Could not extract patched Linux close handler"); } -function registerCloseHandler({ quitInProgress = false, isAppQuitting = false, trayEnabled = true } = {}) { +function registerCloseHandler({ quitInProgress = false, isAppQuitting = false, trayEnabled = true, trayAlive = true } = {}) { const state = { hideCalls: 0 }; const controller = { isAppQuitting, @@ -7651,10 +7656,11 @@ function registerCloseHandler({ quitInProgress = false, isAppQuitting = false, t const factory = new Function( "process", "codexLinuxIsQuitInProgress", + "codexLinuxIsTrayAlive", "state", `return function(){const v=true;const f=\`local\`;const k={handlers:{},on(event,handler){this.handlers[event]=handler},hide(){state.hideCalls+=1}};${closeSnippet};return k.handlers.close;};`, ); - const makeHandler = factory({ platform: "linux" }, () => quitInProgress, state); + const makeHandler = factory({ platform: "linux" }, () => quitInProgress, () => trayAlive, state); const handler = makeHandler.call(controller); return { handler, state }; } @@ -7713,8 +7719,13 @@ if (result.event.prevented || result.state.hideCalls !== 0) { } result = runCloseWithoutHelper({ trayEnabled: true, isAppQuitting: false }); -if (!result.event.prevented || result.state.hideCalls !== 1) { - throw new Error("Linux close should still hide to tray when the quit helper is unavailable"); +if (result.event.prevented || result.state.hideCalls !== 0) { + throw new Error("Linux close should fail closed when the tray liveness helper is unavailable"); +} + +result = runClose({ trayEnabled: true, trayAlive: false, quitInProgress: false, isAppQuitting: false }); +if (result.event.prevented || result.state.hideCalls !== 0) { + throw new Error("Linux close should not hide when the registered tray is unavailable"); } NODE @@ -7724,20 +7735,23 @@ NODE assert_occurrence_count "$extracted/.vite/build/main-test.js" 'nativeImage.createFromPath(process.resourcesPath+`/../.codex-linux/codex-desktop-tray.png`)' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'nativeImage.createFromPath(process.resourcesPath+`/../.codex-linux/codex-desktop.png`)' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'nativeImage.createFromPath(process.resourcesPath+`/../content/webview/assets/app-test.png`)' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`)&&!this.isAppQuitting' '1' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform!==`linux`||typeof codexLinuxIsTrayAlive===`function`&&codexLinuxIsTrayAlive()===!0' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'setLinuxTrayContextMenu(){' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&(codexLinuxSetTrayController(this),this.setLinuxTrayContextMenu()),this.tray.on(`click`' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`?this.openNativeTrayMenu():this.onTrayButtonClick()' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress()' '3' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress()' '2' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxIsQuitInProgress!==`function`||!codexLinuxIsQuitInProgress()' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'openNativeTrayMenu(){if(process.platform===`linux`&&(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress()))return;' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'let e=process.platform===`linux`&&this.setLinuxTrayContextMenu?this.setLinuxTrayContextMenu():n.Menu.buildFromTemplate' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'if(process.platform===`linux`)return;e.once(`menu-will-show`' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&!(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())&&this.setLinuxTrayContextMenu?.()' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&(typeof codexLinuxIsTrayEnabled!==`function`||codexLinuxIsTrayEnabled()))&&oe' '1' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'ire||=(n.app.on(process.platform===`linux`?`will-quit`:`before-quit`,()=>{try{G9()}catch{}}),!0)' '1' assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxRegisterTray=e=>(codexLinuxTray=e,e)' assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxDestroyTray=()=>{if(process.platform!==`linux`)return;' - assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxMarkQuitInProgress=()=>{codexLinuxQuitInProgress=!0,codexLinuxDestroyTray()}' - assert_contains "$extracted/.vite/build/main-test.js" 'n.app.on(`before-quit`,()=>codexLinuxDestroyTray())' + assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxCommitQuit=()=>{if(codexLinuxQuitCommitted===!0)return;' + assert_contains "$extracted/.vite/build/main-test.js" 'n.app.on(`will-quit`,()=>{process.platform===`linux`&&codexLinuxCommitQuit()})' + assert_not_contains "$extracted/.vite/build/main-test.js" 'n.app.on(`before-quit`,()=>codexLinuxDestroyTray())' assert_contains "$extracted/.vite/build/main-test.js" 'i=typeof codexLinuxRegisterTray===`function`?codexLinuxRegisterTray(new n.Tray(r.defaultIcon)):new n.Tray(r.defaultIcon)' assert_not_contains "$extracted/.vite/build/main-test.js" 'codexLinuxTrayQuitDelayMs' } @@ -7754,19 +7768,23 @@ test_linux_explicit_quit_patch_smoke() { const x={o:e=>e};let s=require(`node:url`),n=require(`electron`);n=x.o(n);let l=require(`node:os`);l=x.o(l);let i=require(`node:path`);i=x.o(i);let d=require(`node:util`),q=require(`node:crypto`),a=require(`node:fs`);a=x.o(a); var pb=class{getNativeTrayMenuItems(){return[{label:rB(this.appName),click:()=>{n.app.quit()}}]}}; function qB(r,o){if(o.type===`quit-app`){n.app.quit();return}return o} -n.app.on(`before-quit`,o=>{let s=BI(),c=t.sr().some(e=>e.status===`ACTIVE`);if(e||i.canQuitWithoutPrompt()||r||!s&&!c){g=!0,a.markAppQuitting();return}let l=n.app.getName();if(n.dialog.showMessageBoxSync({type:`warning`,buttons:[`Quit`,`Cancel`],defaultId:0,cancelId:1,noLink:!0,title:`Quit ${l}?`,message:`Quit ${l}?`,detail:vB({hasInProgressLocalConversation:s,hasEnabledAutomations:c})})!==0){o.preventDefault();return}i.markQuitApproved(),g=!0,a.markAppQuitting()}); +n.app.on(`before-quit`,o=>{let s=BI(),c=t.sr().some(e=>e.status===`ACTIVE`);if(e||i.canQuitWithoutPrompt()||r||!s&&!c){g=!0,a.markAppQuitting();return}let l=n.app.getName(),u=h.H(),f=u.formatMessage({messageId:x6,defaultMessage:S6,values:{appName:l}});if(n.dialog.showMessageBoxSync({type:`warning`,buttons:[u.formatMessage({messageId:`desktop.quitConfirmation.quit`,defaultMessage:`Quit`}),u.formatMessage({messageId:`desktop.quitConfirmation.cancel`,defaultMessage:`Cancel`})],defaultId:0,cancelId:1,noLink:!0,title:f,message:f,detail:K6({nativeIntl:u,appName:l,hasInProgressLocalConversation:s,hasEnabledAutomations:c})})!==0){o.preventDefault();return}i.markQuitApproved(),g=!0,a.markAppQuitting()}); n.app.on(`will-quit`,e=>{if(g=!0,!h){if(i.shouldSkipDrainBeforeQuit()){mB({hotkeyWindowLifecycleManager:c,globalDictationLifecycleManager:l,flushAndDisposeContexts:d,disposables:f});return}e.preventDefault(),h=!0,c.dispose(),l.dispose(),Promise.all([u.flush(),p.flush()]).finally(()=>{d(),f.dispose(),n.app.quit()})}}); JS )" make_fake_extracted_asar "$extracted" "$bundle_body" node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1 - assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress()}' - assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxShouldBypassQuitPrompt=()=>codexLinuxExplicitQuitApproved===!0' - assert_contains "$extracted/.vite/build/main-test.js" '{label:rB(this.appName),click:()=>{typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),n.app.quit()}}' - assert_contains "$extracted/.vite/build/main-test.js" 'if(o.type===`quit-app`){typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),n.app.quit();return}' - assert_contains "$extracted/.vite/build/main-test.js" 'if((typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt())||e||i.canQuitWithoutPrompt()||r||!s&&!c){process.platform===`linux`&&typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),g=!0,a.markAppQuitting();return}' - assert_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),i.markQuitApproved(),g=!0,a.markAppQuitting()' + assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitTicket=!0,queueMicrotask(()=>{codexLinuxExplicitQuitTicket=!1})}' + assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxArmQuitWatchdog=()=>{if(process.platform!==`linux`)return;' + assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxAcceptQuitAttempt=(e,t)=>{if(process.platform!==`linux`){e();return}codexLinuxQuitAttempting=!0,codexLinuxQuitCommitCallback=e,queueMicrotask(()=>{t?.defaultPrevented&&codexLinuxCancelQuitAttempt()})}' + assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxShouldBypassQuitPrompt=()=>{if(codexLinuxQuitCommitted===!0)return!0;if(codexLinuxExplicitQuitTicket!==!0)return!1;codexLinuxExplicitQuitTicket=!1;return!0}' + assert_contains "$extracted/.vite/build/main-test.js" '{label:rB(this.appName),click:()=>{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit(),n.app.quit()}}' + assert_contains "$extracted/.vite/build/main-test.js" 'if(o.type===`quit-app`){typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit(),n.app.quit();return}' + assert_contains "$extracted/.vite/build/main-test.js" 'if((typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt())||e||i.canQuitWithoutPrompt()||r||!s&&!c){if(process.platform===`linux`&&typeof codexLinuxAcceptQuitAttempt===`function`){codexLinuxAcceptQuitAttempt(()=>{g=!0,a.markAppQuitting()},o);return}g=!0,a.markAppQuitting();return}' + assert_contains "$extracted/.vite/build/main-test.js" 'i.markQuitApproved();if(process.platform===`linux`&&typeof codexLinuxAcceptQuitAttempt===`function`){codexLinuxAcceptQuitAttempt(()=>{g=!0,a.markAppQuitting()},o);return}g=!0,a.markAppQuitting()' + assert_not_contains "$extracted/.vite/build/main-test.js" 'if((typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt())||e||i.canQuitWithoutPrompt()||r||!s&&!c){typeof codexLinuxArmQuitWatchdog' + assert_not_contains "$extracted/.vite/build/main-test.js" 'markQuitApproved(),typeof codexLinuxArmQuitWatchdog' assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxFinalizeQuit=()=>{d(),f.dispose(),n.app.quit()},codexLinuxDrainPromise=Promise.all(' assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxExplicitQuitDrainTimeoutMs' assert_contains "$extracted/.vite/build/main-test.js" 'setTimeout(e,typeof codexLinuxExplicitQuitDrainTimeoutMs' @@ -7781,45 +7799,41 @@ const fs = require("fs"); const source = fs.readFileSync(process.argv[2], "utf8"); const helperStart = source.indexOf("let codexLinuxTray=null"); -const helperEnd = source.indexOf(";n.app.on(`before-quit`,()=>codexLinuxDestroyTray())", helperStart) + 1; +const helperEnd = source.indexOf(";n.app.on(`will-quit`,()=>{process.platform===`linux`&&codexLinuxCommitQuit()})", helperStart) + 1; const helperSnippet = helperStart === -1 || helperEnd === 0 ? null : source.slice(helperStart, helperEnd); -const traySnippet = source.match(/\{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\)\}\}/)?.[0]; -const quitAppSnippet = source.match(/if\(o\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\);return\}/)?.[0]; -const beforeQuitSnippet = source.match(/if\(\(typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt\(\)\)\|\|e\|\|i\.canQuitWithoutPrompt\(\)\|\|r\|\|!s&&!c\)\{process\.platform===`linux`&&typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),g=!0,a\.markAppQuitting\(\);return\}/)?.[0]; +const traySnippet = source.match(/\{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),n\.app\.quit\(\)\}\}/)?.[0]; +const quitAppSnippet = source.match(/if\(o\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit\(\),n\.app\.quit\(\);return\}/)?.[0]; +const beforeQuitSnippet = source.match(/if\(\(typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt\(\)\)\|\|e\|\|i\.canQuitWithoutPrompt\(\)\|\|r\|\|!s&&!c\)\{if\(process\.platform===`linux`&&typeof codexLinuxAcceptQuitAttempt===`function`\)\{codexLinuxAcceptQuitAttempt\(\(\)=>\{g=!0,a\.markAppQuitting\(\)\},o\);return\}g=!0,a\.markAppQuitting\(\);return\}/)?.[0]; if (!helperSnippet || !traySnippet || !quitAppSnippet || !beforeQuitSnippet) { throw new Error("Could not extract explicit quit snippets"); } function runTrayQuit({ withHelper = true } = {}) { - const state = { markCalls: 0, prepareCalls: 0, quitCalls: 0 }; + const state = { prepareCalls: 0, quitCalls: 0 }; const app = { quit() { state.quitCalls += 1; } }; - const mark = () => { state.markCalls += 1; }; - const prepare = withHelper ? () => { state.prepareCalls += 1; mark(); } : undefined; + const prepare = withHelper ? () => { state.prepareCalls += 1; } : undefined; const factory = new Function( "n", "rB", "codexLinuxPrepareForExplicitQuit", - "codexLinuxMarkQuitInProgress", `return (${traySnippet}).click;`, ); - const click = factory({ app }, () => "Quit", prepare, mark); + const click = factory({ app }, () => "Quit", prepare); click(); return state; } function runQuitApp({ withHelper = true } = {}) { - const state = { markCalls: 0, prepareCalls: 0, quitCalls: 0 }; + const state = { prepareCalls: 0, quitCalls: 0 }; const app = { quit() { state.quitCalls += 1; } }; - const mark = () => { state.markCalls += 1; }; - const prepare = withHelper ? () => { state.prepareCalls += 1; mark(); } : undefined; + const prepare = withHelper ? () => { state.prepareCalls += 1; } : undefined; const handler = new Function( "n", "codexLinuxPrepareForExplicitQuit", - "codexLinuxMarkQuitInProgress", "o", `${quitAppSnippet};return null;`, ); - handler({ app }, prepare, mark, { type: "quit-app" }); + handler({ app }, prepare, { type: "quit-app" }); return state; } @@ -7828,7 +7842,7 @@ function runBeforeQuitBypass() { const scope = new Function( "BI", "t", - `${helperSnippet}return {runBeforeQuitCheck(e,i,r,a){let s=BI(),c=t.sr().some(e=>e.status===\`ACTIVE\`);${beforeQuitSnippet}return \`prompt\`;},prepare:codexLinuxPrepareForExplicitQuit,bypass:codexLinuxShouldBypassQuitPrompt,marked:codexLinuxIsQuitInProgress};`, + `${helperSnippet}return {runBeforeQuitCheck(o,e,i,r,a){let s=BI(),c=t.sr().some(e=>e.status===\`ACTIVE\`);${beforeQuitSnippet}return \`prompt\`;},prepare:codexLinuxPrepareForExplicitQuit,bypass:codexLinuxShouldBypassQuitPrompt,inProgress:codexLinuxIsQuitInProgress,commit:codexLinuxCommitQuit};`, )( () => true, { sr: () => [{ status: "ACTIVE" }] }, @@ -7839,40 +7853,43 @@ function runBeforeQuitBypass() { }; const appQuitting = { markAppQuitting() { state.markCalls += 1; } }; scope.prepare(); - const bypassed = scope.runBeforeQuitCheck(false, controller, false, appQuitting); - return { state, bypassed, shouldBypass: scope.bypass(), marked: scope.marked() }; + const bypassed = scope.runBeforeQuitCheck({ defaultPrevented: false }, false, controller, false, appQuitting); + const preCommit = { markCalls: state.markCalls, inProgress: scope.inProgress() }; + scope.commit(); + return { state, bypassed, shouldBypass: scope.bypass(), preCommit, committed: scope.inProgress() }; } let state = runTrayQuit(); -if (state.prepareCalls !== 1 || state.markCalls !== 1 || state.quitCalls !== 1) { +if (state.prepareCalls !== 1 || state.quitCalls !== 1) { throw new Error("tray quit should prepare explicit quit before quitting"); } state = runQuitApp(); -if (state.prepareCalls !== 1 || state.markCalls !== 1 || state.quitCalls !== 1) { +if (state.prepareCalls !== 1 || state.quitCalls !== 1) { throw new Error("quit-app IPC should prepare explicit quit before quitting"); } state = runTrayQuit({ withHelper: false }); -if (state.prepareCalls !== 0 || state.markCalls !== 1 || state.quitCalls !== 1) { - throw new Error("tray quit should still fall back to the quit-in-progress marker"); +if (state.prepareCalls !== 0 || state.quitCalls !== 1) { + throw new Error("tray quit should remain fail-soft when the explicit quit helper is unavailable"); } state = runQuitApp({ withHelper: false }); -if (state.prepareCalls !== 0 || state.markCalls !== 1 || state.quitCalls !== 1) { - throw new Error("quit-app IPC should still fall back to the quit-in-progress marker"); +if (state.prepareCalls !== 0 || state.quitCalls !== 1) { + throw new Error("quit-app IPC should remain fail-soft when the explicit quit helper is unavailable"); } state = runBeforeQuitBypass(); -if (!state.shouldBypass || state.bypassed !== undefined || state.state.markCalls !== 1 || !state.marked) { - throw new Error("before-quit should bypass the Linux quit confirmation after an explicit quit"); +if (state.shouldBypass !== true || state.bypassed !== undefined || state.preCommit.markCalls !== 0 || !state.preCommit.inProgress || state.state.markCalls !== 1 || !state.committed) { + throw new Error("before-quit should defer upstream quit state until will-quit commits"); } NODE node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1 - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress()}' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxShouldBypassQuitPrompt=()=>codexLinuxExplicitQuitApproved===!0' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress()' '2' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitTicket=!0,queueMicrotask(()=>{codexLinuxExplicitQuitTicket=!1})}' '1' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxArmQuitWatchdog=()=>{' '1' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxShouldBypassQuitPrompt=()=>{if(codexLinuxQuitCommitted===!0)return!0;' '1' + assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxPrepareForExplicitQuit===`function`&&codexLinuxPrepareForExplicitQuit()' '2' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt()' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxDrainPromise=Promise.all(' '1' } @@ -8497,11 +8514,10 @@ NODE node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1 assert_occurrence_count "$extracted/.vite/build/main-test.js" '!n.app.requestSingleInstanceLock()' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxQuitInProgress=!1' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxIsQuitInProgress=()=>codexLinuxQuitInProgress===!0' '1' + assert_not_contains "$extracted/.vite/build/main-test.js" 'codexLinuxQuitCommitted=!1' + assert_not_contains "$extracted/.vite/build/main-test.js" 'codexLinuxIsQuitInProgress=()=>codexLinuxQuitCommitted===!0' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxHandleLaunchActionArgs=' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxHandleLaunchActionArgs=async e=>(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())?!0:' '1' - assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxHandleLaunchActionArgsFallback=(e,t)=>{if(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())return;' '1' + assert_not_contains "$extracted/.vite/build/main-test.js" 'codexLinuxBeforeQuitHandler' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'e.includes(`--new-chat`)' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'e.includes(`--quick-chat`)' '1' assert_occurrence_count "$extracted/.vite/build/main-test.js" 'e.includes(`--prompt-chat`)' '1'