fix(gateway): prevent reconnect race and globally hide windows subprocess console

This commit is contained in:
Haze
2026-03-02 19:40:16 +08:00
parent dfdbde374d
commit 3596464803
7 changed files with 15 additions and 25 deletions

View File

@@ -106,6 +106,7 @@ export class ClawHubService {
...env, ...env,
CLAWHUB_WORKDIR: this.workDir, CLAWHUB_WORKDIR: this.workDir,
}, },
windowsHide: true,
}); });
let stdout = ''; let stdout = '';

View File

@@ -39,7 +39,6 @@ import {
getReconnectSkipReason, getReconnectSkipReason,
isLifecycleSuperseded, isLifecycleSuperseded,
nextLifecycleEpoch, nextLifecycleEpoch,
shouldHideConsoleWindow,
} from './process-policy'; } from './process-policy';
/** /**
@@ -709,7 +708,7 @@ export class GatewayManager extends EventEmitter {
const { stdout } = await new Promise<{ stdout: string }>((resolve, reject) => { const { stdout } = await new Promise<{ stdout: string }>((resolve, reject) => {
import('child_process').then(cp => { 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: '' }); if (err) resolve({ stdout: '' });
else resolve({ stdout }); else resolve({ stdout });
}); });
@@ -739,7 +738,7 @@ export class GatewayManager extends EventEmitter {
import('child_process').then(cp => { import('child_process').then(cp => {
cp.exec( cp.exec(
`taskkill /PID ${pid} /T /F`, `taskkill /PID ${pid} /T /F`,
{ timeout: 5000, windowsHide: shouldHideConsoleWindow() }, { timeout: 5000, windowsHide: true },
() => { } () => { }
); );
}).catch(() => { }); }).catch(() => { });
@@ -844,7 +843,7 @@ export class GatewayManager extends EventEmitter {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
detached: false, detached: false,
shell: false, shell: false,
windowsHide: shouldHideConsoleWindow(), windowsHide: true,
env: spawnEnv, env: spawnEnv,
}); });
@@ -1098,7 +1097,7 @@ export class GatewayManager extends EventEmitter {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
detached: false, detached: false,
shell: useShell, shell: useShell,
windowsHide: shouldHideConsoleWindow(), windowsHide: true,
env: spawnEnv, env: spawnEnv,
}); });
const child = this.process; const child = this.process;

View File

@@ -1,7 +1,3 @@
export function shouldHideConsoleWindow(platform: NodeJS.Platform = process.platform): boolean {
return platform === 'win32';
}
export function nextLifecycleEpoch(currentEpoch: number): number { export function nextLifecycleEpoch(currentEpoch: number): number {
return currentEpoch + 1; return currentEpoch + 1;
} }

View File

@@ -526,6 +526,7 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
cwd: openclawPath, cwd: openclawPath,
encoding: 'utf-8', encoding: 'utf-8',
timeout: 30000, timeout: 30000,
windowsHide: true,
}, },
(err, stdout) => { (err, stdout) => {
if (err) reject(err); if (err) reject(err);

View File

@@ -269,6 +269,7 @@ export function generateCompletionCache(): void {
}, },
stdio: 'ignore', stdio: 'ignore',
detached: false, detached: false,
windowsHide: true,
}); });
child.on('close', (code) => { child.on('close', (code) => {
@@ -305,7 +306,8 @@ export function installCompletionToProfile(): void {
}, },
stdio: 'ignore', stdio: 'ignore',
detached: false, detached: false,
}, windowsHide: true,
}
); );
child.on('close', (code) => { child.on('close', (code) => {

View File

@@ -53,7 +53,7 @@ function resolveUvBin(): { bin: string; source: 'bundled' | 'path' | 'bundled-fa
function findUvInPathSync(): boolean { function findUvInPathSync(): boolean {
try { try {
const cmd = process.platform === 'win32' ? 'where.exe uv' : 'which uv'; 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; return true;
} catch { } catch {
return false; return false;
@@ -95,6 +95,7 @@ export async function isPythonReady(): Promise<boolean> {
try { try {
const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], { const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], {
shell: useShell, shell: useShell,
windowsHide: true,
}); });
child.on('close', (code) => resolve(code === 0)); child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false)); child.on('error', () => resolve(false));
@@ -121,6 +122,7 @@ async function runPythonInstall(
const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'install', '3.12'], { const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'install', '3.12'], {
shell: useShell, shell: useShell,
env, env,
windowsHide: true,
}); });
child.stdout?.on('data', (data) => { child.stdout?.on('data', (data) => {
@@ -210,6 +212,7 @@ export async function setupManagedPython(): Promise<void> {
const child = spawn(verifyShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], { const child = spawn(verifyShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], {
shell: verifyShell, shell: verifyShell,
env: { ...process.env, ...uvEnv }, env: { ...process.env, ...uvEnv },
windowsHide: true,
}); });
let output = ''; let output = '';
child.stdout?.on('data', (data) => { output += data; }); child.stdout?.on('data', (data) => { output += data; });

View File

@@ -3,21 +3,9 @@ import {
getReconnectSkipReason, getReconnectSkipReason,
isLifecycleSuperseded, isLifecycleSuperseded,
nextLifecycleEpoch, nextLifecycleEpoch,
shouldHideConsoleWindow,
} from '@electron/gateway/process-policy'; } from '@electron/gateway/process-policy';
describe('gateway process policy helpers', () => { 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', () => { describe('lifecycle epoch helpers', () => {
it('increments lifecycle epoch by one', () => { it('increments lifecycle epoch by one', () => {
expect(nextLifecycleEpoch(0)).toBe(1); expect(nextLifecycleEpoch(0)).toBe(1);