fix(runtime): hide direct child process windows

This commit is contained in:
iliya 2026-05-16 00:45:58 +03:00
parent 1b086f41b7
commit 876527a51d
11 changed files with 41 additions and 39 deletions

View file

@ -54,6 +54,7 @@ export class GitDiffFallback {
cwd: projectPath, cwd: projectPath,
maxBuffer: GIT_MAX_BUFFER, maxBuffer: GIT_MAX_BUFFER,
timeout: GIT_TIMEOUT, timeout: GIT_TIMEOUT,
windowsHide: true,
}); });
return stdout; return stdout;
} catch { } catch {
@ -75,7 +76,7 @@ export class GitDiffFallback {
const { stdout } = await execFileAsync( const { stdout } = await execFileAsync(
'git', 'git',
['log', '--format=%H', '--before', timestamp, '-1', '--', relativePath], ['log', '--format=%H', '--before', timestamp, '-1', '--', relativePath],
{ cwd: projectPath, timeout: GIT_TIMEOUT } { cwd: projectPath, timeout: GIT_TIMEOUT, windowsHide: true }
); );
return stdout.trim() || null; return stdout.trim() || null;
} catch { } catch {
@ -98,7 +99,7 @@ export class GitDiffFallback {
const { stdout } = await execFileAsync( const { stdout } = await execFileAsync(
'git', 'git',
['diff', fromCommit, toCommit, '--', relativePath], ['diff', fromCommit, toCommit, '--', relativePath],
{ cwd: projectPath, timeout: GIT_TIMEOUT } { cwd: projectPath, timeout: GIT_TIMEOUT, windowsHide: true }
); );
return stdout || null; return stdout || null;
} catch { } catch {
@ -120,7 +121,7 @@ export class GitDiffFallback {
const { stdout } = await execFileAsync( const { stdout } = await execFileAsync(
'git', 'git',
['log', `--max-count=${maxCount}`, '--format=%H|%aI|%s', '--', relativePath], ['log', `--max-count=${maxCount}`, '--format=%H|%aI|%s', '--', relativePath],
{ cwd: projectPath, timeout: GIT_TIMEOUT } { cwd: projectPath, timeout: GIT_TIMEOUT, windowsHide: true }
); );
return stdout return stdout
@ -148,6 +149,7 @@ export class GitDiffFallback {
await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
cwd: projectPath, cwd: projectPath,
timeout: GIT_TIMEOUT, timeout: GIT_TIMEOUT,
windowsHide: true,
}); });
this.gitRepoCache.set(projectPath, true); this.gitRepoCache.set(projectPath, true);
return true; return true;

View file

@ -3,8 +3,8 @@ import {
getMcpConfigsBasePath, getMcpConfigsBasePath,
getMcpServerBasePath, getMcpServerBasePath,
} from '@main/utils/pathDecoder'; } from '@main/utils/pathDecoder';
import { execCli } from '@main/utils/childProcess';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { execFile } from 'child_process';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
@ -185,17 +185,11 @@ async function resolveNodePath(options?: McpLaunchSpecResolveOptions): Promise<s
try { try {
emitProgress(options, 'node-runtime', 'Resolving Node.js runtime for MCP server...'); emitProgress(options, 'node-runtime', 'Resolving Node.js runtime for MCP server...');
const resolved = await new Promise<string>((resolve, reject) => { const { stdout } = await execCli('node', ['-e', 'process.stdout.write(process.execPath)'], {
execFile( encoding: 'utf-8',
'node', timeout: 5000,
['-e', 'process.stdout.write(process.execPath)'],
{
encoding: 'utf-8',
timeout: 5000,
},
(err, stdout) => (err ? reject(err) : resolve(stdout.trim()))
);
}); });
const resolved = stdout.trim();
if (resolved) { if (resolved) {
_resolvedNodePath = resolved; _resolvedNodePath = resolved;
emitProgress(options, 'node-runtime-found', 'Using resolved Node.js runtime...'); emitProgress(options, 'node-runtime-found', 'Using resolved Node.js runtime...');

View file

@ -28,7 +28,7 @@ function execGit(args: string[], cwd: string): Promise<string> {
execFile( execFile(
'git', 'git',
args, args,
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024, windowsHide: true },
(error, stdout, stderr) => { (error, stdout, stderr) => {
if (error) { if (error) {
const message = String(stderr || error.message || 'git command failed').trim(); const message = String(stderr || error.message || 'git command failed').trim();

View file

@ -6426,6 +6426,7 @@ export class TeamProvisioningService {
encoding: 'utf8', encoding: 'utf8',
maxBuffer: 16 * 1024, maxBuffer: 16 * 1024,
timeout: 1000, timeout: 1000,
windowsHide: true,
}, },
(error, stdout) => { (error, stdout) => {
if (error) { if (error) {

View file

@ -11,7 +11,7 @@ function execGit(args: string[], cwd: string): Promise<string> {
execFile( execFile(
'git', 'git',
args, args,
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024, windowsHide: true },
(error, stdout, stderr) => { (error, stdout, stderr) => {
if (error) { if (error) {
const message = String(stderr || error.message || 'git command failed').trim(); const message = String(stderr || error.message || 'git command failed').trim();

View file

@ -278,6 +278,7 @@ function execFileText(
encoding: 'utf8', encoding: 'utf8',
timeout, timeout,
maxBuffer, maxBuffer,
windowsHide: true,
}, },
(error: ExecFileException | null, stdout: string | Buffer) => { (error: ExecFileException | null, stdout: string | Buffer) => {
if (error) { if (error) {

View file

@ -376,8 +376,8 @@ export function killProcessTree(
'System32', 'System32',
'taskkill.exe' 'taskkill.exe'
); );
execFile(taskkillPath, ['/T', '/F', '/PID', String(child.pid)], () => { execFile(taskkillPath, ['/T', '/F', '/PID', String(child.pid)], { windowsHide: true }, () => {
// Best-effort ignore errors (process may have already exited) // Best-effort - ignore errors (process may have already exited)
}); });
return; return;
} catch { } catch {

View file

@ -21,8 +21,8 @@ export function killProcessByPid(pid: number): void {
'System32', 'System32',
'taskkill.exe' 'taskkill.exe'
); );
execFile(taskkillPath, ['/T', '/F', '/PID', String(pid)], () => { execFile(taskkillPath, ['/T', '/F', '/PID', String(pid)], { windowsHide: true }, () => {
// Best-effort ignore errors (process may have already exited) // Best-effort - ignore errors (process may have already exited)
}); });
} catch { } catch {
// taskkill failed to spawn, fall through to process.kill() // taskkill failed to spawn, fall through to process.kill()

View file

@ -87,6 +87,7 @@ async function readShellEnv(shellPath: string, args: string[]): Promise<NodeJS.P
const child = spawn(shellPath, args, { const child = spawn(shellPath, args, {
env: process.env, env: process.env,
stdio: ['ignore', 'pipe', 'ignore'], stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
}); });
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
let settled = false; let settled = false;

View file

@ -9,19 +9,7 @@ const hoisted = vi.hoisted(() => ({
isPackaged: false, isPackaged: false,
version: '9.9.9-test', version: '9.9.9-test',
}, },
execFileMock: vi.fn( execCliMock: vi.fn(async () => ({ stdout: '/mock/node', stderr: '' })),
(
_file: string,
_args: readonly string[],
_options:
| { encoding?: string; timeout?: number }
| ((error: Error | null, stdout: string, stderr: string) => void),
callback?: (error: Error | null, stdout: string, stderr: string) => void
) => {
const cb = typeof _options === 'function' ? _options : callback;
cb?.(null, '/mock/node', '');
}
),
})); }));
let mockHomeDir = ''; let mockHomeDir = '';
@ -29,11 +17,11 @@ type ModuleLoad = (request: string, parent: NodeModule | undefined, isMain: bool
const moduleInternal = Module as unknown as { _load: ModuleLoad }; const moduleInternal = Module as unknown as { _load: ModuleLoad };
const originalModuleLoad = moduleInternal._load; const originalModuleLoad = moduleInternal._load;
vi.mock('child_process', async (importOriginal) => { vi.mock('@main/utils/childProcess', async (importOriginal) => {
const actual = await importOriginal<typeof import('child_process')>(); const actual = await importOriginal<typeof import('@main/utils/childProcess')>();
return { return {
...actual, ...actual,
execFile: hoisted.execFileMock, execCli: hoisted.execCliMock,
}; };
}); });
@ -189,7 +177,7 @@ describe('TeamMcpConfigBuilder', () => {
setAppDataBasePath(tempAppData); setAppDataBasePath(tempAppData);
setPackagedMode(false); setPackagedMode(false);
setResourcesPath(undefined); setResourcesPath(undefined);
hoisted.execFileMock.mockClear(); hoisted.execCliMock.mockClear();
}); });
afterEach(() => { afterEach(() => {
@ -283,6 +271,21 @@ describe('TeamMcpConfigBuilder', () => {
expectNodeEntry(server, builtEntry); expectNodeEntry(server, builtEntry);
}); });
it('uses the shared CLI helper for the Node.js runtime resolver', async () => {
mockBuiltWorkspaceEntryAvailable();
const builder = new TeamMcpConfigBuilder();
const configPath = await builder.writeConfigFile();
createdPaths.push(configPath);
expect(readGeneratedServer(configPath)?.command).toBe('/mock/node');
expect(hoisted.execCliMock).toHaveBeenCalledWith(
'node',
['-e', 'process.stdout.write(process.execPath)'],
expect.objectContaining({ encoding: 'utf-8', timeout: 5000 })
);
});
it('keeps generated team MCP config minimal and does not inline top-level user MCP', async () => { it('keeps generated team MCP config minimal and does not inline top-level user MCP', async () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-')); const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-')); const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-'));

View file

@ -117,13 +117,13 @@ describe('shellEnv', () => {
1, 1,
'/bin/zsh', '/bin/zsh',
['-lic', 'env -0'], ['-lic', 'env -0'],
expect.any(Object) expect.objectContaining({ windowsHide: true })
); );
expect(hoisted.spawn).toHaveBeenNthCalledWith( expect(hoisted.spawn).toHaveBeenNthCalledWith(
2, 2,
'/bin/zsh', '/bin/zsh',
['-ic', 'env -0'], ['-ic', 'env -0'],
expect.any(Object) expect.objectContaining({ windowsHide: true })
); );
}); });