fix(subprocess): preserve literal spawn error stacks

This commit is contained in:
pku-xht
2026-08-29 13:08:43 +08:00
parent c0e7b98169
commit aaa5117ebd
4 changed files with 17 additions and 27 deletions
@@ -35,7 +35,7 @@ import {
probeLinuxNative,
} from './linux-scope.ts'
import { launchWindowsJob, probeWindowsJob } from './windows-job.ts'
import { targetEnvironment, validateTerminalTarget } from './runner-launch.ts'
import { targetEnvironment } from './runner-launch.ts'
import { createProcessInspector } from './process-inspector.ts'
import type { ProcessInspector } from './process-inspector.ts'
import { LocalTerminalHandle } from './terminal.ts'
@@ -230,7 +230,7 @@ export class LocalSubprocessRuntime extends SubprocessRuntime {
throw new Error('subprocess-local: terminal argv must contain a program')
}
spec.signal?.throwIfAborted()
const env = validateTerminalTarget(spec)
const env = targetEnvironment(spec)
const options: IPtyForkOptions = {
name: 'dumb',
rows: spec.rows,
@@ -5,7 +5,7 @@ import { accessSync, constants as fsConstants, statSync } from 'node:fs'
import { extname, isAbsolute } from 'node:path'
import { inspect } from 'node:util'
import { fileURLToPath } from 'node:url'
import type { SubprocessSpawnSpec, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import { childEnv } from './spawn.ts'
/** The one private environment variable consumed before target state is restored. */
@@ -289,12 +289,3 @@ export function targetEnvironment(
}
return env
}
/**
* Validate Linux PTY target strings before creating its request or terminal.
* @param spec - terminal subprocess request to validate.
* @returns complete validated target environment.
*/
export function validateTerminalTarget(spec: SubprocessTerminalSpawnSpec): Record<string, string> {
return targetEnvironment(spec)
}
@@ -78,7 +78,7 @@ function nodeSpawnError(
name: 'Error',
message,
...source.stack === undefined ? {} : {
stack: source.stack.replace(/^[^\n]*/, `Error: ${message}`),
stack: source.stack.replace(/^[^\n]*/, () => `Error: ${message}`),
},
code,
...errno === undefined ? {} : { errno },
@@ -42,7 +42,6 @@ import {
spawnRunnerInvocation,
SUBPROCESS_RUNNER_ENV,
targetEnvironment,
validateTerminalTarget,
WINDOWS_RUNNER_SELECTION,
} from '../src/runner-launch.ts'
import {
@@ -114,10 +113,11 @@ async function runWindows(
host: FakeRunnerHost,
native: SpawnRunnerInternals,
start: unknown = { type: 'start', cwd: 'C:\\target', env: { TARGET: 'yes', dsh_subprocess_runner: 'restored' } },
targetArgv: string[] = ['tool.exe', 'literal arg'],
): Promise<void> {
const running = runSpawnRunner(
WINDOWS_RUNNER_SELECTION,
['--', 'tool.exe', 'literal arg'],
['--', ...targetArgv],
hostArgument(host),
native,
)
@@ -276,7 +276,6 @@ describe('runner launch inputs', () => {
expect(targetEnvironment(spec)).toMatchObject({ EXPLICIT: 'yes' })
expect(targetEnvironment({ ...spec, env: { '=C:': 'C:\\target' } }))
.toMatchObject({ '=C:': 'C:\\target' })
expect(validateTerminalTarget({ ...spec, rows: 24, cols: 80 })).toMatchObject({ EXPLICIT: 'yes' })
for (const invalid of [
{ ...spec, argv: ['node\0'] },
{ ...spec, argv: ['node', 'a\0'] },
@@ -602,20 +601,20 @@ describe('Windows Job runner protocol owner', () => {
})
it('maps the bounded Win32 process-creation error classes', async () => {
for (const [win32Code, code, errno, enriched] of [
[2, 'ENOENT', -4058, true],
[3, 'ENOENT', -4058, true],
[267, 'ENOENT', -4058, true],
[740, 'EACCES', -4092, true],
[5, 'EPERM', -4048, false],
[193, 'EFTYPE', -4028, false],
[999, 'UNKNOWN', -4094, false],
for (const [win32Code, code, errno, enriched, program] of [
[2, 'ENOENT', -4058, true, 'tool.exe'],
[3, 'ENOENT', -4058, true, 'tool.exe'],
[267, 'ENOENT', -4058, true, 'tool.exe'],
[740, 'EACCES', -4092, true, '$&.exe'],
[5, 'EPERM', -4048, false, 'tool.exe'],
[193, 'EFTYPE', -4028, false, 'tool.exe'],
[999, 'UNKNOWN', -4094, false, 'tool.exe'],
] as const) {
const host = new FakeRunnerHost()
await runWindows(host, internals({
spawnCurrentTokenJobProcess: vi.fn(() => { throw new Win32Error('CreateProcessW', win32Code) }),
}))
const syscall = enriched ? 'spawn tool.exe' : 'spawn'
}), undefined, [program, 'literal arg'])
const syscall = enriched ? `spawn ${program}` : 'spawn'
expect(host.sent).toMatchObject([{
type: 'error',
error: {
@@ -630,7 +629,7 @@ describe('Windows Job runner protocol owner', () => {
if (result.type !== 'error') throw new Error('expected runner error')
expect(result.error.stack?.split('\n')[0]).toBe(`Error: ${syscall} ${code}`)
if (enriched) {
expect(result.error).toMatchObject({ path: 'tool.exe', spawnargs: ['literal arg'] })
expect(result.error).toMatchObject({ path: program, spawnargs: ['literal arg'] })
} else {
expect(result.error).not.toHaveProperty('path')
expect(result.error).not.toHaveProperty('spawnargs')