diff --git a/packages/subprocess/subprocess-local/README.i18n.yaml b/packages/subprocess/subprocess-local/README.i18n.yaml index 76150e77d5..972689a0ea 100644 --- a/packages/subprocess/subprocess-local/README.i18n.yaml +++ b/packages/subprocess/subprocess-local/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md -README.md: dd9edbc99578f5411fad993cf88f93429dc9cab2 -README.zh.md: 7bb43f7bbce5ef6b128edc9ef2104d35a5887845 +README.md: b0c96119d6e390afb30a6339d45d4ce186cd67de +README.zh.md: 817e6ae149ea8d6c6062c7262ec374e259e530ef diff --git a/packages/subprocess/subprocess-local/README.md b/packages/subprocess/subprocess-local/README.md index dd9edbc995..b0c96119d6 100644 --- a/packages/subprocess/subprocess-local/README.md +++ b/packages/subprocess/subprocess-local/README.md @@ -25,7 +25,7 @@ Mount `dsh-subprocess-local` in any composition that runs child processes on the ## Use this package -Mount the provider beside its consumers and start processes exactly as the subprocess service specifies; this package decides only how those processes run on the host. +Mount the provider beside its consumers and start processes exactly as the subprocess service specifies; this package decides only how those processes run on the host. On Windows, non-terminal children start with their console windows hidden so background commands do not take focus. ### Mounting the provider diff --git a/packages/subprocess/subprocess-local/README.zh.md b/packages/subprocess/subprocess-local/README.zh.md index 7bb43f7bbc..817e6ae149 100644 --- a/packages/subprocess/subprocess-local/README.zh.md +++ b/packages/subprocess/subprocess-local/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -把提供方与它的消费方挂载在同一组合中,并完全按子进程服务的规定启动进程;本包只决定这些进程在宿主机上如何运行。 +把提供方与它的消费方挂载在同一组合中,并完全按子进程服务的规定启动进程;本包只决定这些进程在宿主机上如何运行。在 Windows 上,非终端子进程会隐藏其控制台窗口,因此后台命令不会抢占焦点。 ### 挂载提供方 diff --git a/packages/subprocess/subprocess-local/src/spawn.ts b/packages/subprocess/subprocess-local/src/spawn.ts index 433ba01791..bec0a0c2d2 100644 --- a/packages/subprocess/subprocess-local/src/spawn.ts +++ b/packages/subprocess/subprocess-local/src/spawn.ts @@ -7,7 +7,7 @@ * @module dsh-subprocess-local/spawn */ -import { type ChildProcess, spawn, spawnSync } from 'node:child_process' +import { type ChildProcess, type SpawnOptions, spawn, spawnSync } from 'node:child_process' import type { Readable } from 'node:stream' import { randomBytes } from 'node:crypto' import { closeSync, mkdtempSync, openSync, unlinkSync, writeSync } from 'node:fs' @@ -26,6 +26,12 @@ import type { } from '@deepseek-ai/dsh-subprocess' import { linuxProcessGroupHasLiveMembers } from './process-inspector.ts' +type SpawnProcess = ( + program: string, + args: readonly string[], + options: SpawnOptions, +) => ChildProcess + /** * Build a child environment: explicit caller entries override the scrubbed * parent base using the target platform's environment-key semantics. A string @@ -48,6 +54,8 @@ export function childEnv(extra?: Readonly): NodeJS.ProcessEnv /** Injectable knobs so tests can exercise spill and platform behavior deterministically. */ export interface SpawnInternals { + /** Node process spawner used by focused option tests. */ + spawn?: SpawnProcess /** Directory for spill files (defaults to the OS temp dir). */ spillDir?: string /** Windows tree-termination runner (defaults to `taskkill /PID /T /F`). */ @@ -329,6 +337,7 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter } const spillDir = internals.spillDir ?? privateSpillDir() const platform = internals.platform ?? process.platform + const spawnProcess = internals.spawn ?? spawn const taskkill = internals.taskkill ?? taskkillProcessTree const linuxGroupHasLiveMembers = internals.linuxProcessGroupHasLiveMembers ?? linuxProcessGroupHasLiveMembers @@ -347,7 +356,7 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter const stdinMode = spec.stdio.stdin const env = childEnv(spec.env) - const child = spawn(program, args, { + const child = spawnProcess(program, args, { cwd: spec.cwd, env, stdio: [ @@ -358,6 +367,7 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter // `detached` gives teardown a tree root on POSIX (its own process group); // Windows terminates by root pid through taskkill /T instead. detached: platform !== 'win32', + windowsHide: platform === 'win32', }) const collectStream = (mode: SubprocessOutputMode, stream: Readable | null, label: string): OutputCollector | undefined => { diff --git a/packages/subprocess/subprocess-local/tests/spawn.spec.ts b/packages/subprocess/subprocess-local/tests/spawn.spec.ts index 1f21dfaaa1..038fe1c392 100644 --- a/packages/subprocess/subprocess-local/tests/spawn.spec.ts +++ b/packages/subprocess/subprocess-local/tests/spawn.spec.ts @@ -1,3 +1,4 @@ +import { spawn as nodeSpawn } from 'node:child_process' import { mkdtempSync, readFileSync, statSync, unlinkSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' @@ -631,6 +632,30 @@ describe('stdio dispositions', () => { }) describe('windows tree semantics (injected platform)', () => { + it('hides the child window without changing output, exit, stdio, or tree-root options', async () => { + let options: Parameters[2] + const result = await finish(spawnSubprocess(spec('echo hello'), { + spillDir, + platform: 'win32', + spawn: (program, args, spawnOptions) => { + options = spawnOptions + return nodeSpawn(program, args, spawnOptions) + }, + })) + + expect(options!).toMatchObject({ + windowsHide: true, + detached: false, + stdio: ['ignore', 'pipe', 'pipe'], + }) + expect(result).toMatchObject({ + exitCode: 0, + signal: null, + stdout: { text: 'hello\n', truncated: false }, + stderr: { text: '', truncated: false }, + }) + }) + it('host-exit termination routes through taskkill immediately', async () => { const killed: number[] = [] const running = spawnSubprocess(spec('exec sleep 60', { graceMs: 60_000 }), {