fix(subprocess): hide Windows child windows
This commit is contained in:
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 }), {
|
||||
|
||||
Reference in new issue
Block a user