From 3596464803c767d5fba105f19ea0e0ce058910e9 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Mon, 2 Mar 2026 19:40:16 +0800 Subject: [PATCH] fix(gateway): prevent reconnect race and globally hide windows subprocess console --- electron/gateway/clawhub.ts | 1 + electron/gateway/manager.ts | 9 ++++----- electron/gateway/process-policy.ts | 4 ---- electron/utils/channel-config.ts | 1 + electron/utils/openclaw-cli.ts | 4 +++- electron/utils/uv-setup.ts | 9 ++++++--- tests/unit/gateway-process-policy.test.ts | 12 ------------ 7 files changed, 15 insertions(+), 25 deletions(-) diff --git a/electron/gateway/clawhub.ts b/electron/gateway/clawhub.ts index 1504557..25cb268 100644 --- a/electron/gateway/clawhub.ts +++ b/electron/gateway/clawhub.ts @@ -106,6 +106,7 @@ export class ClawHubService { ...env, CLAWHUB_WORKDIR: this.workDir, }, + windowsHide: true, }); let stdout = ''; diff --git a/electron/gateway/manager.ts b/electron/gateway/manager.ts index f1e3d69..cf96d3c 100644 --- a/electron/gateway/manager.ts +++ b/electron/gateway/manager.ts @@ -39,7 +39,6 @@ import { getReconnectSkipReason, isLifecycleSuperseded, nextLifecycleEpoch, - shouldHideConsoleWindow, } from './process-policy'; /** @@ -709,7 +708,7 @@ export class GatewayManager extends EventEmitter { const { stdout } = await new Promise<{ stdout: string }>((resolve, reject) => { import('child_process').then(cp => { - cp.exec(cmd, { timeout: 5000, windowsHide: shouldHideConsoleWindow() }, (err, stdout) => { + cp.exec(cmd, { timeout: 5000, windowsHide: true }, (err, stdout) => { if (err) resolve({ stdout: '' }); else resolve({ stdout }); }); @@ -739,7 +738,7 @@ export class GatewayManager extends EventEmitter { import('child_process').then(cp => { cp.exec( `taskkill /PID ${pid} /T /F`, - { timeout: 5000, windowsHide: shouldHideConsoleWindow() }, + { timeout: 5000, windowsHide: true }, () => { } ); }).catch(() => { }); @@ -844,7 +843,7 @@ export class GatewayManager extends EventEmitter { stdio: ['ignore', 'pipe', 'pipe'], detached: false, shell: false, - windowsHide: shouldHideConsoleWindow(), + windowsHide: true, env: spawnEnv, }); @@ -1098,7 +1097,7 @@ export class GatewayManager extends EventEmitter { stdio: ['ignore', 'pipe', 'pipe'], detached: false, shell: useShell, - windowsHide: shouldHideConsoleWindow(), + windowsHide: true, env: spawnEnv, }); const child = this.process; diff --git a/electron/gateway/process-policy.ts b/electron/gateway/process-policy.ts index b88df68..ea8b9c2 100644 --- a/electron/gateway/process-policy.ts +++ b/electron/gateway/process-policy.ts @@ -1,7 +1,3 @@ -export function shouldHideConsoleWindow(platform: NodeJS.Platform = process.platform): boolean { - return platform === 'win32'; -} - export function nextLifecycleEpoch(currentEpoch: number): number { return currentEpoch + 1; } diff --git a/electron/utils/channel-config.ts b/electron/utils/channel-config.ts index eba0897..1732e41 100644 --- a/electron/utils/channel-config.ts +++ b/electron/utils/channel-config.ts @@ -526,6 +526,7 @@ export async function validateChannelConfig(channelType: string): Promise { if (err) reject(err); diff --git a/electron/utils/openclaw-cli.ts b/electron/utils/openclaw-cli.ts index 9305e1b..9d9e9f5 100644 --- a/electron/utils/openclaw-cli.ts +++ b/electron/utils/openclaw-cli.ts @@ -269,6 +269,7 @@ export function generateCompletionCache(): void { }, stdio: 'ignore', detached: false, + windowsHide: true, }); child.on('close', (code) => { @@ -305,7 +306,8 @@ export function installCompletionToProfile(): void { }, stdio: 'ignore', detached: false, - }, + windowsHide: true, + } ); child.on('close', (code) => { diff --git a/electron/utils/uv-setup.ts b/electron/utils/uv-setup.ts index c16fcd9..9a70a70 100644 --- a/electron/utils/uv-setup.ts +++ b/electron/utils/uv-setup.ts @@ -14,7 +14,7 @@ function getBundledUvPath(): string { const arch = process.arch; const target = `${platform}-${arch}`; const binName = platform === 'win32' ? 'uv.exe' : 'uv'; - + if (app.isPackaged) { return join(process.resourcesPath, 'bin', binName); } else { @@ -53,7 +53,7 @@ function resolveUvBin(): { bin: string; source: 'bundled' | 'path' | 'bundled-fa function findUvInPathSync(): boolean { try { const cmd = process.platform === 'win32' ? 'where.exe uv' : 'which uv'; - execSync(cmd, { stdio: 'ignore', timeout: 5000 }); + execSync(cmd, { stdio: 'ignore', timeout: 5000, windowsHide: true }); return true; } catch { return false; @@ -95,6 +95,7 @@ export async function isPythonReady(): Promise { try { const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], { shell: useShell, + windowsHide: true, }); child.on('close', (code) => resolve(code === 0)); child.on('error', () => resolve(false)); @@ -121,6 +122,7 @@ async function runPythonInstall( const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'install', '3.12'], { shell: useShell, env, + windowsHide: true, }); child.stdout?.on('data', (data) => { @@ -210,12 +212,13 @@ export async function setupManagedPython(): Promise { const child = spawn(verifyShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], { shell: verifyShell, env: { ...process.env, ...uvEnv }, + windowsHide: true, }); let output = ''; child.stdout?.on('data', (data) => { output += data; }); child.on('close', () => resolve(output.trim())); }); - + if (findPath) { logger.info(`Managed Python 3.12 installed at: ${findPath}`); } diff --git a/tests/unit/gateway-process-policy.test.ts b/tests/unit/gateway-process-policy.test.ts index 78da3aa..1b7e001 100644 --- a/tests/unit/gateway-process-policy.test.ts +++ b/tests/unit/gateway-process-policy.test.ts @@ -3,21 +3,9 @@ import { getReconnectSkipReason, isLifecycleSuperseded, nextLifecycleEpoch, - shouldHideConsoleWindow, } from '@electron/gateway/process-policy'; describe('gateway process policy helpers', () => { - describe('shouldHideConsoleWindow', () => { - it('returns true on Windows', () => { - expect(shouldHideConsoleWindow('win32')).toBe(true); - }); - - it('returns false on non-Windows platforms', () => { - expect(shouldHideConsoleWindow('darwin')).toBe(false); - expect(shouldHideConsoleWindow('linux')).toBe(false); - }); - }); - describe('lifecycle epoch helpers', () => { it('increments lifecycle epoch by one', () => { expect(nextLifecycleEpoch(0)).toBe(1);