From 8338f91487b5eb025e814b3f9eabdfae567cdee5 Mon Sep 17 00:00:00 2001 From: Chirag Chandrashekhar Date: Wed, 1 Apr 2026 10:54:50 +0530 Subject: [PATCH] fix: app crash on clicking close button (#7637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: app crash on clicking close button \n Added collection, workspace, and api spec watcher cleanup on app close method * fix: close file watchers before app exit to prevent crash on macOS Close all chokidar file watchers (collection, workspace, apiSpec) before the Node environment is torn down. The native FSEvents watchers run on their own threads and their cleanup races with FreeEnvironment, causing an abort when fse_instance_destroy tries to lock a destroyed mutex. Watchers are closed in both mainWindow.on('close') and app.on('before-quit') to cover the native close button path and the app.exit() path. * fix: move watcher cleanup from close handler to before-quit only The close event is cancelable — if the user cancels the unsaved changes dialog, watchers would remain closed for the rest of the session. Move closeAllWatchers() to before-quit which only fires on actual quit. --------- Co-authored-by: Chirag Chandrashekhar --- .../bruno-electron/src/app/apiSpecsWatcher.js | 10 ++++++++++ .../src/app/collection-watcher.js | 9 +++++++++ .../src/app/workspace-watcher.js | 18 ++++++++++++++++++ packages/bruno-electron/src/index.js | 7 +++++++ 4 files changed, 44 insertions(+) diff --git a/packages/bruno-electron/src/app/apiSpecsWatcher.js b/packages/bruno-electron/src/app/apiSpecsWatcher.js index b8ce55a62..490441225 100644 --- a/packages/bruno-electron/src/app/apiSpecsWatcher.js +++ b/packages/bruno-electron/src/app/apiSpecsWatcher.js @@ -141,6 +141,16 @@ class ApiSpecWatcher { delete this.watcherWorkspaces[watchPath]; } } + + closeAllWatchers() { + for (const [watchPath, watcher] of Object.entries(this.watchers)) { + try { + watcher?.close(); + } catch (err) {} + } + this.watchers = {}; + this.watcherWorkspaces = {}; + } } module.exports = ApiSpecWatcher; diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index c4aece50a..12ed78aac 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -958,6 +958,15 @@ class CollectionWatcher { .filter(([path, watcher]) => !!watcher) .map(([path, _watcher]) => path); } + + closeAllWatchers() { + for (const [watchPath, watcher] of Object.entries(this.watchers)) { + try { + watcher?.close(); + } catch (err) {} + } + this.watchers = {}; + } } const collectionWatcher = new CollectionWatcher(); diff --git a/packages/bruno-electron/src/app/workspace-watcher.js b/packages/bruno-electron/src/app/workspace-watcher.js index 1cc38c87c..ebeca7e2c 100644 --- a/packages/bruno-electron/src/app/workspace-watcher.js +++ b/packages/bruno-electron/src/app/workspace-watcher.js @@ -224,6 +224,24 @@ class WorkspaceWatcher { hasWatcher(workspacePath) { return Boolean(this.watchers[workspacePath]); } + + closeAllWatchers() { + for (const [watchPath, watcher] of Object.entries(this.watchers)) { + try { + watcher?.close(); + } catch (err) {} + } + this.watchers = {}; + + for (const [watchPath, watcher] of Object.entries(this.environmentWatchers)) { + try { + watcher?.close(); + } catch (err) {} + } + this.environmentWatchers = {}; + + dotEnvWatcher.closeAll(); + } } module.exports = WorkspaceWatcher; diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index e3a45931b..bf70bc274 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -122,6 +122,12 @@ const focusMainWindow = () => { } }; +const closeAllWatchers = () => { + collectionWatcher.closeAllWatchers(); + workspaceWatcher.closeAllWatchers(); + apiSpecWatcher.closeAllWatchers(); +}; + // Parse protocol URL from command line arguments (if any) appProtocolUrl = getAppProtocolUrlFromArgv(process.argv); @@ -459,6 +465,7 @@ app.on('ready', async () => { // Quit the app once all windows are closed app.on('before-quit', () => { + closeAllWatchers(); // Release single instance lock to allow other instances to take over if (useSingleInstance && gotTheLock) { app.releaseSingleInstanceLock();