fix: lock state not persisting across quit/restart on Windows#484
fix: lock state not persisting across quit/restart on Windows#484lacymorrow merged 2 commits intomainfrom
Conversation
…480) Two changes: 1. before-quit handler: Reset closable/focusable/ignoreMouseEvents on all windows so they can properly close. Previously, locked windows had closable=false which prevented destruction, leaving a zombie process on Windows portable builds. 2. init.js: Replace fragile 400ms setTimeout with did-finish-load event to ensure the renderer's IPC listeners are ready before sending lock_window. Fixes lock state not restoring properly on restart.
There was a problem hiding this comment.
Code Review
This pull request improves window initialization and shutdown logic to address reliability issues and prevent zombie processes on Windows. Key changes include replacing a fixed timeout with event-driven window state setup in init.js and ensuring all windows are marked as closable before the application quits in register.js. Review feedback focuses on reducing code duplication by refactoring the window setup scheduling and extracting window-unlocking logic into reusable helper functions.
src/main/init.js
Outdated
| } else if ( windows.win.webContents.isLoading() ) { | ||
|
|
||
| // First boot: wait for renderer to finish loading | ||
| windows.win.webContents.once( 'did-finish-load', () => { | ||
|
|
||
| setTimeout( setupLockState, 50 ) | ||
|
|
||
| } ) | ||
|
|
||
| } else { | ||
|
|
||
| // Page already loaded | ||
| setTimeout( setupLockState, 50 ) | ||
|
|
||
| } |
There was a problem hiding this comment.
The logic for scheduling setupLockState when the page is loading or already loaded is very similar. You can refactor this to reduce code duplication and improve readability.
} else {
const scheduleSetup = () => setTimeout( setupLockState, 50 )
if ( windows.win.webContents.isLoading() ) {
// First boot: wait for renderer to finish loading
windows.win.webContents.once( 'did-finish-load', scheduleSetup )
} else {
// Page already loaded
scheduleSetup()
}
}
src/main/register.js
Outdated
| const win = windows.win | ||
| if ( win && !win.isDestroyed() ) { | ||
|
|
||
| win.closable = true | ||
| win.setFocusable( true ) | ||
| win.setIgnoreMouseEvents( false ) | ||
|
|
||
| } | ||
|
|
||
| // Also unlock shadow windows | ||
| for ( const shadowWin of windows.shadowWindows ) { | ||
|
|
||
| if ( shadowWin && !shadowWin.isDestroyed() ) { | ||
|
|
||
| shadowWin.closable = true | ||
| shadowWin.setFocusable( true ) | ||
| shadowWin.setIgnoreMouseEvents( false ) | ||
|
|
||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
To improve maintainability and reduce code duplication, you can extract the logic for making a window closable into a helper function. This logic is repeated for the main window and for each shadow window.
| const win = windows.win | |
| if ( win && !win.isDestroyed() ) { | |
| win.closable = true | |
| win.setFocusable( true ) | |
| win.setIgnoreMouseEvents( false ) | |
| } | |
| // Also unlock shadow windows | |
| for ( const shadowWin of windows.shadowWindows ) { | |
| if ( shadowWin && !shadowWin.isDestroyed() ) { | |
| shadowWin.closable = true | |
| shadowWin.setFocusable( true ) | |
| shadowWin.setIgnoreMouseEvents( false ) | |
| } | |
| } | |
| const makeWindowClosable = win => { | |
| if ( win && !win.isDestroyed() ) { | |
| win.closable = true | |
| win.setFocusable( true ) | |
| win.setIgnoreMouseEvents( false ) | |
| } | |
| } | |
| makeWindowClosable(windows.win) | |
| windows.shadowWindows.forEach(makeWindowClosable) |
- Extract makeWindowClosable helper in register.js to reduce duplication - Extract scheduleSetup in init.js to deduplicate the loading/loaded paths
Closes #480
Problem:
When CrossOver is quit from the tray while the crosshair is locked, the process stays alive as a zombie on Windows (portable builds). The locked window has
closable = false, which prevents Electron from destroying it during quit. On next launch, the keybind fires (plays sound) but lock state is out of sync because the old process still holds the global shortcut.Root cause:
lockWindow(true)setsclosable = falseon the BrowserWindowapp.quit(), but Electron can't close a window withclosable = falsesetTimeoutto sendlock_windowto the renderer, which could fire before the renderer's IPC listeners were readyFix:
before-quithandler (register.js): Resetclosable,focusable, andignoreMouseEventson all windows (main + shadows) so Electron can properly destroy theminit.js: Replace the 400ms timeout withwebContents.once('did-finish-load')to ensure the renderer is ready before restoring lock state. Includes a guard against double-initialization and proper fallback paths for reset vs first boot scenarios