Give Windows taskkill a 5s budget to reduce flaky tree cleanup (#28577)

* Give Windows taskkill a 5s budget before tree cleanup is reported unveri

On busy Windows machines, `taskkill` could take longer than the 2s budget
used for other subprocesses. Tree termination then gave up before the
process tree was confirmed gone. The taskkill timeout is now 5s, matching
the other Windows tree-cleanup paths.

Tests cover a slow taskkill that still verifies termination, and a hung
taskkill that is bounded and falls back to SIGKILL. The batch-shim recovery
test gets a longer hang and a 30s timeout so it has room on slow runners.

* Pass the context clock into structured session option writes

- Expose the injected `now` clock to the structured agent session option writer, so timestamps come from the same source as the rest of the context.
- Update the main and renderer layout-writer baselines to match the reduced line counts in `src/main/ipc/session.ts` and the removed `set-active-folder-workspace.ts` entry.
This commit is contained in:
Jinjing
2026-10-14 05:38:48 +00:00
committed by GitHub
parent 069bbaa4f9
commit ea7942e638
6 changed files with 48 additions and 5 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
1 src/main/ipc/renderer-shutdown-checkpoint.ts
1 src/main/ipc/runtime-environment-removal-cleanup.ts
1 src/main/ipc/runtime-environment-session-reconcile.ts
3 src/main/ipc/session.ts
2 src/main/ipc/session.ts
6 src/main/orca-profiles/profile-project-session-state.ts
4 src/main/orca-profiles/profile-project-source-removal.ts
2 src/main/orca-profiles/profile-project-state-file.ts
@@ -102,7 +102,6 @@
6 src/renderer/src/store/slices/terminal-orphan-helpers.ts
4 src/renderer/src/store/slices/terminal-tab-title-batch.ts
6 src/renderer/src/store/slices/terminals.ts
5 src/renderer/src/store/slices/worktrees/session/set-active-folder-workspace.ts
7 src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts
15 src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts
2 src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts
@@ -265,6 +265,7 @@ describe.runIf(process.platform === 'win32')('Windows preflight batch shims', ()
}
})
// Include the probe, tree-cleanup deadline, and recovery probe on a busy Windows runner.
it('recovers after a hung batch launcher and terminates its Node child', async () => {
const first = path.join(root, 'hung')
const second = path.join(root, 'working')
@@ -273,7 +274,7 @@ describe.runIf(process.platform === 'win32')('Windows preflight batch shims', ()
const name = `${COMMAND}.CMD`
await writeFile(
path.join(first, 'hang.js'),
`require('node:fs').writeFileSync(require('node:path').join(__dirname, 'child.pid'), String(process.pid)); setTimeout(function () {}, 30000);`
`require('node:fs').writeFileSync(require('node:path').join(__dirname, 'child.pid'), String(process.pid)); setTimeout(function () {}, 60000);`
)
await writeFile(path.join(first, name), `@echo off\r\n"${process.execPath}" "%~dp0hang.js"\r\n`)
await writeFile(
@@ -290,5 +291,5 @@ describe.runIf(process.platform === 'win32')('Windows preflight batch shims', ()
const childPid = Number(await readFile(path.join(first, 'child.pid'), 'utf8'))
expect(Number.isSafeInteger(childPid) && childPid > 0).toBe(true)
expect(() => process.kill(childPid, 0)).toThrow()
}, 15_000)
}, 30_000)
})
@@ -19,6 +19,7 @@ export async function writeStructuredAgentSessionOption(
const ctx = {
sessionId,
fence,
now: () => context.now(),
adapter: context.deps.adapter,
now: context.now,
persistOptions: (options: Readonly<Record<string, string>>) =>
@@ -105,6 +105,46 @@ describe('forceTerminateProcessTree', () => {
})
})
it('allows a slow Windows taskkill to verify tree termination', async () => {
await withWindows(async () => {
vi.useFakeTimers()
const child = mockProcess(1234)
const taskkill = mockProcess(5678)
spawnMock.mockReturnValue(taskkill)
let settled = false
const pending = forceTerminateProcessTree(child)
void pending.then(() => {
settled = true
})
await vi.advanceTimersByTimeAsync(3_000)
expect(settled).toBe(false)
expect(taskkill.kill).not.toHaveBeenCalled()
expect(child.kill).not.toHaveBeenCalled()
taskkill.emit('close', 0)
await expect(pending).resolves.toBe(true)
})
})
it('bounds a hung Windows taskkill and leaves tree termination unverified', async () => {
await withWindows(async () => {
vi.useFakeTimers()
const child = mockProcess(1234)
const taskkill = mockProcess(5678)
spawnMock.mockReturnValue(taskkill)
const pending = forceTerminateProcessTree(child)
await vi.advanceTimersByTimeAsync(5_000)
await expect(pending).resolves.toBe(false)
expect(taskkill.kill).toHaveBeenCalledOnce()
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
taskkill.emit('close', 0)
expect(child.kill).toHaveBeenCalledOnce()
})
})
it.skipIf(process.platform === 'win32')(
'stops waiting when a POSIX process group cannot become quiescent',
async () => {
@@ -4,6 +4,8 @@ import { windowsSystem32Binary } from './windows-system-binary'
const PROBE_INTERVAL_MS = 25
const SUBPROCESS_TIMEOUT_MS = 2_000
// Allow the same taskkill startup budget as the other Windows tree-cleanup paths.
const WINDOWS_TASKKILL_TIMEOUT_MS = 5_000
const MAX_PS_OUTPUT_BYTES = 8 * 1024 * 1024
/**
@@ -131,7 +133,7 @@ function taskkillTree(
const timer = setTimeout(() => {
killer.kill()
finish(true)
}, SUBPROCESS_TIMEOUT_MS)
}, WINDOWS_TASKKILL_TIMEOUT_MS)
timer.unref?.()
})
}