Quellcode durchsuchen

fix(subprocess): preserve Windows search miss errors

pku-xht vor 2 Wochen
Ursprung
Commit
a416b13eb5

+ 3 - 6
packages/subprocess/subprocess-local/src/runner-launch.ts

@@ -223,7 +223,7 @@ function windowsExecutableNames(command: string, name: string): string[] {
  * @param env - final target environment containing the child PATH.
  * @param exists - injectable non-directory candidate probe used by tests.
  * @param currentEnv - runner environment supplying PATH fallback and cwd-search policy.
- * @returns a resolved application name suitable for `CreateProcessW`.
+ * @returns a resolved application name suitable for `CreateProcessW`, or undefined when no candidate exists.
  */
 export function resolveWindowsExecutable(
   command: string,
@@ -231,7 +231,7 @@ export function resolveWindowsExecutable(
   env: Readonly<Record<string, string>>,
   exists: (candidate: string) => boolean = executableCandidateExists,
   currentEnv: Readonly<Record<string, string | undefined>> = process.env,
-): string {
+): string | undefined {
   const nameStart = windowsFileNameStart(command)
   const directory = command.slice(0, nameStart)
   const name = command.slice(nameStart)
@@ -254,10 +254,7 @@ export function resolveWindowsExecutable(
     }
   }
 
-  const unresolved = windowsSearchPathJoin(directory, name, cwd)
-  if (hasPath) return unresolved
-  const dot = name.indexOf('.')
-  return dot >= 0 && dot < name.length - 1 ? unresolved : `${unresolved}.exe`
+  return undefined
 }
 
 function throwNullByteError(property: string, value: string, argument: boolean): never {

+ 19 - 0
packages/subprocess/subprocess-local/src/spawn-runner.ts

@@ -90,6 +90,18 @@ function asSpawnError(error: unknown, program: string, args: readonly string[]):
   }
 }
 
+function windowsPathNotFoundError(program: string, args: readonly string[]): SerializedRunnerError {
+  return {
+    name: 'Error',
+    message: `spawn ${program} ENOENT`,
+    code: 'ENOENT',
+    errno: -4058,
+    syscall: `spawn ${program}`,
+    path: program,
+    spawnargs: [...args],
+  }
+}
+
 function linuxPathNotFoundError(program: string): NodeJS.ErrnoException {
   return Object.assign(new Error(`spawn ${program} ENOENT`), {
     code: 'ENOENT',
@@ -266,6 +278,13 @@ class WindowsJobRunner {
         undefined,
         { ...this.host.env },
       )
+      if (applicationName === undefined) {
+        await this.publishTerminalResult({
+          type: 'error',
+          error: windowsPathNotFoundError(command as string, args),
+        }, 0)
+        return
+      }
       this.api = this.internals.loadWin32ProcessBindings()
       const spawned = this.internals.spawnCurrentTokenJobProcess(this.api, {
         command: command as string,

+ 29 - 4
packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts

@@ -374,7 +374,7 @@ describe('runner launch inputs', () => {
     expect(resolveWindowsExecutable('tool.', 'C:\\target', {}, candidate =>
       candidate === 'C:\\target\\tool.exe')).toBe('C:\\target\\tool.exe')
     expect(resolveWindowsExecutable('.\\missing', 'C:\\target', {}, () => false))
-      .toBe('C:\\target\\.\\missing')
+      .toBeUndefined()
 
     expect(resolveWindowsExecutable('tool', 'C:\\target', {
       PATH: ';;C:\\bin',
@@ -395,9 +395,9 @@ describe('runner launch inputs', () => {
 
     const noSearchEnvironment = { NoDefaultCurrentDirectoryInExePath: '1' }
     expect(resolveWindowsExecutable('missing', 'C:\\target', {}, () => false, noSearchEnvironment))
-      .toBe('C:\\target\\missing.exe')
+      .toBeUndefined()
     expect(resolveWindowsExecutable('missing.cmd', 'C:\\target', {}, () => false, noSearchEnvironment))
-      .toBe('C:\\target\\missing.cmd')
+      .toBeUndefined()
 
     const directory = mkdtempSync(join(tmpdir(), 'dsh-windows-resolver-'))
     scratch.push(directory)
@@ -409,7 +409,7 @@ describe('runner launch inputs', () => {
     writeFileSync(`${directoryCandidate}.exe`, '')
     expect(resolveWindowsExecutable(executable, '', {})).toBe(executable)
     expect(resolveWindowsExecutable(directoryCandidate, '', {})).toBe(`${directoryCandidate}.exe`)
-    expect(resolveWindowsExecutable(missingExecutable, '', {})).toBe(missingExecutable)
+    expect(resolveWindowsExecutable(missingExecutable, '', {})).toBeUndefined()
   })
 })
 
@@ -554,6 +554,31 @@ describe('Linux one-shot exec bootstrap', () => {
 })
 
 describe('Windows Job runner protocol owner', () => {
+  it('publishes a Node-shaped path-search miss before loading Win32 bindings', async () => {
+    const host = new FakeRunnerHost()
+    const loadWin32ProcessBindings = vi.fn(() => ({} as CurrentTokenProcessBindings))
+    const native = internals({
+      loadWin32ProcessBindings,
+      resolveWindowsExecutable: vi.fn(() => undefined),
+    })
+    await runWindows(host, native)
+    expect(loadWin32ProcessBindings).not.toHaveBeenCalled()
+    expect(native.spawnCurrentTokenJobProcess).not.toHaveBeenCalled()
+    expect(host.sent).toEqual([{
+      type: 'error',
+      error: {
+        name: 'Error',
+        message: 'spawn tool.exe ENOENT',
+        code: 'ENOENT',
+        errno: -4058,
+        syscall: 'spawn tool.exe',
+        path: 'tool.exe',
+        spawnargs: ['literal arg'],
+      },
+    }])
+    expect(host.exitCode).toBe(0)
+  })
+
   it('maps the bounded Win32 process-creation error classes', async () => {
     for (const [win32Code, code] of [
       [3, 'ENOENT'],