fix windows

This commit is contained in:
iliya 2026-02-27 20:10:54 +02:00
parent 228e8868ed
commit 697f5bb896
3 changed files with 50 additions and 10 deletions

View file

@ -17,7 +17,7 @@
* - Human-readable error messages per phase
*/
import { execCli, spawnCli } from '@main/utils/childProcess';
import { execCli, killProcessTree, spawnCli } from '@main/utils/childProcess';
import { getHomeDir } from '@main/utils/pathDecoder';
import { getErrorMessage } from '@shared/utils/errorHandling';
import { createLogger } from '@shared/utils/logger';
@ -467,7 +467,7 @@ export class CliInstallerService {
});
const timeout = setTimeout(() => {
child.kill();
killProcessTree(child);
reject(
new Error(
`Timed out after ${INSTALL_TIMEOUT_MS / 1000}s. ` +

View file

@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign -- ProvisioningRun object is intentionally mutated as a state tracker throughout the provisioning lifecycle */
import { ConfigManager } from '@main/services/infrastructure/ConfigManager';
import { spawnCli } from '@main/utils/childProcess';
import { killProcessTree, spawnCli } from '@main/utils/childProcess';
import {
encodePath,
extractBaseDir,
@ -1035,7 +1035,7 @@ export class TeamProvisioningService {
void (async () => {
const readyOnTimeout = await this.tryCompleteAfterTimeout(run);
run.child?.stdin?.end();
run.child?.kill();
killProcessTree(run.child);
if (readyOnTimeout) {
return; // cleanupRun already called inside tryCompleteAfterTimeout
}
@ -1344,7 +1344,7 @@ export class TeamProvisioningService {
void (async () => {
const readyOnTimeout = await this.tryCompleteAfterTimeout(run);
run.child?.stdin?.end();
run.child?.kill();
killProcessTree(run.child);
if (readyOnTimeout) {
return;
}
@ -1395,7 +1395,7 @@ export class TeamProvisioningService {
run.cancelRequested = true;
run.processKilled = true;
run.child?.stdin?.end();
run.child?.kill();
killProcessTree(run.child);
const progress = updateProgress(run, 'cancelled', 'Provisioning cancelled by user');
run.onProgress(progress);
this.cleanupRun(run);
@ -1824,7 +1824,7 @@ export class TeamProvisioningService {
run.processKilled = true;
run.cancelRequested = true;
run.child?.stdin?.end();
run.child?.kill();
killProcessTree(run.child);
const progress = updateProgress(run, 'disconnected', 'Team stopped by user');
run.onProgress(progress);
this.cleanupRun(run);
@ -1966,7 +1966,7 @@ export class TeamProvisioningService {
// Kill the process on provisioning error
run.processKilled = true;
run.child?.stdin?.end();
run.child?.kill();
killProcessTree(run.child);
this.cleanupRun(run);
} else if (run.provisioningComplete) {
// Post-provisioning error: process alive, waiting for input
@ -2028,7 +2028,7 @@ export class TeamProvisioningService {
run.onProgress(progress);
run.processKilled = true;
run.child?.stdin?.end();
run.child?.kill();
killProcessTree(run.child);
this.cleanupRun(run);
return;
}
@ -3137,7 +3137,7 @@ export class TeamProvisioningService {
const stderrChunks: Buffer[] = [];
const timeoutHandle = setTimeout(() => {
child.kill();
killProcessTree(child);
reject(new Error(`Timeout running: claude ${args.join(' ')}`));
}, timeoutMs);

View file

@ -1,4 +1,5 @@
import {
type ChildProcess,
exec,
execFile,
type ExecFileOptions,
@ -6,6 +7,7 @@ import {
spawn,
type SpawnOptions,
} from 'child_process';
import path from 'path';
/**
* Promise wrapper for execFile that always returns { stdout, stderr }.
@ -156,3 +158,41 @@ export function spawnCli(
throw err;
}
}
/**
* Kill a child process and its entire process tree.
*
* On Windows with `shell: true`, `child.kill()` only kills the intermediate
* `cmd.exe` shell, leaving the actual process (e.g. `claude.cmd`) orphaned.
* `taskkill /T /F /PID` recursively kills the entire process tree.
*
* On macOS/Linux, processes are killed directly (no shell wrapper), so
* the standard `child.kill(signal)` works correctly.
*/
export function killProcessTree(
child: ChildProcess | null | undefined,
signal?: NodeJS.Signals
): void {
if (!child?.pid) {
// Process is null, never started, or already exited
return;
}
if (process.platform === 'win32') {
try {
const taskkillPath = path.join(
process.env.SystemRoot ?? 'C:\\Windows',
'System32',
'taskkill.exe'
);
execFile(taskkillPath, ['/T', '/F', '/PID', String(child.pid)], () => {
// Best-effort — ignore errors (process may have already exited)
});
return;
} catch {
// taskkill failed, fall through to standard kill
}
}
child.kill(signal);
}