# 进程管理器 [English](subprocess.md) | 中文 进程管理器 seam 分为接口([dsh-subprocess](../../packages/subprocess/subprocess),`ctx.subprocess`)与实现([dsh-subprocess-local](../../packages/subprocess/subprocess-local));它的消费方是其他能力 seam 与进程外后端:[bash 执行器家族](bash.md)使用收集模式的批量输出,LSP 与 Code Runtime 主机使用原始协议管道,PTY 后端使用终端原语,ACP(Agent Client Protocol)subagent 后端则使用管道化 ndjson 加 inherit 的 stderr。该 seam 拥有受管的 `DSH_*` 环境命名空间、共享的凭据清除(`scrubbedParentEnv`)与 `CollectedOutput` 形状;[dsh-bash](../../packages/bash/bash) 重导出这套词汇,使 bash 消费方保持单一导入入口。 源码:[`packages/subprocess/subprocess/src/types.ts`](../../packages/subprocess/subprocess/src/types.ts) 与 [`packages/subprocess/subprocess/src/index.ts`](../../packages/subprocess/subprocess/src/index.ts) ## 执行世界坐标 一个提供方的 `cwd`、可执行文件路径、普通进程与终端会话,和挂载的文件系统提供方处于同一路径与进程命名空间。`resolveExecutable(command, env?, signal?)` 验证绝对可执行文件路径,或通过提供方清理后的 `PATH` 加有意覆盖来解析裸名称。 ## 受管环境命名空间与捕获的输出 `DSH_*` 变量是归 Harness 所有的子进程事实;实现会在合并调用方显式 `env` 之前丢弃环境中已有的 `DSH_*` 名称,因此当前事实只会以有意提供的条目形式到达,每条被收集的流都通过 `CollectedOutput` 报告自身的截断与 spill 恢复状态。 ```ts type-equiv /** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */ type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}` ``` ```ts type-equiv /** Trusted DeepSeek Harness variables for one child-process execution. */ type DshEnvironment = Readonly> ``` ```ts type-equiv /** One captured stream: the (possibly truncated) text plus recovery info. */ interface CollectedOutput { /** Collected text — the TAIL of the stream when truncated. */ text: string /** True when bytes were dropped from `text`. */ truncated: boolean /** Path to a file holding the COMPLETE stream, when truncated and available. */ spillPath?: string } ``` ## Node 形状的 stdio 处置方式(disposition) 每条流的处置方式都显式给出,由各消费方自行选择:原始管道用于协议分帧(LSP JSON-RPC、ACP ndjson),inherit 用于直通的诊断输出,收集模式用于有界的批量输出;其中 spill 文件是可选的,因此诊断尾部(语言服务器的 stderr)可以只在内存中缓冲,不留下任何文件。 ```ts type-equiv /** * stdin disposition. `'ignore'` leaves fd 0 on `/dev/null`; `'pipe'` exposes * {@link SubprocessHandle.stdin} for the caller's ongoing protocol writes; * `{ data }` writes the bytes and closes (the batch shape). */ type SubprocessStdinMode = 'ignore' | 'pipe' | { readonly data: string } ``` ```ts type-equiv /** * Bounded in-memory collection for one output stream, with an optional * full-stream spill file. Omitting `spill` keeps only the in-memory tail — * the diagnostic-tail shape (a language server's stderr); including it makes * the complete stream recoverable up to its cap (the bash tool shape). */ interface SubprocessCollect { /** In-memory cap in bytes; overflow keeps the TAIL. */ maxBytes: number /** Full-stream spill file; absent disables spilling entirely. */ spill?: { /** Whole-stream byte cap; a larger stream discards its now-incomplete spill. */ maxBytes: number } } ``` ```ts type-equiv /** * stdout/stderr disposition. `'pipe'` exposes the raw `Readable` for the * caller's protocol decoding; `'inherit'` passes the parent's descriptor * through (child diagnostics land on the harness's own stream); a * {@link SubprocessCollect} object buffers boundedly with offset-based reads. */ type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect ``` ```ts type-equiv /** Per-stream stdio dispositions, all explicit — this seam applies no defaults. */ interface SubprocessStdio { stdin: SubprocessStdinMode stdout: SubprocessOutputMode stderr: SubprocessOutputMode } ``` ## 完全显式的 spawn spec 该 seam 不应用任何默认值:每项处置方式、限制与目录都在 spec 上显式给出,因此由调用方自己的配置决定它们,而不是由某个隐藏的进程管理器默认值决定。`argv` 绝不经过 shell 解释。 ```ts type-equiv /** * A fully-specified spawn request. This seam applies no defaults: every * disposition, limit, and directory is explicit, so the caller's own config — * not a hidden subprocess-service default — decides them (the `dsh-bash` * request/spec split is the owning template). */ interface SubprocessSpawnSpec { /** Executable and arguments; `argv[0]` is the program. Never shell-interpreted here. */ argv: readonly string[] /** Working directory for the child. */ cwd: string /** Per-stream stdio dispositions. */ stdio: SubprocessStdio /** * Grace period in milliseconds for the {@link SubprocessHandle.terminate} * escalation and for draining still-open collected pipes after the process * exits (an inherited descriptor held by a surviving descendant cannot hold * the outcome open indefinitely). */ graceMs: number /** * Abort signal — starts the terminate escalation on the process tree when * it fires. The caller owns deadlines and cause classification; this seam * only reacts to the abort. */ signal?: AbortSignal | undefined /** * Explicit environment entries merged onto the implementation's scrubbed * parent base (see `scrubbedParentEnv`), with no namespace validation: * every entry is a deliberate caller opt-in, so a forwarded * credential-shaped entry or a current `DSH_*` fact survives precisely * because this layer merges after the scrub that drops its ambient * namesake. */ env?: Record | undefined } ``` ## 句柄:流、读取器与以进程树为范围的终止 spawn 会立即返回一个实时句柄。收集模式的读取器接受全流字节偏移量且从不消费,因此独立的读取器不会抢走彼此的增量;管道化的流归调用方所有。终止在每个平台上都以进程树为范围:`terminate()`(唯一的终止动词)执行 SIGTERM→宽限期→SIGKILL 升级,`waitForExit()` 观察整棵进程树——这足以让消费方构建自己的拆卸阶梯(ACP 后端以 stdin EOF 打头的 `disposeAcpChild` 即是模板)。 ```ts type-equiv /** * A live child process rooted in its own process tree. Collected output * remains readable after exit; piped streams belong to the caller. * * Termination is tree-scoped everywhere: POSIX signals the detached process * group (falling back to the direct child when the group is gone), Windows * terminates the tree via `taskkill /T`, so helper processes cannot outlive * the handle unnoticed. */ interface SubprocessHandle { /** Process id (tree root); -1 when the spawn itself failed. */ readonly pid: number /** The child's stdin, present iff spawned with `stdin: 'pipe'`. */ readonly stdin: Writable | undefined /** The child's raw stdout, present iff spawned with `stdout: 'pipe'`. */ readonly stdout: Readable | undefined /** The child's raw stderr, present iff spawned with `stderr: 'pipe'`. */ readonly stderr: Readable | undefined /** Offset-based readers for collect-mode streams (also readable after exit). */ readonly collected: SubprocessCollectedOutputs /** Resolves at process close with exit facts; rejects only for spawn-level failures. */ readonly done: Promise /** * Begin the SIGTERM → `graceMs` → SIGKILL escalation on the process tree * (Windows force-terminates immediately) — the seam's only termination * verb. Idempotent, a no-op once the tree is gone (the pid may be reused), * and also triggered by the spec's abort signal. */ terminate(): void /** * Wait until the process tree has exited — the tree, not just the direct * child, so a still-running helper is observable before teardown returns. * @param signal - optional bound for the wait. * @returns `true` when the tree exited, `false` when the signal aborted first. */ waitForExit(signal?: AbortSignal): Promise } ``` ```ts type-equiv /** * Cursor-free incremental access to one collected output stream. Offsets are * whole-stream byte coordinates owned by the caller, so independent readers * cannot consume one another's output; `readFrom(0)` after settlement is the * batch result (`lossy` then means the in-memory tail lost its head — the * {@link CollectedOutput.truncated} fact). */ interface SubprocessOutputReader { /** * Read everything captured since `fromByte`. When that offset has slid out * of the in-memory tail window the read is `lossy` — it returns the whole * retained tail and the gap is only recoverable from the spill file. * @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read). * @returns the delta text, the next offset, the `lossy` flag, and the spill path when one exists. */ readFrom(fromByte: number): SubprocessOutputRead } ``` ```ts type-equiv /** One incremental {@link SubprocessOutputReader.readFrom} read. */ interface SubprocessOutputRead { /** Stream text from the requested offset (the whole retained tail when lossy). */ text: string /** Whole-stream offset to resume from on the next read. */ nextOffset: number /** True when the requested offset slid out of the in-memory tail window. */ lossy: boolean /** Path to the full-stream spill file, when one was created and remains intact. */ spillPath?: string } ``` ```ts type-equiv /** Offset-based readers for the streams spawned in collect mode. */ interface SubprocessCollectedOutputs { /** Present iff stdout is a {@link SubprocessCollect}. */ readonly stdout?: SubprocessOutputReader /** Present iff stderr is a {@link SubprocessCollect}. */ readonly stderr?: SubprocessOutputReader } ``` ## 结果只承载退出事实 `done` 报告 Node close 事件的词汇,不携带原因分类:服务会在中止时终止进程,但绝不判定原因(调用方读取归自己所有的 deadline 信号,例如 bash 执行器的 `timedOut`/`aborted` 拆分)。收集到的输出在结算后仍可经 `handle.collected` 读取,因此批量与流式调用方共用一条访问路径。 ```ts type-equiv /** * Exit facts of one closed process — Node's `close`-event vocabulary. * Deliberately carries NO timeout or cancellation classification (the caller * reads the signal it owns to classify causes) and NO output: collected * streams stay readable through {@link SubprocessHandle.collected} after * settlement, so batch and streaming callers share one access path. */ interface SubprocessOutcome { /** Exit code; null when the process died from a signal. */ exitCode: number | null /** Terminating signal (e.g. 'SIGTERM'); null on normal exit. */ signal: NodeJS.Signals | null } ``` ## 终端进程原语 `spawnTerminal(spec)` 是非管道进程原语。提供方分配控制终端,并负责 UTF-8 文本传输、前台进程组检查与信号发送,以及一项须等待的 TERM→KILL 操作;该操作会使提供方仍可观察到的每个会话成员完全停稳,提供方则会记录执行基底特有的可观察性限制。PTY 后端仍负责提示符检测、就绪推断、scrollback、沙箱策略和持久会话所有权;普通 `spawn()` 无法重建控制终端语义。 终端 spec 完全指定 argv、cwd、环境覆盖、尺寸、清理宽限期与可选的分配取消。其句柄公开 `pid`、有序输出、`done`、`write`、`inspectForeground`、`signalForeground` 和须等待的 `terminate`;确切的公共形状生成到 [`ctx.subprocess` 服务目录](../cordis-catalog/services.md#ctxsubprocess--subprocessservice-abstract-seam)中。 ## 服务行为 抽象的 [`SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam 定义执行世界坐标、可执行文件查找、普通 `spawn` 与 `spawnTerminal`。[`LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) 以临时运行时存储、detached 进程树、按处置方式接线、凭据清除、`node-pty`、平台进程检查,以及先终止再等待退出的资源释放实现这些能力。接口契约见 [`dsh-subprocess`](../../packages/subprocess/subprocess/README.md),本地机制见 [`dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md)。