返回源码地图

packages/sdk/client/src/dispose.ts

main snapshot · da00f7f5358f · 正文引用章节 14;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Private teardown ladder for the runtime subprocess: stdin EOF (cooperative
3 * quiesce), then SIGTERM, then SIGKILL, resolving only after the process has
4 * actually exited. The SDK client runs OUTSIDE any harness context, so it
5 * cannot ride the `dsh-subprocess` service — this module is the seam's
6 * documented exception for SDK-managed transports.
7 *
8 * @module @deepseek-ai/dsh-sdk-client/dispose
9 */
10
11import type { ChildProcess } from 'node:child_process'
12
13/**
14 * Race the child's exit against a timer. Neither outcome leaves anything
15 * behind on the child: the exit listener is removed on timeout and the timer
16 * is cleared on exit, so the ladder's tiers never accumulate listeners.
17 */
18function exitsWithin(child: ChildProcess, ms: number): Promise<boolean> {
19 if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true)
20 return new Promise<boolean>((resolve) => {
21 const onExit = (): void => {
22 clearTimeout(timer)
23 resolve(true)
24 }
25 // `.unref()` so a pending grace timer never keeps the parent's loop alive.
26 const timer = setTimeout(() => {
27 child.removeListener('exit', onExit)
28 resolve(false)
29 }, ms).unref()
30 child.once('exit', onExit)
31 })
32}
33
34/** Force-terminate the runtime and reject if no exit edge arrives within the grace. */
35function forceTerminateWithin(child: ChildProcess, ms: number): Promise<void> {
36 if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
37 return new Promise<void>((resolve, reject) => {
38 let accepted = false
39 let settled = false
40 const cleanup = (): void => {
41 clearTimeout(timer)
42 child.off('exit', onExit)
43 child.off('error', onError)
44 }
45 const settle = (complete: () => void): void => {
46 if (settled) return
47 settled = true
48 cleanup()
49 complete()
50 }
51 const onExit = (): void => { settle(resolve) }
52 const onError = (error: Error): void => { settle(() => { reject(error) }) }
53 child.once('exit', onExit)
54 child.once('error', onError)
55 const timer = setTimeout(() => {
56 const disposition = accepted ? 'accepted' : 'refused'
57 settle(() => {
58 reject(new Error(`runtime process did not exit within ${ms}ms after SIGKILL was ${disposition}`))
59 })
60 }, ms).unref()
61 try {
62 accepted = child.kill('SIGKILL')
63 if (child.exitCode !== null || child.signalCode !== null) settle(resolve)
64 } catch (error: unknown) {
65 settle(() => { reject(new Error('SIGKILL failed', { cause: error })) })
66 }
67 })
68}
69
70/**
71 * Tear the runtime down to quiescence, resolving only after exit: close stdin
72 * and allow cooperative flush, then use the host's graceful and forced
73 * termination semantics. POSIX sends `SIGTERM` before `SIGKILL`; Windows
74 * skips directly to forced termination because Node maps both signals to
75 * `TerminateProcess`.
76 * @param child - the runtime child process to tear down.
77 * @param graces - the EOF and termination-confirmation windows (ms).
78 * @param platform - the host platform, injectable for unit coverage.
79 * @throws When forced termination errors or the child does not report exit
80 * within `disposeGraceMs`.
81 */
82export async function disposeRuntimeProcess(
83 child: ChildProcess,
84 graces: { disposeEofGraceMs: number; disposeGraceMs: number },
85 platform: NodeJS.Platform = process.platform,
86): Promise<void> {
87 // Already gone: nothing to reap.
88 if (child.exitCode !== null || child.signalCode !== null) return
89 // 1. Close stdin and allow cooperative teardown and durable-state flush.
90 child.stdin?.end()
91 if (await exitsWithin(child, graces.disposeEofGraceMs)) return
92 // 2. POSIX gets a catchable graceful signal; Windows signals all force-terminate.
93 if (platform !== 'win32') {
94 child.kill('SIGTERM')
95 if (await exitsWithin(child, graces.disposeGraceMs)) return
96 }
97 // 3. Force-kill and await a bounded exit edge.
98 await forceTerminateWithin(child, graces.disposeGraceMs)
99}