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

View file

@ -3,8 +3,8 @@ import {
getMcpConfigsBasePath,
getMcpServerBasePath,
} from '@main/utils/pathDecoder';
import { execCli } from '@main/utils/childProcess';
import { createLogger } from '@shared/utils/logger';
import { execFile } from 'child_process';
import { randomUUID } from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
@ -185,17 +185,11 @@ async function resolveNodePath(options?: McpLaunchSpecResolveOptions): Promise<s
try {
emitProgress(options, 'node-runtime', 'Resolving Node.js runtime for MCP server...');
const resolved = await new Promise<string>((resolve, reject) => {
execFile(
'node',
['-e', 'process.stdout.write(process.execPath)'],
{
encoding: 'utf-8',
timeout: 5000,
},
(err, stdout) => (err ? reject(err) : resolve(stdout.trim()))
);
const { stdout } = await execCli('node', ['-e', 'process.stdout.write(process.execPath)'], {
encoding: 'utf-8',
timeout: 5000,
});
const resolved = stdout.trim();
if (resolved) {
_resolvedNodePath = resolved;
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(
'git',
args,
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024 },
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 1024 * 1024, windowsHide: true },
(error, stdout, stderr) => {
if (error) {
const message = String(stderr || error.message || 'git command failed').trim();

View file

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

View file

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

View file

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

View file

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

View file

@ -21,8 +21,8 @@ export function killProcessByPid(pid: number): void {
'System32',
'taskkill.exe'
);
execFile(taskkillPath, ['/T', '/F', '/PID', String(pid)], () => {
// Best-effort ignore errors (process may have already exited)
execFile(taskkillPath, ['/T', '/F', '/PID', String(pid)], { windowsHide: true }, () => {
// Best-effort - ignore errors (process may have already exited)
});
} catch {
// 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, {
env: process.env,
stdio: ['ignore', 'pipe', 'ignore'],
windowsHide: true,
});
const chunks: Buffer[] = [];
let settled = false;

View file

@ -9,19 +9,7 @@ const hoisted = vi.hoisted(() => ({
isPackaged: false,
version: '9.9.9-test',
},
execFileMock: vi.fn(
(
_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', '');
}
),
execCliMock: vi.fn(async () => ({ stdout: '/mock/node', stderr: '' })),
}));
let mockHomeDir = '';
@ -29,11 +17,11 @@ type ModuleLoad = (request: string, parent: NodeModule | undefined, isMain: bool
const moduleInternal = Module as unknown as { _load: ModuleLoad };
const originalModuleLoad = moduleInternal._load;
vi.mock('child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('child_process')>();
vi.mock('@main/utils/childProcess', async (importOriginal) => {
const actual = await importOriginal<typeof import('@main/utils/childProcess')>();
return {
...actual,
execFile: hoisted.execFileMock,
execCli: hoisted.execCliMock,
};
});
@ -189,7 +177,7 @@ describe('TeamMcpConfigBuilder', () => {
setAppDataBasePath(tempAppData);
setPackagedMode(false);
setResourcesPath(undefined);
hoisted.execFileMock.mockClear();
hoisted.execCliMock.mockClear();
});
afterEach(() => {
@ -283,6 +271,21 @@ describe('TeamMcpConfigBuilder', () => {
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 () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-project-'));

View file

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