fix(subprocess): treat zombie-only groups as quiescent

This commit is contained in:
Tianyi Cui
2026-07-29 06:04:36 +08:00
parent ea75333c92
commit d29c73ddd6
4 changed files with 76 additions and 1 deletions

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { createProcessInspector, parseProcStat } from '@deepseek-ai/dsh-subprocess-local/src/process-inspector.ts'
import {
createProcessInspector,
linuxProcessGroupHasLiveMembers,
parseProcStat,
} from '@deepseek-ai/dsh-subprocess-local/src/process-inspector.ts'
import type { ProcessInspectorInternals } from '@deepseek-ai/dsh-subprocess-local/src/process-inspector.ts'
function stat(pid: number, pgrp: number, session: number, tpgid: number, started: string, parentPid = 1, state = 'S'): string {
@@ -63,6 +67,21 @@ function fakeInternals() {
}
describe('Linux process inspector', () => {
it('treats zombie-only process groups as quiescent and fails closed when unobservable', () => {
const fake = fakeInternals()
expect(linuxProcessGroupHasLiveMembers(77, fake.internals)).toBeUndefined()
fake.dirs.set('/proc', ['self', '10', '11', '12'])
fake.files.set('/proc/10/stat', stat(10, 77, 10, -1, '500', 1, 'Z'))
fake.files.set('/proc/11/stat', stat(11, 77, 10, -1, '501', 1, 'X'))
fake.files.set('/proc/12/stat', stat(12, 88, 12, -1, '502'))
expect(linuxProcessGroupHasLiveMembers(77, fake.internals)).toBe(false)
expect(linuxProcessGroupHasLiveMembers(99, fake.internals)).toBeUndefined()
fake.files.set('/proc/11/stat', stat(11, 77, 10, -1, '501'))
expect(linuxProcessGroupHasLiveMembers(77, fake.internals)).toBe(true)
})
it('parses stat safely, captures only the rooted process tree, and signals identities', () => {
expect(parseProcStat('bad')).toBeUndefined()
expect(parseProcStat('1 () ')).toBeUndefined()

View File

@@ -268,6 +268,24 @@ describe('spawnSubprocess', () => {
expect(result.signal).toBe('SIGTERM')
})
it('does not wait for a Linux group that has only zombie members', async () => {
const pidFile = join(spillDir, `zombie-group-${Date.now()}.pid`)
let hasLiveMembers = false
const running = spawnSubprocess(spec(`sleep 60 & echo $! > ${pidFile}; echo leader-done`, { graceMs: 100 }), {
platform: 'linux',
linuxProcessGroupHasLiveMembers: () => hasLiveMembers,
})
const descendant = await waitForPidFile(pidFile)
try {
await running.done
await expect(running.waitForExit()).resolves.toBe(true)
} finally {
hasLiveMembers = true
running.terminate()
await waitGone(descendant)
}
})
it('bounds inherited-pipe draining after the shell exits', async () => {
const pidFile = join(spillDir, `pipe-holder-${Date.now()}.pid`)
const started = Date.now()