From 1d2cbf8f265cc918ccfcda8789a905bfdf257f57 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:49:16 +0800 Subject: [PATCH] Revert "fix(gateway): terminate owned process before retry to prevent port conflict on Windows" (#755) --- electron/gateway/manager.ts | 27 ++------------------ electron/gateway/startup-orchestrator.ts | 32 +----------------------- electron/gateway/supervisor.ts | 14 +++-------- 3 files changed, 6 insertions(+), 67 deletions(-) diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index 415274a6a..7d23cdd77 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -259,8 +259,8 @@ export class GatewayManager extends EventEmitter { this.startHealthCheck(); }, - waitForPortFree: async (port, signal) => { - await waitForPortFree(port, 30000, signal); + waitForPortFree: async (port) => { + await waitForPortFree(port); }, startProcess: async () => { await this.startProcess(); @@ -282,29 +282,6 @@ export class GatewayManager extends EventEmitter { delay: async (ms) => { await new Promise((resolve) => setTimeout(resolve, ms)); }, - terminateOwnedProcess: async () => { - if (this.process && this.ownsProcess) { - logger.info('Terminating owned Gateway process before retry...'); - const proc = this.process; - const pid = proc.pid; - await terminateOwnedGatewayProcess(proc).catch(() => {}); - // Only clear the handle if the process has actually exited. - // terminateOwnedGatewayProcess may resolve via its timeout path - // while the child is still alive; in that case keep the reference - // so subsequent retries and stop() can still target it. - if (pid != null) { - try { - process.kill(pid, 0); - // Still alive — keep this.process so later cleanup can reach it - } catch { - // Process is gone — safe to clear the handle - this.process = null; - } - } else { - this.process = null; - } - } - }, }); } catch (error) { if (error instanceof LifecycleSupersededError) { diff --git a/electron/gateway/startup-orchestrator.ts b/electron/gateway/startup-orchestrator.ts index 102618554..ae2c3ee13 100644 --- a/electron/gateway/startup-orchestrator.ts +++ b/electron/gateway/startup-orchestrator.ts @@ -18,15 +18,13 @@ type StartupHooks = { findExistingGateway: (port: number) => Promise; connect: (port: number, externalToken?: string) => Promise; onConnectedToExistingGateway: () => void; - waitForPortFree: (port: number, signal?: AbortSignal) => Promise; + waitForPortFree: (port: number) => Promise; startProcess: () => Promise; waitForReady: (port: number) => Promise; onConnectedToManagedGateway: () => void; runDoctorRepair: () => Promise; onDoctorRepairSuccess: () => void; delay: (ms: number) => Promise; - /** Called before a retry to terminate the previously spawned process if still running */ - terminateOwnedProcess?: () => Promise; }; export async function runGatewayStartupSequence(hooks: StartupHooks): Promise { @@ -99,34 +97,6 @@ export async function runGatewayStartupSequence(hooks: StartupHooks): Promise { - logger.warn('Failed to terminate owned process before retry:', err); - }); - } - hooks.assertLifecycle('start/retry-pre-port-wait'); - // Wait for port to become free before retrying (handles lingering processes). - // Use a short-polling AbortController so that a superseding stop()/restart() - // can cancel the wait promptly instead of blocking for the full 30s timeout. - if (hooks.shouldWaitForPortFree) { - const abortController = new AbortController(); - // Poll lifecycle every 500ms and abort the port-wait if superseded - const lifecyclePollInterval = setInterval(() => { - try { - hooks.assertLifecycle('start/retry-port-wait-poll'); - } catch { - abortController.abort(); - } - }, 500); - try { - await hooks.waitForPortFree(hooks.port, abortController.signal); - } finally { - clearInterval(lifecyclePollInterval); - } - } - hooks.assertLifecycle('start/retry-post-port-wait'); continue; } diff --git a/electron/gateway/supervisor.ts b/electron/gateway/supervisor.ts index 272380348..36122fbb2 100644 --- a/electron/gateway/supervisor.ts +++ b/electron/gateway/supervisor.ts @@ -34,12 +34,11 @@ export async function terminateOwnedGatewayProcess(child: Electron.UtilityProces // Register a single exit listener before any kill attempt to avoid // the race where exit fires between two separate `once('exit')` calls. - const exitListener = () => { + child.once('exit', () => { exited = true; clearTimeout(timeout); resolve(); - }; - child.once('exit', exitListener); + }); const pid = child.pid; logger.info(`Sending kill to Gateway process (pid=${pid ?? 'unknown'})`); @@ -73,8 +72,6 @@ export async function terminateOwnedGatewayProcess(child: Electron.UtilityProces } } } - // Clean up the exit listener on timeout to prevent listener leaks - child.off('exit', exitListener); resolve(); }, 5000); }); @@ -128,18 +125,13 @@ export async function unloadLaunchctlGatewayService(): Promise { } } -export async function waitForPortFree(port: number, timeoutMs = 30000, signal?: AbortSignal): Promise { +export async function waitForPortFree(port: number, timeoutMs = 30000): Promise { const net = await import('net'); const start = Date.now(); const pollInterval = 500; let logged = false; while (Date.now() - start < timeoutMs) { - if (signal?.aborted) { - logger.debug(`waitForPortFree: aborted while waiting for port ${port}`); - return; - } - const available = await new Promise((resolve) => { const server = net.createServer(); server.once('error', () => resolve(false));