fix windows
This commit is contained in:
parent
228e8868ed
commit
697f5bb896
3 changed files with 50 additions and 10 deletions
|
|
@ -17,7 +17,7 @@
|
||||||
* - Human-readable error messages per phase
|
* - 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 { getHomeDir } from '@main/utils/pathDecoder';
|
||||||
import { getErrorMessage } from '@shared/utils/errorHandling';
|
import { getErrorMessage } from '@shared/utils/errorHandling';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
|
@ -467,7 +467,7 @@ export class CliInstallerService {
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
child.kill();
|
killProcessTree(child);
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`Timed out after ${INSTALL_TIMEOUT_MS / 1000}s. ` +
|
`Timed out after ${INSTALL_TIMEOUT_MS / 1000}s. ` +
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable no-param-reassign -- ProvisioningRun object is intentionally mutated as a state tracker throughout the provisioning lifecycle */
|
/* 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 { ConfigManager } from '@main/services/infrastructure/ConfigManager';
|
||||||
import { spawnCli } from '@main/utils/childProcess';
|
import { killProcessTree, spawnCli } from '@main/utils/childProcess';
|
||||||
import {
|
import {
|
||||||
encodePath,
|
encodePath,
|
||||||
extractBaseDir,
|
extractBaseDir,
|
||||||
|
|
@ -1035,7 +1035,7 @@ export class TeamProvisioningService {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const readyOnTimeout = await this.tryCompleteAfterTimeout(run);
|
const readyOnTimeout = await this.tryCompleteAfterTimeout(run);
|
||||||
run.child?.stdin?.end();
|
run.child?.stdin?.end();
|
||||||
run.child?.kill();
|
killProcessTree(run.child);
|
||||||
if (readyOnTimeout) {
|
if (readyOnTimeout) {
|
||||||
return; // cleanupRun already called inside tryCompleteAfterTimeout
|
return; // cleanupRun already called inside tryCompleteAfterTimeout
|
||||||
}
|
}
|
||||||
|
|
@ -1344,7 +1344,7 @@ export class TeamProvisioningService {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const readyOnTimeout = await this.tryCompleteAfterTimeout(run);
|
const readyOnTimeout = await this.tryCompleteAfterTimeout(run);
|
||||||
run.child?.stdin?.end();
|
run.child?.stdin?.end();
|
||||||
run.child?.kill();
|
killProcessTree(run.child);
|
||||||
if (readyOnTimeout) {
|
if (readyOnTimeout) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1395,7 +1395,7 @@ export class TeamProvisioningService {
|
||||||
run.cancelRequested = true;
|
run.cancelRequested = true;
|
||||||
run.processKilled = true;
|
run.processKilled = true;
|
||||||
run.child?.stdin?.end();
|
run.child?.stdin?.end();
|
||||||
run.child?.kill();
|
killProcessTree(run.child);
|
||||||
const progress = updateProgress(run, 'cancelled', 'Provisioning cancelled by user');
|
const progress = updateProgress(run, 'cancelled', 'Provisioning cancelled by user');
|
||||||
run.onProgress(progress);
|
run.onProgress(progress);
|
||||||
this.cleanupRun(run);
|
this.cleanupRun(run);
|
||||||
|
|
@ -1824,7 +1824,7 @@ export class TeamProvisioningService {
|
||||||
run.processKilled = true;
|
run.processKilled = true;
|
||||||
run.cancelRequested = true;
|
run.cancelRequested = true;
|
||||||
run.child?.stdin?.end();
|
run.child?.stdin?.end();
|
||||||
run.child?.kill();
|
killProcessTree(run.child);
|
||||||
const progress = updateProgress(run, 'disconnected', 'Team stopped by user');
|
const progress = updateProgress(run, 'disconnected', 'Team stopped by user');
|
||||||
run.onProgress(progress);
|
run.onProgress(progress);
|
||||||
this.cleanupRun(run);
|
this.cleanupRun(run);
|
||||||
|
|
@ -1966,7 +1966,7 @@ export class TeamProvisioningService {
|
||||||
// Kill the process on provisioning error
|
// Kill the process on provisioning error
|
||||||
run.processKilled = true;
|
run.processKilled = true;
|
||||||
run.child?.stdin?.end();
|
run.child?.stdin?.end();
|
||||||
run.child?.kill();
|
killProcessTree(run.child);
|
||||||
this.cleanupRun(run);
|
this.cleanupRun(run);
|
||||||
} else if (run.provisioningComplete) {
|
} else if (run.provisioningComplete) {
|
||||||
// Post-provisioning error: process alive, waiting for input
|
// Post-provisioning error: process alive, waiting for input
|
||||||
|
|
@ -2028,7 +2028,7 @@ export class TeamProvisioningService {
|
||||||
run.onProgress(progress);
|
run.onProgress(progress);
|
||||||
run.processKilled = true;
|
run.processKilled = true;
|
||||||
run.child?.stdin?.end();
|
run.child?.stdin?.end();
|
||||||
run.child?.kill();
|
killProcessTree(run.child);
|
||||||
this.cleanupRun(run);
|
this.cleanupRun(run);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -3137,7 +3137,7 @@ export class TeamProvisioningService {
|
||||||
const stderrChunks: Buffer[] = [];
|
const stderrChunks: Buffer[] = [];
|
||||||
|
|
||||||
const timeoutHandle = setTimeout(() => {
|
const timeoutHandle = setTimeout(() => {
|
||||||
child.kill();
|
killProcessTree(child);
|
||||||
reject(new Error(`Timeout running: claude ${args.join(' ')}`));
|
reject(new Error(`Timeout running: claude ${args.join(' ')}`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
type ChildProcess,
|
||||||
exec,
|
exec,
|
||||||
execFile,
|
execFile,
|
||||||
type ExecFileOptions,
|
type ExecFileOptions,
|
||||||
|
|
@ -6,6 +7,7 @@ import {
|
||||||
spawn,
|
spawn,
|
||||||
type SpawnOptions,
|
type SpawnOptions,
|
||||||
} from 'child_process';
|
} from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promise wrapper for execFile that always returns { stdout, stderr }.
|
* Promise wrapper for execFile that always returns { stdout, stderr }.
|
||||||
|
|
@ -156,3 +158,41 @@ export function spawnCli(
|
||||||
throw err;
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue