Merge branch 'main' into memory_usage3

This commit is contained in:
Spencer
2026-04-10 15:35:19 -04:00
committed by GitHub
96 changed files with 834726 additions and 624 deletions
+12 -2
View File
@@ -75,7 +75,6 @@ describe('process-utils', () => {
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGKILL');
});
it('should use escalation on Unix if requested', async () => {
vi.mocked(os.platform).mockReturnValue('linux');
const exited = false;
@@ -87,6 +86,11 @@ describe('process-utils', () => {
isExited,
});
// flush microtasks
await new Promise(process.nextTick);
await new Promise(process.nextTick);
await new Promise(process.nextTick);
// First call should be SIGTERM
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGTERM');
@@ -110,6 +114,11 @@ describe('process-utils', () => {
isExited,
});
// flush microtasks
await new Promise(process.nextTick);
await new Promise(process.nextTick);
await new Promise(process.nextTick);
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGTERM');
// Simulate process exiting
@@ -117,10 +126,11 @@ describe('process-utils', () => {
await vi.advanceTimersByTimeAsync(SIGKILL_TIMEOUT_MS);
// Second call should NOT be SIGKILL because it exited
expect(mockProcessKill).not.toHaveBeenCalledWith(-1234, 'SIGKILL');
await killPromise;
});
it('should fallback to specific process kill if group kill fails', async () => {
vi.mocked(os.platform).mockReturnValue('linux');
mockProcessKill.mockImplementationOnce(() => {
+65 -30
View File
@@ -32,7 +32,8 @@ export interface KillOptions {
* or the PTY's built-in kill method.
*
* On Unix, it attempts to kill the process group (using -pid) with escalation
* from SIGTERM to SIGKILL if requested.
* from SIGTERM to SIGKILL if requested. It also walks the process tree using pgrep
* to ensure all descendants are killed.
*/
export async function killProcessGroup(options: KillOptions): Promise<void> {
const { pid, escalate = false, isExited = () => false, pty } = options;
@@ -55,12 +56,59 @@ export async function killProcessGroup(options: KillOptions): Promise<void> {
return;
}
// Unix logic
// Unix logic: Walk process tree to find all descendants
const getAllDescendants = async (parentPid: number): Promise<number[]> => {
let children: number[] = [];
try {
const { stdout } = await spawnAsync('pgrep', [
'-P',
parentPid.toString(),
]);
const pids = stdout
.trim()
.split('\n')
.map((p: string) => parseInt(p, 10))
.filter((p: number) => !isNaN(p));
for (const p of pids) {
children.push(p);
const grandchildren = await getAllDescendants(p);
children = children.concat(grandchildren);
}
} catch {
// pgrep exits with 1 if no children are found
}
return children;
};
const descendants = await getAllDescendants(pid);
const allPidsToKill = [...descendants.reverse(), pid];
try {
const initialSignal = options.signal || (escalate ? 'SIGTERM' : 'SIGKILL');
// Try killing the process group first (-pid)
process.kill(-pid, initialSignal);
try {
process.kill(-pid, initialSignal);
} catch {
// Ignore
}
// Kill individual processes in the tree to ensure detached descendants are caught
for (const targetPid of allPidsToKill) {
try {
process.kill(targetPid, initialSignal);
} catch {
// Ignore
}
}
if (pty) {
try {
pty.kill(typeof initialSignal === 'string' ? initialSignal : undefined);
} catch {
// Ignore
}
}
if (escalate && !isExited()) {
await new Promise((res) => setTimeout(res, SIGKILL_TIMEOUT_MS));
@@ -70,43 +118,30 @@ export async function killProcessGroup(options: KillOptions): Promise<void> {
} catch {
// Ignore
}
}
}
} catch {
// Fallback to specific process kill if group kill fails or on error
if (!isExited()) {
if (pty) {
if (escalate) {
for (const targetPid of allPidsToKill) {
try {
// Attempt the group kill BEFORE the pty session leader dies
process.kill(-pid, 'SIGTERM');
pty.kill('SIGTERM');
await new Promise((res) => setTimeout(res, SIGKILL_TIMEOUT_MS));
if (!isExited()) {
try {
process.kill(-pid, 'SIGKILL');
} catch {
// Ignore
}
pty.kill('SIGKILL');
}
process.kill(targetPid, 'SIGKILL');
} catch {
// Ignore
}
} else {
}
if (pty) {
try {
process.kill(-pid, 'SIGKILL'); // Group kill first
pty.kill('SIGKILL');
} catch {
// Ignore
}
}
} else {
try {
process.kill(pid, 'SIGKILL');
} catch {
// Ignore
}
}
}
} catch {
// Ultimate fallback if something unexpected throws
if (!isExited()) {
try {
process.kill(pid, 'SIGKILL');
} catch {
// Ignore
}
}
}