From 39874e62ba9b17d4216c8b07dfd1ab8bd85ca981 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:32:15 +0800 Subject: [PATCH] docs(subprocess-local): state the provider-lifetime Linux probe and its cost The README says the provider also keeps a failed or timed-out first probe and names the user manager as reachable rather than live, since it is checked once. The probe cache documents why no later spawn repeats the probe, and the spawn test now starts two Windows commands to match its per-spawn Job recheck title. --- packages/subprocess/subprocess-local/README.i18n.yaml | 4 ++-- packages/subprocess/subprocess-local/README.md | 2 +- packages/subprocess/subprocess-local/README.zh.md | 2 +- packages/subprocess/subprocess-local/src/index.ts | 7 +++++-- .../subprocess/subprocess-local/tests/local.spec.ts | 11 +++++++---- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/subprocess/subprocess-local/README.i18n.yaml b/packages/subprocess/subprocess-local/README.i18n.yaml index 0aee3574a1..d50c32b8c0 100644 --- a/packages/subprocess/subprocess-local/README.i18n.yaml +++ b/packages/subprocess/subprocess-local/README.i18n.yaml @@ -63,8 +63,8 @@ en: e290ccc9b3c92c08 zh: d75d48f485447a74 /deepseek-ai-dsh-subprocess-local/known-limitations-and-deferred-work: - en: a4f48658efb1b423 - zh: 6a6e17e9bdbb5bca + en: b8a16a03111a6e3e + zh: 93b2ab68263ffef3 /deepseek-ai-dsh-subprocess-local/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/subprocess/subprocess-local/README.md b/packages/subprocess/subprocess-local/README.md index 20f38aa2fa..bea8d84e22 100644 --- a/packages/subprocess/subprocess-local/README.md +++ b/packages/subprocess/subprocess-local/README.md @@ -147,7 +147,7 @@ These limits define when the provider is a poor fit or needs special operational - **Linux direct exit has no independent deadline** — after direct `SIGKILL` is acknowledged, uninterruptible kernel I/O can keep disposal pending indefinitely; `graceMs` and scope-query polling budgets do not bound this wait. - **Native ownership has explicit host requirements** — Linux needs a readable user manager whose `systemd-run` keeps scope arguments literal: version 254 and later receive `--expand-environment=no`, and earlier versions, whose scope mode never expands arguments, run without it. Hosts without a usable user manager use the warned PGID fallback. macOS always uses that fallback because no supported public persistent owner exists. -- **Native selection has bounded per-spawn costs** — Linux checks the bootstrap entry, libc `execve`/`fcntl` bindings, live user manager, and literal-argv scope probe once, on the first eligible ordinary or terminal spawn, and keeps that verdict for the provider lifetime: a user manager that stops later makes scope launches fail, and one that starts later is used only after the provider restarts. Windows rechecks the runner entry, bindings, and current Job support before every ordinary spawn. Fallback-warning de-duplication also persists for the provider lifetime. All probes finish before the user command can run, and child-process probes have a 5-second timeout. Each Linux launch creates a private request directory, checks unresolved scope establishment every 50 milliseconds, then exponentially backs off an established active scope to at most 5 seconds between queries; a Windows ordinary launch keeps one runner and IPC channel until the Job reports zero active processes. Target standard handles are inherited directly, with no named-pipe stdio or result files. +- **Native selection has bounded per-spawn costs** — Linux checks the bootstrap entry, libc `execve`/`fcntl` bindings, reachable user manager, and literal-argv scope probe once, on the first eligible ordinary or terminal spawn, and keeps that verdict for the provider lifetime, including a failed or timed-out first probe: a user manager that stops later makes scope launches fail, and one that starts later is used only after the provider restarts. Windows rechecks the runner entry, bindings, and current Job support before every ordinary spawn. Fallback-warning de-duplication also persists for the provider lifetime. All probes finish before the user command can run, and child-process probes have a 5-second timeout. Each Linux launch creates a private request directory, checks unresolved scope establishment every 50 milliseconds, then exponentially backs off an established active scope to at most 5 seconds between queries; a Windows ordinary launch keeps one runner and IPC channel until the Job reports zero active processes. Target standard handles are inherited directly, with no named-pipe stdio or result files. - **Windows Job inheritance has defined exclusions** — ordinary descendants inherit the Job by default, but breakaway processes are outside the guarantee. The target starts only after Job assignment; external termination of the runner in the narrow create-to-assignment interval can leave a suspended target behind. - **Windows terminal signalling is console-wide** — SIGINT is delivered as a `\x03` Ctrl-C input write that the console host turns into a console-wide CTRL_C event (on Windows that host is node-pty's bundled OpenConsole); SIGTSTP and SIGHUP are rejected as unavailable; a `taskkill` without `/F` does not terminate console processes, so the teardown TERM tier is a grace wait before the `/F` escalation. Windows readiness has no exact stdin-wait tier: the prompt-marker fast path compares the shell pid as the pseudo foreground group, and silence/timing tiers cover the rest. - **Windows terminal allocation requires node-pty's bundled console host assets** — every Windows terminal is spawned with `useConptyDll: true`, so allocation resolves `conpty/conpty.dll` and `OpenConsole.exe` relative to node-pty's own addon; an asset that is missing or unpacked wrong makes `spawnTerminal` throw instead of falling back to the operating system console host. node-pty marks the option experimental, so a node-pty upgrade can change this behaviour. See the [bundled console host note](../../../.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.md). diff --git a/packages/subprocess/subprocess-local/README.zh.md b/packages/subprocess/subprocess-local/README.zh.md index 520bbd2f8b..69d817c265 100644 --- a/packages/subprocess/subprocess-local/README.zh.md +++ b/packages/subprocess/subprocess-local/README.zh.md @@ -147,7 +147,7 @@ spill 文件以 `0600` 权限、`O_EXCL` 与随机名称在 `0700` 每进程目 - **Linux 直接进程退出没有独立截止时间**——直接 `SIGKILL` 获得确认后,不可中断的内核 I/O 可能让 dispose 无限期等待;`graceMs` 和 scope 查询的轮询预算不限制此等待。 - **native ownership 有明确宿主要求**——Linux 需要可读的 user manager,且其 `systemd-run` 保持 scope 参数按字面传递:254 及以上版本传入 `--expand-environment=no`,更早版本的 scope 模式从不展开参数,因此不传该选项。没有可用 user manager 的宿主使用带告警的 PGID fallback。macOS 因没有受支持的公开 persistent owner,始终使用该 fallback。 -- **native 选择具有有界的每次 spawn 成本**——Linux 只在第一次符合条件的普通命令或终端 spawn 时检查一次 bootstrap 入口、libc `execve`/`fcntl` bindings、存活的 user manager 与 literal-argv scope 支持,并在提供方生命周期内保留这一结论:之后停止的 user manager 会让 scope 启动失败,之后才启动的 user manager 要等提供方重启后才会被使用。Windows 会在每次普通 spawn 前重新检查 runner 入口、bindings 与当前 Job 支持。fallback 告警去重同样在提供方生命周期内持续保留。所有探测都会在用户命令可能运行前完成,子进程探测的超时为 5 秒。每次 Linux 启动都会创建私有请求目录,以 50 毫秒间隔检查尚未确定的 scope 建立状态;scope 已建立且仍 active 后,查询间隔按指数增长,最多为 5 秒。Windows 普通命令会保留一个 runner 与一条 IPC 通道,直到 Job 报告活动进程数为零。目标会直接继承标准句柄,不使用 named-pipe stdio 或结果文件。 +- **native 选择具有有界的每次 spawn 成本**——Linux 只在第一次符合条件的普通命令或终端 spawn 时检查一次 bootstrap 入口、libc `execve`/`fcntl` bindings、可连接的 user manager 与 literal-argv scope 支持,并在提供方生命周期内保留这一结论,首次探测失败或超时的结论也是如此:之后停止的 user manager 会让 scope 启动失败,之后才启动的 user manager 要等提供方重启后才会被使用。Windows 会在每次普通 spawn 前重新检查 runner 入口、bindings 与当前 Job 支持。fallback 告警去重同样在提供方生命周期内持续保留。所有探测都会在用户命令可能运行前完成,子进程探测的超时为 5 秒。每次 Linux 启动都会创建私有请求目录,以 50 毫秒间隔检查尚未确定的 scope 建立状态;scope 已建立且仍 active 后,查询间隔按指数增长,最多为 5 秒。Windows 普通命令会保留一个 runner 与一条 IPC 通道,直到 Job 报告活动进程数为零。目标会直接继承标准句柄,不使用 named-pipe stdio 或结果文件。 - **Windows Job inheritance 有明确排除项**——普通后代默认继承 Job,但 breakaway 进程不在保证范围。目标只在 Job 分配后启动;runner 若在 create-to-assignment 极窄区间遭外力终止,可能留下 suspended target。 - **Windows 终端信号是控制台级的**——SIGINT 以 `\x03` Ctrl-C 输入写入投递,由控制台宿主(Windows 上为 node-pty 自带的 OpenConsole)转为控制台级 CTRL_C 事件;SIGTSTP 与 SIGHUP 被拒绝(不可用);不带 `/F` 的 `taskkill` 无法终止控制台进程,因此拆卸的 TERM 档是 `/F` 升级前的宽限等待。Windows 就绪没有精确的 stdin-wait 档:prompt-marker 快路径把 shell pid 作为伪前台进程组比较,其余由静默与计时档覆盖。 - **Windows 终端分配依赖 node-pty 自带的控制台宿主资产**——每个 Windows 终端都以 `useConptyDll: true` 启动,分配时按 node-pty 自身 addon 的相对位置解析 `conpty/conpty.dll` 与 `OpenConsole.exe`;资产缺失或解包错误会让 `spawnTerminal` 抛错,而不是回退到操作系统控制台宿主。node-pty 把该选项标为实验性,升级 node-pty 可能改变这一行为。见[自带控制台宿主说明](../../../.agents/notes/implemented/bug-fix/2026-09-28-pty-bundled-console-host.zh.md)。 diff --git a/packages/subprocess/subprocess-local/src/index.ts b/packages/subprocess/subprocess-local/src/index.ts index ec7bc35935..1debf9525a 100644 --- a/packages/subprocess/subprocess-local/src/index.ts +++ b/packages/subprocess/subprocess-local/src/index.ts @@ -67,7 +67,10 @@ export class LocalSubprocessRuntime extends SubprocessRuntime { internals: SpawnInternals = {} /** Provider-lifetime latch suppressing repeated weaker-containment warnings. */ private fallbackWarningIssued = false - /** Provider-lifetime result of the synchronous Linux native probe; false when native containment is unavailable. */ + /** + * Provider-lifetime result of the Linux native probe; false when native containment is unavailable. + * Each probe is a spawnSync that blocks the event loop and forks the host, so no later spawn repeats it. + */ private linuxScope: LiteralArgvOptions | false | undefined /** Test hook for platform process inspection; production resolves lazily on terminal spawn. */ terminalInspector: ProcessInspector | undefined @@ -211,7 +214,7 @@ export class LocalSubprocessRuntime extends SubprocessRuntime { return handle } - /** Run the Linux native probe on the first eligible spawn and keep its result for the provider lifetime. */ + /** Run the Linux native probe on the first eligible ordinary or terminal spawn and keep its result for the provider lifetime. */ private linuxScopeOptions(): LiteralArgvOptions | undefined { this.linuxScope ??= probeLinuxNative() ?? false return this.linuxScope === false ? undefined : this.linuxScope diff --git a/packages/subprocess/subprocess-local/tests/local.spec.ts b/packages/subprocess/subprocess-local/tests/local.spec.ts index 2bffbfcb0e..623d00ed7d 100644 --- a/packages/subprocess/subprocess-local/tests/local.spec.ts +++ b/packages/subprocess/subprocess-local/tests/local.spec.ts @@ -888,7 +888,7 @@ describe('LocalSubprocessRuntime', () => { const probeLinuxNative = vi.fn((): string[] => []) const probeWindowsJob = vi.fn(() => true) const prepareManagedProcessBinding = vi.fn(() => ({ spillDir: '/tmp/dsh-test-spill' })) - const handles = [true, false, false].map((failFirstWait) => { + const handles = [true, false, false, false].map((failFirstWait) => { let waits = 0 return { collected: {}, @@ -953,14 +953,17 @@ describe('LocalSubprocessRuntime', () => { windowsRuntime.internals = { platform: 'win32' } await windowsRuntime.spawn(spec('true')).done await new Promise(resolve => setImmediate(resolve)) - expect(probeWindowsJob).toHaveBeenCalledOnce() - expect(launchWindowsJob).toHaveBeenCalledOnce() + await windowsRuntime.spawn(spec('true')).done + await new Promise(resolve => setImmediate(resolve)) + expect(probeWindowsJob).toHaveBeenCalledTimes(2) + expect(launchWindowsJob).toHaveBeenCalledTimes(2) expect(bindManagedProcess.mock.calls.map(([, launch]) => launch)).toEqual([ linuxLaunch, linuxLaunch, windowsLaunch, + windowsLaunch, ]) - expect(prepareManagedProcessBinding).toHaveBeenCalledTimes(4) + expect(prepareManagedProcessBinding).toHaveBeenCalledTimes(5) expect(spawnSubprocess).not.toHaveBeenCalled() } finally { for (const fiber of fibers.reverse()) await fiber.dispose()