Skip to content

fix: lock state not persisting across quit/restart on Windows#484

Merged
lacymorrow merged 2 commits intomainfrom
fix/lock-state-quit
Mar 29, 2026
Merged

fix: lock state not persisting across quit/restart on Windows#484
lacymorrow merged 2 commits intomainfrom
fix/lock-state-quit

Conversation

@lacymorrow
Copy link
Copy Markdown
Owner

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:

  1. lockWindow(true) sets closable = false on the BrowserWindow
  2. Tray quit triggers app.quit(), but Electron can't close a window with closable = false
  3. The process lingers, holding onto global shortcuts
  4. On restart, the init sequence used a fragile 400ms setTimeout to send lock_window to the renderer, which could fire before the renderer's IPC listeners were ready

Fix:

  1. before-quit handler (register.js): Reset closable, focusable, and ignoreMouseEvents on all windows (main + shadows) so Electron can properly destroy them
  2. init.js: Replace the 400ms timeout with webContents.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

…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.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Comment on lines +98 to +112
} 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 )

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()
	}
}

Comment on lines +66 to +86
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 )

}

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
@lacymorrow lacymorrow merged commit 489579c into main Mar 29, 2026
1 of 2 checks passed
@lacymorrow lacymorrow deleted the fix/lock-state-quit branch March 29, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Closing CrossOver has locking/unlocking issue.

1 participant