fix(subprocess): hide Windows child windows

This commit is contained in:
Turtle committed 2026-09-03 17:05:18 +08:00
1 parent 100fcb286f
commit cc8099dc5f
5 files changed
+41 -6

No files matched your search

@@ -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
@@ -25,7 +25,7 @@ Mount `dsh-subprocess-local` in any composition that runs child processes on the
<a id="use-this-package"></a>
## 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
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
把提供方与它的消费方挂载在同一组合中,并完全按子进程服务的规定启动进程;本包只决定这些进程在宿主机上如何运行。
把提供方与它的消费方挂载在同一组合中,并完全按子进程服务的规定启动进程;本包只决定这些进程在宿主机上如何运行。在 Windows 上,非终端子进程会隐藏其控制台窗口,因此后台命令不会抢占焦点。
### 挂载提供方
@@ -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>): 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 <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 => {
@@ -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<typeof nodeSpawn>[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 }), {