From c3788124d0f26bbb6ed610e711002afd3a68b3a3 Mon Sep 17 00:00:00 2001 From: sikbrad <58461081+sikbrad@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:58:02 +0900 Subject: [PATCH 1/2] fix: ensure server process tree is fully terminated on macOS/Linux Previously, the Electron app only sent SIGTERM to the server process on Unix systems, which could leave child processes running and cause port conflicts when restarting the app. This change uses pkill to terminate all child processes and SIGKILL for reliable termination. Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/main.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts index 8930d664f..b5ae8fb1a 100644 --- a/apps/ui/src/main.ts +++ b/apps/ui/src/main.ts @@ -774,7 +774,17 @@ app.on('window-all-closed', () => { logger.error('Failed to kill server process:', (error as Error).message); } } else { - serverProcess.kill('SIGTERM'); + // Unix/Linux: kill entire process tree + try { + execSync(`pkill -P ${serverProcess.pid}`, { stdio: 'ignore' }); + } catch { + // pkill returns non-zero if no processes found, ignore + } + try { + process.kill(serverProcess.pid, 'SIGKILL'); + } catch { + // Process may already be dead + } } serverProcess = null; } @@ -802,7 +812,20 @@ app.on('before-quit', () => { logger.error('Failed to kill server process:', (error as Error).message); } } else { - serverProcess.kill('SIGTERM'); + // Unix/macOS: kill entire process tree using pkill + // SIGTERM first, then SIGKILL if needed + try { + // Kill all child processes of the server + execSync(`pkill -P ${serverProcess.pid}`, { stdio: 'ignore' }); + } catch { + // pkill returns non-zero if no processes found, ignore + } + try { + // Kill the server process itself with SIGKILL for reliability + process.kill(serverProcess.pid, 'SIGKILL'); + } catch { + // Process may already be dead + } } serverProcess = null; } From dd39b8ee41a7de572a56aeeaf78829f251c827fc Mon Sep 17 00:00:00 2001 From: sikbrad <58461081+sikbrad@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:04:03 +0900 Subject: [PATCH 2/2] refactor: extract server cleanup logic into helper function Address PR review feedback: - Extract duplicate process termination logic into cleanupServerProcess() - Fix misleading comment about SIGTERM (code only uses SIGKILL) - Consolidate static server cleanup into the same helper Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/main.ts | 69 +++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts index b5ae8fb1a..5f4964cb0 100644 --- a/apps/ui/src/main.ts +++ b/apps/ui/src/main.ts @@ -761,67 +761,31 @@ app.whenReady().then(async () => { }); }); -app.on('window-all-closed', () => { - // On macOS, keep the app and servers running when all windows are closed - // (standard macOS behavior). On other platforms, stop servers and quit. - if (process.platform !== 'darwin') { - if (serverProcess && serverProcess.pid) { - logger.info('All windows closed, stopping server...'); - if (process.platform === 'win32') { - try { - execSync(`taskkill /f /t /pid ${serverProcess.pid}`, { stdio: 'ignore' }); - } catch (error) { - logger.error('Failed to kill server process:', (error as Error).message); - } - } else { - // Unix/Linux: kill entire process tree - try { - execSync(`pkill -P ${serverProcess.pid}`, { stdio: 'ignore' }); - } catch { - // pkill returns non-zero if no processes found, ignore - } - try { - process.kill(serverProcess.pid, 'SIGKILL'); - } catch { - // Process may already be dead - } - } - serverProcess = null; - } - - if (staticServer) { - logger.info('Stopping static server...'); - staticServer.close(); - staticServer = null; - } - - app.quit(); - } -}); - -app.on('before-quit', () => { +/** + * Clean up server and static server processes. + * Kills entire process tree to prevent orphaned processes and port conflicts on restart. + */ +function cleanupServerProcess(reason: 'window-closed' | 'quitting'): void { if (serverProcess && serverProcess.pid) { - logger.info('Stopping server...'); + const logMessage = + reason === 'window-closed' ? 'All windows closed, stopping server...' : 'Stopping server...'; + logger.info(logMessage); + if (process.platform === 'win32') { try { // Windows: use taskkill with /t to kill entire process tree - // This prevents orphaned node processes when closing the app - // Using execSync to ensure process is killed before app exits execSync(`taskkill /f /t /pid ${serverProcess.pid}`, { stdio: 'ignore' }); } catch (error) { logger.error('Failed to kill server process:', (error as Error).message); } } else { - // Unix/macOS: kill entire process tree using pkill - // SIGTERM first, then SIGKILL if needed + // Unix/macOS: kill child processes with pkill, then SIGKILL the server try { - // Kill all child processes of the server execSync(`pkill -P ${serverProcess.pid}`, { stdio: 'ignore' }); } catch { // pkill returns non-zero if no processes found, ignore } try { - // Kill the server process itself with SIGKILL for reliability process.kill(serverProcess.pid, 'SIGKILL'); } catch { // Process may already be dead @@ -835,6 +799,19 @@ app.on('before-quit', () => { staticServer.close(); staticServer = null; } +} + +app.on('window-all-closed', () => { + // On macOS, keep the app and servers running when all windows are closed + // (standard macOS behavior). On other platforms, stop servers and quit. + if (process.platform !== 'darwin') { + cleanupServerProcess('window-closed'); + app.quit(); + } +}); + +app.on('before-quit', () => { + cleanupServerProcess('quitting'); }); // ============================================