From 2e759108c1dd85680a7ec1c9865c5facb1b41799 Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 13 Apr 2026 23:35:23 -0500 Subject: [PATCH] fix(electron): improve child process termination handling during app quit to ensure graceful shutdown --- electron/main-legacy.js | 48 +++++++++++++++++------ electron/main.js | 48 +++++++++++++++++------ meshchatx/meshchat.py | 9 +++++ meshchatx/src/backend/identity_context.py | 14 +++---- 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/electron/main-legacy.js b/electron/main-legacy.js index af61fe2..ac55abb 100644 --- a/electron/main-legacy.js +++ b/electron/main-legacy.js @@ -500,21 +500,47 @@ app.whenReady().then(async () => { }); function quit() { - // kill python process - if (exeChildProcess) { - exeChildProcess.kill("SIGKILL"); + if (!exeChildProcess) { + app.quit(); + return; } - - // quit electron app - app.quit(); + if (exeChildProcess.exitCode !== null || exeChildProcess.signalCode !== null) { + return; + } + try { + exeChildProcess.kill("SIGTERM"); + } catch (e) { + log(e); + try { + exeChildProcess.kill("SIGKILL"); + } catch (e2) { + log(e2); + } + app.quit(); + return; + } + const timeoutMs = 5000; + const timeout = setTimeout(() => { + try { + if ( + exeChildProcess && + exeChildProcess.exitCode === null && + exeChildProcess.signalCode === null + ) { + exeChildProcess.kill("SIGKILL"); + } + } catch (e) { + log(e); + } + app.quit(); + }, timeoutMs); + exeChildProcess.once("exit", () => { + clearTimeout(timeout); + app.quit(); + }); } // quit electron if all windows are closed app.on("window-all-closed", () => { quit(); }); - -// make sure child process is killed if app is quiting -app.on("quit", () => { - quit(); -}); diff --git a/electron/main.js b/electron/main.js index 70e21aa..144b86c 100644 --- a/electron/main.js +++ b/electron/main.js @@ -752,21 +752,47 @@ app.whenReady().then(async () => { }); function quit() { - // kill python process - if (exeChildProcess) { - exeChildProcess.kill("SIGKILL"); + if (!exeChildProcess) { + app.quit(); + return; } - - // quit electron app - app.quit(); + if (exeChildProcess.exitCode !== null || exeChildProcess.signalCode !== null) { + return; + } + try { + exeChildProcess.kill("SIGTERM"); + } catch (e) { + log(e); + try { + exeChildProcess.kill("SIGKILL"); + } catch (e2) { + log(e2); + } + app.quit(); + return; + } + const timeoutMs = 5000; + const timeout = setTimeout(() => { + try { + if ( + exeChildProcess && + exeChildProcess.exitCode === null && + exeChildProcess.signalCode === null + ) { + exeChildProcess.kill("SIGKILL"); + } + } catch (e) { + log(e); + } + app.quit(); + }, timeoutMs); + exeChildProcess.once("exit", () => { + clearTimeout(timeout); + app.quit(); + }); } // quit electron if all windows are closed app.on("window-all-closed", () => { quit(); }); - -// make sure child process is killed if app is quiting -app.on("quit", () => { - quit(); -}); diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py index 28985c8..a4299b5 100644 --- a/meshchatx/meshchat.py +++ b/meshchatx/meshchat.py @@ -2296,6 +2296,15 @@ class ReticulumMeshChat: # web server has shutdown, likely ctrl+c, but if we don't do the following, the script never exits async def shutdown(self, app): + for identity_hash in list(self.contexts.keys()): + ctx = self.contexts.get(identity_hash) + if ctx is None: + continue + bh = getattr(ctx, "bot_handler", None) + if bh is not None: + with contextlib.suppress(Exception): + bh.stop_all() + if hasattr(self, "page_node_manager"): self.page_node_manager.teardown() diff --git a/meshchatx/src/backend/identity_context.py b/meshchatx/src/backend/identity_context.py index c4dccfa..861d919 100644 --- a/meshchatx/src/backend/identity_context.py +++ b/meshchatx/src/backend/identity_context.py @@ -475,6 +475,13 @@ class IdentityContext: self.auto_propagation_manager.stop() self.auto_propagation_manager = None + if self.bot_handler: + try: + self.bot_handler.stop_all() + except Exception as e: + print(f"Error while stopping bots for {self.identity_hash}: {e}") + self.bot_handler = None + # 1. Deregister announce handlers for handler in self.announce_handlers: with contextlib.suppress(Exception): @@ -586,13 +593,6 @@ class IdentityContext: if self.nomadnet_manager: self.nomadnet_manager = None - if self.bot_handler: - try: - self.bot_handler.stop_all() - except Exception as e: - print(f"Error while stopping bots for {self.identity_hash}: {e}") - self.bot_handler = None - if self.database: try: if not getattr(self.app, "emergency", False):