From fcf5e5fe307df413713d6b715d24e3bae0828b1b Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sun, 29 Jun 2025 17:31:28 +0200 Subject: [PATCH] feat: improve shutdown cleanup --- electron/full-screen-blocker.ts | 8 +++++ electron/main-window.ts | 6 ++++ .../overlay-indicator/overlay-indicator.ts | 34 +++++++++++++------ electron/overlay-indicator/overlay-preload.ts | 9 ++++- electron/plugin-node-executor.ts | 6 ++++ electron/start-app.ts | 20 +++++++++-- 6 files changed, 69 insertions(+), 14 deletions(-) diff --git a/electron/full-screen-blocker.ts b/electron/full-screen-blocker.ts index 61ccb7072..56b6b7d20 100644 --- a/electron/full-screen-blocker.ts +++ b/electron/full-screen-blocker.ts @@ -66,6 +66,14 @@ export const initFullScreenBlocker = (IS_DEV: boolean): void => { evI.preventDefault(); } }); + + win.on('closed', () => { + // Clean up references + isFullScreenWindowOpen = false; + if (closeTimeout) { + clearTimeout(closeTimeout); + } + }); }, ); }; diff --git a/electron/main-window.ts b/electron/main-window.ts index 64c283d71..2bb9d6466 100644 --- a/electron/main-window.ts +++ b/electron/main-window.ts @@ -319,6 +319,12 @@ const appCloseHandler = (app: App): void => { } }); + mainWin.on('closed', () => { + // Dereference the window object + mainWin = null; + mainWinModule.win = null; + }); + mainWin.webContents.on('render-process-gone', (event, detailed) => { log('!crashed, reason: ' + detailed.reason + ', exitCode = ' + detailed.exitCode); if (detailed.reason == 'crashed') { diff --git a/electron/overlay-indicator/overlay-indicator.ts b/electron/overlay-indicator/overlay-indicator.ts index 00ca23f2a..a65faf5a1 100644 --- a/electron/overlay-indicator/overlay-indicator.ts +++ b/electron/overlay-indicator/overlay-indicator.ts @@ -66,21 +66,31 @@ export const destroyOverlayWindow = (): void => { // Remove IPC listeners ipcMain.removeAllListeners('overlay-show-main-window'); + ipcMain.removeAllListeners('overlay-set-ignore-mouse'); - if (overlayWindow) { - // Remove ALL event listeners - overlayWindow.removeAllListeners('close'); - overlayWindow.removeAllListeners('closed'); - overlayWindow.removeAllListeners('ready-to-show'); - overlayWindow.removeAllListeners('system-context-menu'); + if (overlayWindow && !overlayWindow.isDestroyed()) { + try { + // Remove ALL event listeners + overlayWindow.removeAllListeners(); - // Remove webContents listeners - if (overlayWindow.webContents) { - overlayWindow.webContents.removeAllListeners('context-menu'); + // Remove webContents listeners + if (overlayWindow.webContents && !overlayWindow.webContents.isDestroyed()) { + overlayWindow.webContents.removeAllListeners(); + } + + // Hide first to prevent visual issues + overlayWindow.hide(); + + // Set closable to ensure we can close it + overlayWindow.setClosable(true); + + // Force destroy the window + overlayWindow.destroy(); + } catch (e) { + // Window might already be destroyed + console.error('Error destroying overlay window:', e); } - // Force destroy the window - overlayWindow.destroy(); overlayWindow = null; } }; @@ -106,6 +116,7 @@ const createOverlayWindow = (): void => { // resizable: false, minimizable: false, maximizable: false, + closable: true, // Ensure window is closable hasShadow: false, // Disable shadow with transparent windows autoHideMenuBar: true, roundedCorners: false, // Disable rounded corners for better compatibility @@ -116,6 +127,7 @@ const createOverlayWindow = (): void => { disableDialogs: true, webSecurity: true, allowRunningInsecureContent: false, + backgroundThrottling: false, // Prevent throttling when hidden }, }); diff --git a/electron/overlay-indicator/overlay-preload.ts b/electron/overlay-indicator/overlay-preload.ts index 98b5dbe16..30bee8b9b 100644 --- a/electron/overlay-indicator/overlay-preload.ts +++ b/electron/overlay-indicator/overlay-preload.ts @@ -8,6 +8,13 @@ contextBridge.exposeInMainWorld('overlayAPI', { ipcRenderer.send('overlay-show-main-window'); }, onUpdateContent: (callback: (data: any) => void) => { - ipcRenderer.on('update-content', (event, data) => callback(data)); + const listener = (event: Electron.IpcRendererEvent, data: any): void => + callback(data); + ipcRenderer.on('update-content', listener); + + // Return cleanup function + return () => { + ipcRenderer.removeListener('update-content', listener); + }; }, }); diff --git a/electron/plugin-node-executor.ts b/electron/plugin-node-executor.ts index debe6c986..2b4a0026d 100644 --- a/electron/plugin-node-executor.ts +++ b/electron/plugin-node-executor.ts @@ -192,6 +192,12 @@ class PluginNodeExecutor { const timer = setTimeout(() => { killed = true; child.kill('SIGTERM'); + // Force kill after a short delay if process doesn't terminate + setTimeout(() => { + if (!child.killed) { + child.kill('SIGKILL'); + } + }, 1000); reject(new Error(`Script execution timed out after ${timeoutMs}ms`)); }, timeoutMs); diff --git a/electron/start-app.ts b/electron/start-app.ts index 8f055e53a..73613a320 100644 --- a/electron/start-app.ts +++ b/electron/start-app.ts @@ -5,6 +5,7 @@ import { app, BrowserWindow, globalShortcut, + ipcMain, powerMonitor, protocol, } from 'electron'; @@ -254,15 +255,30 @@ export const startApp = (): void => { }); appIN.on('before-quit', () => { + log('App before-quit: cleaning up resources'); + // Clean up overlay window before quitting destroyOverlayWindow(); + + // Remove all IPC listeners to prevent memory leaks + ipcMain.removeAllListeners(); + + // Clear any pending timeouts/intervals + if (global.gc) { + global.gc(); + } }); appIN.on('window-all-closed', () => { log('Quit after all windows being closed'); - // if (!IS_MAC) { + // Force quit the app app.quit(); - // } + + // If app doesn't quit within 2 seconds, force exit + setTimeout(() => { + log('Force exiting app as it did not quit properly'); + app.exit(0); + }, 2000); }); process.on('uncaughtException', (err) => { console.log(err);