diff --git a/src/packages/process-host/src/windows-tree-kill.test.ts b/src/packages/process-host/src/windows-tree-kill.test.ts index f931f483fcc..225e96b17e7 100644 --- a/src/packages/process-host/src/windows-tree-kill.test.ts +++ b/src/packages/process-host/src/windows-tree-kill.test.ts @@ -1,5 +1,17 @@ import { ChildProcess } from 'node:child_process' +import type * as Os from 'node:os' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { getPriorityMock, setPriorityMock } = vi.hoisted(() => ({ + getPriorityMock: vi.fn(), + setPriorityMock: vi.fn() +})) + +vi.mock('node:os', async (importOriginal) => ({ + ...(await importOriginal()), + getPriority: getPriorityMock, + setPriority: setPriorityMock +})) import { _setWindowsTaskkillLauncherForTests, inFlightWindowsTreeKill, @@ -154,4 +166,75 @@ describe('windows tree kill tracker', () => { expect(launch).toHaveBeenCalledTimes(2) }) + + describe('termination helper scheduling', () => { + const originalPlatform = process.platform + + beforeEach(() => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + getPriorityMock.mockReset().mockReturnValue(10) + setPriorityMock.mockReset() + launch.mockImplementationOnce(() => { + const killer = new FakeProcess() + Object.defineProperty(killer, 'pid', { value: 5678 }) + launched.push(killer) + return killer + }) + }) + + afterEach(() => { + Object.defineProperty(process, 'platform', { + configurable: true, + value: originalPlatform + }) + }) + + it.each([10, 19])('restores normal priority for a background helper at %i', (priority) => { + getPriorityMock.mockReturnValue(priority) + const killRoot = vi.fn() + const kill = startWindowsTreeKill(new FakeProcess(), 1234, killRoot) + + expect(setPriorityMock).toHaveBeenCalledWith(5678, 0) + expect(kill.verdict()).toBe('live') + expect(killRoot).not.toHaveBeenCalled() + launched[0].emit('close', 0) + expect(kill.verdict()).toBe('exited') + }) + + it.each([0, -7, -14])('preserves a helper already at priority %i', (priority) => { + getPriorityMock.mockReturnValue(priority) + startWindowsTreeKill(new FakeProcess(), 1234, vi.fn()) + + expect(setPriorityMock).not.toHaveBeenCalled() + launched[0].emit('close', 0) + }) + + it.each(['read', 'write'])('keeps tracking when the priority %s fails', async (operation) => { + const fail = (): never => { + throw new Error('priority access denied or helper already exited') + } + if (operation === 'read') { + getPriorityMock.mockImplementation(fail) + } else { + setPriorityMock.mockImplementation(fail) + } + const killRoot = vi.fn() + const kill = startWindowsTreeKill(new FakeProcess(), 1234, killRoot) + + await vi.advanceTimersByTimeAsync(WINDOWS_TASKKILL_WAIT_MS) + expect(kill.verdict()).toBe('live') + expect(killRoot).not.toHaveBeenCalled() + launched[0].emit('close', 0) + await expect(kill.settled).resolves.toBe('exited') + }) + + it('leaves POSIX scheduling unchanged', () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) + startWindowsTreeKill(new FakeProcess(), 1234, vi.fn()) + + expect(getPriorityMock).not.toHaveBeenCalled() + expect(setPriorityMock).not.toHaveBeenCalled() + launched[0].emit('close', 0) + }) + }) }) diff --git a/src/packages/process-host/src/windows-tree-kill.ts b/src/packages/process-host/src/windows-tree-kill.ts index a97af0f6ae2..0837e96f21a 100644 --- a/src/packages/process-host/src/windows-tree-kill.ts +++ b/src/packages/process-host/src/windows-tree-kill.ts @@ -1,4 +1,5 @@ import { spawn as nodeSpawn, type ChildProcess } from 'node:child_process' +import { constants, getPriority, setPriority } from 'node:os' import { windowsSystem32Binary } from './windows-system-binary' /** @@ -118,6 +119,16 @@ export function startWindowsTreeKill( finish('unverifiable') return kill } + if (process.platform === 'win32' && killer.pid !== undefined) { + try { + // Tree termination must not inherit a background caller's scheduling penalty. + if (getPriority(killer.pid) > constants.priority.PRIORITY_NORMAL) { + setPriority(killer.pid, constants.priority.PRIORITY_NORMAL) + } + } catch { + /* Priority changes are best effort; keep tracking the original termination. */ + } + } killer.once('error', () => finish('unverifiable')) killer.once('close', (code) => finish(code === 0 ? 'exited' : 'unverifiable')) deadline = setTimeout(abandon, WINDOWS_TASKKILL_DEADLINE_MS)