fix(subprocess): retain failed terminal cleanup

This commit is contained in:
Tianyi Cui
2026-07-29 08:00:21 +08:00
parent a95e01d8b0
commit 06a9224904
2 changed files with 29 additions and 4 deletions

View File

@@ -64,10 +64,9 @@ export class LocalSubprocessService extends SubprocessService {
// an identity-fenced descendant survives escalation). Await the cleanup
// transaction directly so disposal reports that failure rather than
// waiting forever on `done`.
pending.push(terminal.waitForExit())
pending.push(terminal.waitForExit().then(() => { this.terminals.delete(terminal) }))
}
this.live.clear()
this.terminals.clear()
await Promise.all(pending)
await rm(this.runtimeRoot, { recursive: true, force: true })
}, 'local subprocess teardown')

View File

@@ -1,6 +1,6 @@
import { PassThrough } from 'node:stream'
import { describe, expect, it, vi } from 'vitest'
import { stat } from 'node:fs/promises'
import { rm, stat } from 'node:fs/promises'
import { basename, delimiter, dirname, relative } from 'node:path'
import { Context } from 'cordis'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
@@ -108,10 +108,36 @@ describe('LocalSubprocessService', () => {
terminate,
waitForExit,
}
;(ctx.subprocess as unknown as { terminals: Set<SubprocessTerminalHandle> }).terminals.add(terminal)
const terminals = (ctx.subprocess as unknown as { terminals: Set<SubprocessTerminalHandle> }).terminals
terminals.add(terminal)
await fiber.dispose()
expect(terminate).toHaveBeenCalledOnce()
expect(waitForExit).toHaveBeenCalledOnce()
expect(terminals.size).toBe(0)
})
it('retains an owned terminal when disposal cleanup rejects', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(LocalSubprocessService)
const service = ctx.subprocess
const runtimeRoot = service.runtimeRoot
const terminal: SubprocessTerminalHandle = {
pid: 1,
output: new PassThrough(),
done: Promise.resolve({ exitCode: 0, signal: null }),
write: async () => {},
inspectForeground: async () => undefined,
signalForeground: async () => 1,
terminate: vi.fn(),
waitForExit: vi.fn(async () => { throw new Error('retryable cleanup failure') }),
}
const terminals = (service as unknown as { terminals: Set<SubprocessTerminalHandle> }).terminals
terminals.add(terminal)
await fiber.dispose()
expect(terminals).toEqual(new Set([terminal]))
expect((await stat(runtimeRoot)).isDirectory()).toBe(true)
await rm(runtimeRoot, { recursive: true, force: true })
})
it('releases a terminal after top-level exit reaches quiescence', async () => {