2026-01-30 09:53:09 -08:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-11 21:06:25 +05:30
|
|
|
import {
|
|
|
|
|
describe,
|
|
|
|
|
it,
|
|
|
|
|
expect,
|
|
|
|
|
vi,
|
|
|
|
|
beforeEach,
|
|
|
|
|
afterEach,
|
|
|
|
|
type Mock,
|
|
|
|
|
type MockInstance,
|
|
|
|
|
} from 'vitest';
|
2026-01-30 09:53:09 -08:00
|
|
|
import os from 'node:os';
|
|
|
|
|
import { killProcessGroup, SIGKILL_TIMEOUT_MS } from './process-utils.js';
|
2026-03-11 21:06:25 +05:30
|
|
|
import { spawnAsync } from './shell-utils.js';
|
2026-01-30 09:53:09 -08:00
|
|
|
|
|
|
|
|
vi.mock('node:os');
|
2026-03-11 21:06:25 +05:30
|
|
|
vi.mock('./shell-utils.js');
|
2026-01-30 09:53:09 -08:00
|
|
|
|
|
|
|
|
describe('process-utils', () => {
|
2026-03-11 21:06:25 +05:30
|
|
|
let mockProcessKill: MockInstance;
|
|
|
|
|
let mockSpawnAsync: Mock;
|
2026-01-30 09:53:09 -08:00
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
vi.useFakeTimers();
|
2026-03-11 21:06:25 +05:30
|
|
|
mockProcessKill = vi.spyOn(process, 'kill').mockImplementation(() => true);
|
|
|
|
|
mockSpawnAsync = vi.mocked(spawnAsync);
|
2026-01-30 09:53:09 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.useRealTimers();
|
2026-03-11 21:06:25 +05:30
|
|
|
vi.restoreAllMocks();
|
2026-01-30 09:53:09 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('killProcessGroup', () => {
|
|
|
|
|
it('should use taskkill on Windows', async () => {
|
|
|
|
|
vi.mocked(os.platform).mockReturnValue('win32');
|
|
|
|
|
|
|
|
|
|
await killProcessGroup({ pid: 1234 });
|
|
|
|
|
|
2026-03-11 21:06:25 +05:30
|
|
|
expect(mockSpawnAsync).toHaveBeenCalledWith('taskkill', [
|
2026-01-30 09:53:09 -08:00
|
|
|
'/pid',
|
|
|
|
|
'1234',
|
|
|
|
|
'/f',
|
|
|
|
|
'/t',
|
|
|
|
|
]);
|
|
|
|
|
expect(mockProcessKill).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-11 21:06:25 +05:30
|
|
|
it('should use pty.kill() on Windows if pty is provided and also taskkill for descendants', async () => {
|
2026-01-30 09:53:09 -08:00
|
|
|
vi.mocked(os.platform).mockReturnValue('win32');
|
|
|
|
|
const mockPty = { kill: vi.fn() };
|
|
|
|
|
|
|
|
|
|
await killProcessGroup({ pid: 1234, pty: mockPty });
|
|
|
|
|
|
|
|
|
|
expect(mockPty.kill).toHaveBeenCalled();
|
2026-03-11 21:06:25 +05:30
|
|
|
// taskkill is also called to reap orphaned descendant processes
|
|
|
|
|
expect(mockSpawnAsync).toHaveBeenCalledWith('taskkill', [
|
|
|
|
|
'/pid',
|
|
|
|
|
'1234',
|
|
|
|
|
'/f',
|
|
|
|
|
'/t',
|
|
|
|
|
]);
|
2026-01-30 09:53:09 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should kill the process group on Unix with SIGKILL by default', async () => {
|
|
|
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
|
|
|
|
|
|
|
|
await killProcessGroup({ pid: 1234 });
|
|
|
|
|
|
|
|
|
|
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGKILL');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use escalation on Unix if requested', async () => {
|
|
|
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
|
|
|
const exited = false;
|
|
|
|
|
const isExited = () => exited;
|
|
|
|
|
|
|
|
|
|
const killPromise = killProcessGroup({
|
|
|
|
|
pid: 1234,
|
|
|
|
|
escalate: true,
|
|
|
|
|
isExited,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// First call should be SIGTERM
|
|
|
|
|
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGTERM');
|
|
|
|
|
|
|
|
|
|
// Advance time
|
|
|
|
|
await vi.advanceTimersByTimeAsync(SIGKILL_TIMEOUT_MS);
|
|
|
|
|
|
|
|
|
|
// Second call should be SIGKILL
|
|
|
|
|
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGKILL');
|
|
|
|
|
|
|
|
|
|
await killPromise;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should skip SIGKILL if isExited returns true after SIGTERM', async () => {
|
|
|
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
|
|
|
let exited = false;
|
|
|
|
|
const isExited = vi.fn().mockImplementation(() => exited);
|
|
|
|
|
|
|
|
|
|
const killPromise = killProcessGroup({
|
|
|
|
|
pid: 1234,
|
|
|
|
|
escalate: true,
|
|
|
|
|
isExited,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGTERM');
|
|
|
|
|
|
|
|
|
|
// Simulate process exiting
|
|
|
|
|
exited = true;
|
|
|
|
|
|
|
|
|
|
await vi.advanceTimersByTimeAsync(SIGKILL_TIMEOUT_MS);
|
|
|
|
|
|
|
|
|
|
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(() => {
|
|
|
|
|
throw new Error('ESRCH');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await killProcessGroup({ pid: 1234 });
|
|
|
|
|
|
|
|
|
|
// Failed group kill
|
|
|
|
|
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGKILL');
|
|
|
|
|
// Fallback individual kill
|
|
|
|
|
expect(mockProcessKill).toHaveBeenCalledWith(1234, 'SIGKILL');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use pty fallback on Unix if group kill fails', async () => {
|
|
|
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
|
|
|
mockProcessKill.mockImplementationOnce(() => {
|
|
|
|
|
throw new Error('ESRCH');
|
|
|
|
|
});
|
|
|
|
|
const mockPty = { kill: vi.fn() };
|
|
|
|
|
|
|
|
|
|
await killProcessGroup({ pid: 1234, pty: mockPty });
|
|
|
|
|
|
|
|
|
|
expect(mockPty.kill).toHaveBeenCalledWith('SIGKILL');
|
|
|
|
|
});
|
2026-03-11 21:06:25 +05:30
|
|
|
|
|
|
|
|
it('should attempt process group kill on Unix after pty fallback to reap orphaned descendants', async () => {
|
|
|
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
|
|
|
// First call (group kill) throws to trigger PTY fallback
|
|
|
|
|
mockProcessKill.mockImplementationOnce(() => {
|
|
|
|
|
throw new Error('ESRCH');
|
|
|
|
|
});
|
|
|
|
|
// Second call (group kill retry after pty.kill) should succeed
|
|
|
|
|
mockProcessKill.mockImplementationOnce(() => true);
|
|
|
|
|
const mockPty = { kill: vi.fn() };
|
|
|
|
|
|
|
|
|
|
await killProcessGroup({ pid: 1234, pty: mockPty });
|
|
|
|
|
|
|
|
|
|
// Group kill should be called first to ensure it's hit before PTY leader dies
|
|
|
|
|
expect(mockProcessKill).toHaveBeenCalledWith(-1234, 'SIGKILL');
|
|
|
|
|
// Then PTY kill should be called
|
|
|
|
|
expect(mockPty.kill).toHaveBeenCalledWith('SIGKILL');
|
|
|
|
|
});
|
2026-01-30 09:53:09 -08:00
|
|
|
});
|
|
|
|
|
});
|