Sfoglia il codice sorgente

refactor(subprocess): simplify Windows runner settlement

pku-xht 3 settimane fa
parent
commit
b03e52fab0

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.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 .agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.md
-2026-08-19-shared-win32-process-primitives.md: 67ee4527cdbb63724e371707bfa2ef19f4a86838
-2026-08-19-shared-win32-process-primitives.zh.md: 4c7a683c38c03dbc64a3fd77f33d71daf7b7a209
+2026-08-19-shared-win32-process-primitives.md: eef98440b4e1c6762f5c7f5fafa63ea77d795d39
+2026-08-19-shared-win32-process-primitives.zh.md: 795248083297eb14d45e7fa625c38954d072140c

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.md

@@ -10,17 +10,17 @@ The Windows ACL sandbox owns restricted-token, SID, DACL, grant, and workspace p
 
 ## Decision
 
-`@deepseek-ai/dsh-win32-process` owns the reusable Win32 process ABI and native resource operations consumed by `sandbox-windows-acl` and the ordinary subprocess Job runner. The package lazily loads `kernel32.dll` and `advapi32.dll`, verifies the x64 `STARTUPINFOW` and `PROCESS_INFORMATION` layouts, quotes argv for `CreateProcessAsUserW` or `CreateProcessW`, and exposes checked pipe, Job, wait, polling, termination, and handle operations.
+`@deepseek-ai/dsh-win32-process` owns the reusable Win32 process ABI and native resource operations consumed by `sandbox-windows-acl` and the ordinary subprocess Job runner. The package lazily loads `kernel32.dll` and `advapi32.dll`, verifies the x64 `STARTUPINFOW` and `PROCESS_INFORMATION` layouts, quotes argv for `CreateProcessAsUserW` or `CreateProcessW`, and exposes checked pipe, Job, wait, termination, and handle operations.
 
 The Windows ACL sandbox remains the only owner of restricted-token creation, SID and DACL policy, grants, writable-path decisions, temporary-directory policy, and the public sandbox child result. It extends the shared binding context with policy-specific APIs, supplies the primary token, combines pipe drains and waits, and closes the caller-owned Job at its lifecycle boundary.
 
-Every native allocation and HANDLE has one owner within each shared operation. A process operation frees its Koffi out-parameters and closes every pipe, thread, process, or Job handle it acquired before a controlled failure. Successful pipe creation returns the process plus stdout/stderr read handles to the sandbox. Restricted and ordinary inherited-stdio creation both start the target suspended, assign it to the kill-on-close Job, and resume it only after assignment, so target code cannot run outside the Job. The sandbox retains its existing pipe-drain and direct-wait lifecycle; the ordinary runner polls the direct process separately, while the subprocess parent owns Job accounting, termination, and closure.
+Every native allocation and HANDLE has one owner within each shared operation. A process operation frees its Koffi out-parameters and closes every pipe, thread, process, or Job handle it acquired before a controlled failure. Successful pipe creation returns the process plus stdout/stderr read handles to the sandbox. Restricted and ordinary inherited-stdio creation both start the target suspended, assign it to the kill-on-close Job, and resume it only after assignment, so target code cannot run outside the Job. The sandbox retains its existing pipe-drain and direct-wait lifecycle; the ordinary runner waits for the direct process separately, while the subprocess parent owns Job accounting, termination, and closure.
 
 The package exports only operations used by the two production consumers. Exact `applicationName`, parent-stdio release, public process handles, and backend selection remain outside. The package is a library, not a Cordis service or a public Windows SDK.
 
 ## Verification
 
-The shared suite covers x64 ABI values, command-line quoting, binding extension, pipe EOF and drain allocation reuse, restricted and ordinary process creation, suspended creation followed by Job assignment and resume, blocking and zero-time exit reads, Job-empty probes and termination, native allocation release, and the acquired-resource failure paths. Sandbox tests retain restricted-token, fail-closed, pipe/inherit, result, and disposal composition without duplicating the low-level matrix. The committed header probes and Windows package tests cover the native paths; Wine supplies the emulated Windows package and composition signal.
+The shared suite covers x64 ABI values, command-line quoting, binding extension, pipe EOF and drain allocation reuse, restricted and ordinary process creation, suspended creation followed by Job assignment and resume, blocking direct-exit reads, Job-empty probes and termination, native allocation release, and the acquired-resource failure paths. Sandbox tests retain restricted-token, fail-closed, pipe/inherit, result, and disposal composition without duplicating the low-level matrix. The committed header probes and Windows package tests cover the native paths; Wine supplies the emulated Windows package and composition signal.
 
 ## Alternatives considered
 
@@ -28,7 +28,7 @@ The shared suite covers x64 ABI values, command-line quoting, binding extension,
 
 **Copy the Koffi implementation into each consumer.** Rejected because struct layouts, error capture, and partial-failure cleanup would have multiple owners.
 
-**Publish ordinary-runner operations before a current consumer exists.** Rejected because unused operations would freeze speculative obligations. The ordinary CreateProcess, polling, and Job controls were added only with their runner consumer.
+**Publish ordinary-runner operations before a current consumer exists.** Rejected because unused operations would freeze speculative obligations. The ordinary CreateProcess, direct wait, and Job controls were added only with their runner consumer.
 
 ## Consequences
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-19-shared-win32-process-primitives.zh.md

@@ -10,17 +10,17 @@ Windows ACL sandbox 拥有 restricted token、SID、DACL、grant 与 workspace p
 
 ## Decision
 
-`@deepseek-ai/dsh-win32-process` 拥有 `sandbox-windows-acl` 与 ordinary subprocess Job runner 消费的可复用 Win32 process ABI 与 native resource 操作。该包惰性加载 `kernel32.dll` 和 `advapi32.dll`,核验 x64 `STARTUPINFOW` 与 `PROCESS_INFORMATION` 布局,为 `CreateProcessAsUserW` 或 `CreateProcessW` 引用 argv,并提供带检查的 pipe、Job、wait、polling、termination 与 handle 操作。
+`@deepseek-ai/dsh-win32-process` 拥有 `sandbox-windows-acl` 与 ordinary subprocess Job runner 消费的可复用 Win32 process ABI 与 native resource 操作。该包惰性加载 `kernel32.dll` 和 `advapi32.dll`,核验 x64 `STARTUPINFOW` 与 `PROCESS_INFORMATION` 布局,为 `CreateProcessAsUserW` 或 `CreateProcessW` 引用 argv,并提供带检查的 pipe、Job、wait、termination 与 handle 操作。
 
 Windows ACL sandbox 继续唯一拥有 restricted-token 创建、SID 与 DACL policy、grants、可写路径裁定、临时目录 policy 和公共 sandbox child result。它通过共享 binding context 扩展 policy-specific API,提供 primary token,组合 pipe drain 与 wait,并在自己的生命周期边界关闭调用方拥有的 Job。
 
-每项 native allocation 与 HANDLE 在各个 shared operation 内只有一个 owner。process operation 会释放 Koffi out-parameter,并在受控失败前关闭它已经取得的每个 pipe、thread、process 或 Job handle。pipe 创建成功时,把 process 与 stdout/stderr read handles 返回给 sandbox。restricted 与 ordinary inherited-stdio 创建都会以 suspended 状态启动目标,把它分配给 kill-on-close Job,并只在分配后恢复,因此目标代码不会在 Job 外运行。sandbox 保留既有 pipe-drain 与 direct-wait 生命周期;ordinary runner 单独轮询 direct process,而 subprocess parent 拥有 Job accounting、termination 与 closure。
+每项 native allocation 与 HANDLE 在各个 shared operation 内只有一个 owner。process operation 会释放 Koffi out-parameter,并在受控失败前关闭它已经取得的每个 pipe、thread、process 或 Job handle。pipe 创建成功时,把 process 与 stdout/stderr read handles 返回给 sandbox。restricted 与 ordinary inherited-stdio 创建都会以 suspended 状态启动目标,把它分配给 kill-on-close Job,并只在分配后恢复,因此目标代码不会在 Job 外运行。sandbox 保留既有 pipe-drain 与 direct-wait 生命周期;ordinary runner 单独等待 direct process,而 subprocess parent 拥有 Job accounting、termination 与 closure。
 
 该包只导出两个生产 consumer 已使用的操作。精确 `applicationName`、parent-stdio release、公共 process handle 与 backend selection 仍留在外部。该包是 library,不是 Cordis service 或公共 Windows SDK。
 
 ## Verification
 
-shared suite 覆盖 x64 ABI 值、命令行引用、binding extension、pipe EOF 与 drain allocation 复用、restricted 与 ordinary process 创建、suspended 创建后的 Job 分配与恢复、blocking 与 zero-time exit 读取、Job-empty probe 与 termination、native allocation 释放,以及已取得资源的失败路径。sandbox 测试保留 restricted-token、fail-closed、pipe/inherit、result 与 disposal 组合行为,不重复低层矩阵。已提交的 header probe 与 Windows package 测试覆盖 native 路径;Wine 提供模拟 Windows package 与组合信号。
+shared suite 覆盖 x64 ABI 值、命令行引用、binding extension、pipe EOF 与 drain allocation 复用、restricted 与 ordinary process 创建、suspended 创建后的 Job 分配与恢复、blocking direct-exit 读取、Job-empty probe 与 termination、native allocation 释放,以及已取得资源的失败路径。sandbox 测试保留 restricted-token、fail-closed、pipe/inherit、result 与 disposal 组合行为,不重复低层矩阵。已提交的 header probe 与 Windows package 测试覆盖 native 路径;Wine 提供模拟 Windows package 与组合信号。
 
 ## Alternatives considered
 
@@ -28,7 +28,7 @@ shared suite 覆盖 x64 ABI 值、命令行引用、binding extension、pipe EOF
 
 **为每个 consumer 复制 Koffi 实现。** 拒绝,因为 struct layout、错误捕获与局部失败清理会出现多个 owner。
 
-**在当前 consumer 出现前发布 ordinary-runner operations。** 拒绝,因为未使用的操作会冻结推测性义务。ordinary CreateProcess、polling 与 Job control 只随实际 runner consumer 一起加入。
+**在当前 consumer 出现前发布 ordinary-runner operations。** 拒绝,因为未使用的操作会冻结推测性义务。ordinary CreateProcess、direct wait 与 Job control 只随实际 runner consumer 一起加入。
 
 ## Consequences
 

+ 12 - 26
packages/subprocess/subprocess-local/src/spawn-runner.ts

@@ -5,8 +5,8 @@ import {
   closeHandleChecked,
   loadWin32ProcessBindings,
   openJobForAssignment,
-  pollProcessExit,
   spawnOrdinaryProcessInJob,
+  waitForProcessExit,
   Win32Error,
 } from '@deepseek-ai/dsh-win32-process'
 import type { NativePtr } from '@deepseek-ai/dsh-win32-process'
@@ -102,7 +102,7 @@ function replaceEnvironment(env: Record<string, string>): void {
   Object.assign(process.env, env)
 }
 
-async function runWin32(request: RunnerRequest, eventsPath: string, jobName: string): Promise<void> {
+function runWin32(request: RunnerRequest, eventsPath: string, jobName: string): void {
   replaceEnvironment(request.env)
   const api = loadWin32ProcessBindings()
   let processHandle: NativePtr | undefined
@@ -118,26 +118,10 @@ async function runWin32(request: RunnerRequest, eventsPath: string, jobName: str
     closeHandleChecked(api, jobHandle, 'ordinary process Job assignment')
     jobHandle = undefined
     appendRunnerEvent(eventsPath, { type: 'started', pid: spawned.pid })
-
-    await new Promise<void>((resolve, reject) => {
-      const timer = setInterval(() => {
-        try {
-          if (processHandle !== undefined) {
-            const exitCode = pollProcessExit(api, processHandle)
-            if (exitCode !== undefined) {
-              appendRunnerEvent(eventsPath, { type: 'exit', exitCode, signal: null })
-              closeHandleChecked(api, processHandle, 'ordinary direct process')
-              processHandle = undefined
-              clearInterval(timer)
-              resolve()
-            }
-          }
-        } catch (error) {
-          clearInterval(timer)
-          reject(error instanceof Error ? error : new Error(String(error)))
-        }
-      }, 10)
-    })
+    const directProcess = processHandle
+    processHandle = undefined
+    const exitCode = waitForProcessExit(api, directProcess)
+    appendRunnerEvent(eventsPath, { type: 'exit', exitCode, signal: null })
   } catch (error) {
     appendRunnerEvent(eventsPath, {
       type: targetStarted ? 'runner-error' : 'spawn-error',
@@ -154,7 +138,7 @@ async function runWin32(request: RunnerRequest, eventsPath: string, jobName: str
   }
 }
 
-async function main(): Promise<void> {
+function main(): void {
   const args = parseArgs(process.argv.slice(2))
   if (args.mode === 'probe-node') return
   if (args.mode === 'probe-win32') {
@@ -163,10 +147,12 @@ async function main(): Promise<void> {
   }
   const request = consumeRunnerRequest(args.requestPath)
   if (args.mode === 'node') runNode(request, args.eventsPath)
-  else await runWin32(request, args.eventsPath, args.jobName)
+  else runWin32(request, args.eventsPath, args.jobName)
 }
 
-main().catch((error: unknown) => {
+try {
+  main()
+} catch (error: unknown) {
   try {
     const args = parseArgs(process.argv.slice(2))
     if (args.mode !== 'probe-node' && args.mode !== 'probe-win32') {
@@ -176,4 +162,4 @@ main().catch((error: unknown) => {
     // No trustworthy transport remains; the parent reports the missing result.
   }
   process.exitCode = 127
-})
+}

+ 1 - 2
packages/subprocess/subprocess-local/src/windows-job.ts

@@ -50,7 +50,6 @@ export interface WindowsJobInternals {
   spawnSync?: typeof spawnSync
   runnerInvocation?: string[]
   jobs?: WindowsJobOperations
-  jobName?: () => string
 }
 
 /**
@@ -137,7 +136,7 @@ export function launchWindowsJob(
   if (command === undefined) throw new Error('subprocess-local: Windows runner invocation is empty')
   /* v8 ignore next -- the native Windows suite exercises the real Job operations. */
   const jobs = internals.jobs ?? nativeJobOperations()
-  const jobName = (internals.jobName ?? (() => `Local\\dsh-subprocess-${randomUUID()}`))()
+  const jobName = `Local\\dsh-subprocess-${randomUUID()}`
   const files = runnerFiles(spec)
   let job: NativePtr
   try {

+ 3 - 7
packages/subprocess/subprocess-local/tests/windows-job.spec.ts

@@ -67,12 +67,12 @@ describe('Windows Job runner adapter', () => {
       spawn,
       runnerInvocation: invocation,
       jobs: jobs.operations,
-      jobName: () => 'Local\\dsh-test-job',
     })
     expect(launch.pid).toBeGreaterThan(0)
     await expect(launch.direct).resolves.toEqual({ exitCode: 7, signal: null })
     await expect(launch.owner.waitForExit()).resolves.toBe(true)
-    expect(jobs.create).toHaveBeenCalledExactlyOnceWith('Local\\dsh-test-job')
+    expect(jobs.create).toHaveBeenCalledOnce()
+    expect(jobs.create.mock.calls[0]?.[0]).toMatch(/^Local\\dsh-subprocess-/u)
     expect(jobs.close).toHaveBeenCalledExactlyOnceWith(50n)
   })
 
@@ -99,7 +99,6 @@ describe('Windows Job runner adapter', () => {
       spawn: run,
       runnerInvocation: ['fake-runner'],
       jobs: jobs.operations,
-      jobName: () => 'Local\\dsh-test-job',
     })
     launch.owner.signal('SIGTERM')
     await expect(launch.direct).resolves.toEqual({ exitCode: 1, signal: null })
@@ -124,7 +123,6 @@ describe('Windows Job runner adapter', () => {
       spawn: run,
       runnerInvocation: ['fake-runner'],
       jobs: jobs.operations,
-      jobName: () => 'Local\\dsh-test-job',
     })
     const directFailure = launch.direct.catch((error: unknown) => error)
 
@@ -153,7 +151,6 @@ describe('Windows Job runner adapter', () => {
       spawn: run,
       runnerInvocation: ['fake-runner'],
       jobs: jobs.operations,
-      jobName: () => 'Local\\dsh-test-job',
     })
     void launch.direct.catch(() => {})
     launch.owner.signal('SIGTERM')
@@ -177,7 +174,6 @@ describe('Windows Job runner adapter', () => {
       spawn: run,
       runnerInvocation: ['fake-runner'],
       jobs: jobs.operations,
-      jobName: () => 'Local\\dsh-test-job',
     })
     appendRunnerEvent(eventsPath, { type: 'exit', exitCode: 0, signal: null })
     child.emit('close', 0, null)
@@ -194,7 +190,6 @@ describe('Windows Job runner adapter', () => {
       spawn: vi.fn(() => { throw failure }) as unknown as typeof spawn,
       runnerInvocation: ['fake-runner'],
       jobs: jobs.operations,
-      jobName: () => 'Local\\dsh-test-job',
     })).toThrow(failure)
     expect(jobs.close).toHaveBeenCalledExactlyOnceWith(50n)
   })
@@ -229,5 +224,6 @@ describe('Windows Job runner adapter', () => {
     await expect(launch.direct).resolves.toEqual({ exitCode: 0, signal: null })
     await expect(launch.owner.waitForExit()).resolves.toBe(true)
     expect(jobName).toMatch(/^Local\\dsh-subprocess-/u)
+    expect(jobs.create).toHaveBeenCalledExactlyOnceWith(jobName)
   })
 })

+ 2 - 2
packages/subprocess/win32-process/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/win32-process/README.md
-README.md: 83edff9e4c7cc7dab0c05d539578893ddc409a4a
-README.zh.md: 18c36f6bf425f86218bd8398ea770bb17a765eb7
+README.md: 71130d4289716e55d645743c5c9e9f6fa4cb40ca
+README.zh.md: 3de82596dffdfa264ebd31ad87517b2e8e330052

+ 2 - 2
packages/subprocess/win32-process/README.md

@@ -10,8 +10,8 @@ Low-level Win32 process library consumed by the Windows ACL sandbox and the ordi
 - **Restricted-token creation** — `RestrictedProcessSpawnOptions` requires the sandbox's primary token and uses `CreateProcessAsUserW`. Piped and inherited-stdio paths share command-line quoting, cwd, the inherited environment block, checked return values, and handle cleanup.
 - **Piped process primitive** — `spawnPipedProcess()` creates anonymous stdin/stdout/stderr pipes, closes stdin immediately, returns the two read ends, and leaves process waiting and pipe draining to the caller. Every partial failure closes the handles already owned by the operation, and every Koffi out-parameter or struct allocation is freed after its Win32 lifetime.
 - **Inherited-stdio Job primitive** — `spawnInheritedJobProcess()` creates one kill-on-close Job, temporarily marks the current stdio handles inheritable, creates the restricted child suspended, assigns it to the Job, and then resumes its initial thread. Target code cannot run before Job assignment; controlled assignment or resume failures terminate the suspended child or close the assigned Job before releasing every owned handle.
-- **Ordinary Job runner primitives** — the parent creates a named kill-on-close Job, the runner opens it for assignment, and `spawnOrdinaryProcessInJob()` applies the suspended-create, Job-assignment, and resume lifecycle through `CreateProcessW`. A zero-time process wait lets the runner publish direct exit, while the parent polls `QueryInformationJobObject(JobObjectBasicAccountingInformation)` until `ActiveProcesses` reaches zero.
-- **Explicit settlement ownership** — `waitForProcessExit()` waits and closes a sandbox process handle; ordinary runner process polling and parent-owned Job accounting, termination, and closure remain separate operations. `drainPipe()` reuses one native count slot while draining, frees it, and closes the pipe read handle. Each caller owns its result composition and returned handles.
+- **Ordinary Job runner primitives** — the parent creates a named kill-on-close Job, the runner opens it for assignment, and `spawnOrdinaryProcessInJob()` applies the suspended-create, Job-assignment, and resume lifecycle through `CreateProcessW`. A blocking process wait inside the isolated runner publishes direct exit, while the parent polls `QueryInformationJobObject(JobObjectBasicAccountingInformation)` until `ActiveProcesses` reaches zero.
+- **Explicit settlement ownership** — `waitForProcessExit()` waits for and closes a sandbox or ordinary-runner process handle; parent-owned Job accounting, termination, and closure remain separate operations. `drainPipe()` reuses one native count slot while draining, frees it, and closes the pipe read handle. Each caller owns its result composition and returned handles.
 
 The Windows ACL sandbox adds SID, DACL, grant, workspace, and public child policy above these primitives.
 

+ 2 - 2
packages/subprocess/win32-process/README.zh.md

@@ -10,8 +10,8 @@
 - **restricted-token 创建** — `RestrictedProcessSpawnOptions` 要求 sandbox 的 primary token,并使用 `CreateProcessAsUserW`。pipe 与 inherited-stdio 路径共用命令行引用、cwd、继承环境块、返回值检查与句柄清理。
 - **管道进程原语** — `spawnPipedProcess()` 创建匿名 stdin/stdout/stderr 管道,立即关闭 stdin,并返回两个读取端;调用方负责等待进程与排空管道。任一局部失败都会关闭该操作已经拥有的句柄,并在各自 Win32 生命周期结束后释放每个 Koffi 输出槽与结构体分配。
 - **继承 stdio 的 Job 原语** — `spawnInheritedJobProcess()` 创建一个 kill-on-close Job,临时把当前 stdio 句柄设为可继承,以 suspended 状态创建 restricted child,把它分配给 Job,再恢复初始线程。目标代码不会在 Job 分配前运行;受控的分配或恢复失败会终止 suspended child,或在释放全部已拥有句柄前关闭已分配的 Job。
-- **ordinary Job runner 原语** — parent 创建 named kill-on-close Job,runner 以 assignment 权限打开该 Job,`spawnOrdinaryProcessInJob()` 再通过 `CreateProcessW` 应用 suspended-create、Job-assignment 与 resume 生命周期。process 的 zero-time wait 让 runner 单独发布 direct exit,parent 则轮询 `QueryInformationJobObject(JobObjectBasicAccountingInformation)` 直到 `ActiveProcesses` 归零。
-- **显式结算归属** — `waitForProcessExit()` 等待并关闭 sandbox process handle;ordinary runner 的 process polling 与 parent-owned Job 的 accounting、termination、closure 是独立操作。`drainPipe()` 在排空期间复用一个 native count slot,释放该分配并关闭管道读取句柄。每个调用方拥有自己的 result 组合与返回 handle。
+- **ordinary Job runner 原语** — parent 创建 named kill-on-close Job,runner 以 assignment 权限打开该 Job,`spawnOrdinaryProcessInJob()` 再通过 `CreateProcessW` 应用 suspended-create、Job-assignment 与 resume 生命周期。隔离 runner 内的 blocking process wait 会发布 direct exit,parent 则轮询 `QueryInformationJobObject(JobObjectBasicAccountingInformation)` 直到 `ActiveProcesses` 归零。
+- **显式结算归属** — `waitForProcessExit()` 等待并关闭 sandbox 或 ordinary runner 的 process handle;parent-owned Job 的 accounting、termination 与 closure 仍是独立操作。`drainPipe()` 在排空期间复用一个 native count slot,释放该分配并关闭管道读取句柄。每个调用方拥有自己的 result 组合与返回 handle。
 
 Windows ACL 沙箱在这些原语上增加 SID、DACL、grant、workspace 与公共 child policy。
 

+ 0 - 2
packages/subprocess/win32-process/src/abi.ts

@@ -6,8 +6,6 @@ export const STARTF_USESTDHANDLES = 0x00000100
 export const HANDLE_FLAG_INHERIT = 0x1
 /** Infinite WaitForSingleObject timeout. */
 export const INFINITE = 0xFFFFFFFF
-/** WaitForSingleObject returned because a zero-time probe is not signalled. */
-export const WAIT_TIMEOUT = 258
 /** CreateProcess flag that prevents user code from running before resume. */
 export const CREATE_SUSPENDED = 0x4
 /** GetStdHandle selector for standard input. */

+ 0 - 1
packages/subprocess/win32-process/src/index.ts

@@ -23,7 +23,6 @@ export {
   drainPipe,
   isJobEmpty,
   openJobForAssignment,
-  pollProcessExit,
   spawnInheritedJobProcess,
   spawnOrdinaryProcessInJob,
   spawnPipedProcess,

+ 0 - 19
packages/subprocess/win32-process/src/process.ts

@@ -502,25 +502,6 @@ export function spawnOrdinaryProcessInJob(
     ))
 }
 
-/**
- * Poll one process handle without blocking the runner event loop.
- * @param api - active binding table.
- * @param process - caller-owned process handle.
- * @returns the direct exit code when signalled, or undefined while running.
- */
-export function pollProcessExit(api: Win32ProcessBindings, process: NativePtr): number | undefined {
-  const waitResult = api.waitForSingleObject(process, 0)
-  if (waitResult === abi.WAIT_TIMEOUT) return undefined
-  if (waitResult === 0xFFFFFFFF) throwLastError(api, 'WaitForSingleObject')
-  const exitCodeSlot = allocUint32()
-  try {
-    if (api.getExitCodeProcess(process, exitCodeSlot) === 0) throwLastError(api, 'GetExitCodeProcess')
-    return decodeUint32(exitCodeSlot)
-  } finally {
-    koffi.free(exitCodeSlot)
-  }
-}
-
 /**
  * Return whether a Job has no active processes.
  * @param api - active binding table.

+ 4 - 19
packages/subprocess/win32-process/tests/ordinary-process.spec.ts

@@ -5,7 +5,6 @@ import {
   createKillOnCloseJob,
   isJobEmpty,
   openJobForAssignment,
-  pollProcessExit,
   spawnOrdinaryProcessInJob,
   terminateJob,
   Win32Error,
@@ -16,7 +15,6 @@ import {
   JOBOBJECT_BASIC_ACCOUNTING_ACTIVE_PROCESSES_OFFSET,
   JOBOBJECT_BASIC_ACCOUNTING_SIZE,
   JobObjectBasicAccountingInformation,
-  WAIT_TIMEOUT,
 } from '../src/abi.ts'
 import { PROCESS_INFORMATION } from '../src/ffi.ts'
 import type { NativePtr, Win32ProcessBindings } from '../src/index.ts'
@@ -138,16 +136,12 @@ describe('ordinary Job process operations', () => {
     expect(closeHandle).not.toHaveBeenCalledWith(50n)
   })
 
-  it('polls direct exit and Job emptiness without blocking', () => {
+  it('reads Job emptiness without blocking', () => {
     const queryInformationJobObject = vi.fn((_job: NativePtr, _cls: number, information: Buffer) => {
       information.writeUInt32LE(1, JOBOBJECT_BASIC_ACCOUNTING_ACTIVE_PROCESSES_OFFSET)
       return 1
     })
-    const running = api({
-      waitForSingleObject: vi.fn(() => WAIT_TIMEOUT),
-      queryInformationJobObject,
-    })
-    expect(pollProcessExit(running, 60n as NativePtr)).toBeUndefined()
+    const running = api({ queryInformationJobObject })
     expect(isJobEmpty(running, 50n as NativePtr)).toBe(false)
     expect(queryInformationJobObject).toHaveBeenCalledWith(
       50n,
@@ -156,19 +150,10 @@ describe('ordinary Job process operations', () => {
       JOBOBJECT_BASIC_ACCOUNTING_SIZE,
       null,
     )
-
-    const exited = api()
-    expect(pollProcessExit(exited, 60n as NativePtr)).toBe(42)
-    expect(isJobEmpty(exited, 50n as NativePtr)).toBe(true)
+    expect(isJobEmpty(api(), 50n as NativePtr)).toBe(true)
   })
 
-  it('reports wait and exit-code query failures', () => {
-    const processWait = api({ waitForSingleObject: vi.fn(() => 0xFFFFFFFF) })
-    expect(() => pollProcessExit(processWait, 60n as NativePtr)).toThrow(Win32Error)
-
-    const exitCode = api({ getExitCodeProcess: vi.fn(() => 0) })
-    expect(() => pollProcessExit(exitCode, 60n as NativePtr)).toThrow(Win32Error)
-
+  it('reports a Job accounting query failure', () => {
     const jobQuery = api({ queryInformationJobObject: vi.fn(() => 0) })
     expect(() => isJobEmpty(jobQuery, 50n as NativePtr)).toThrow(Win32Error)
   })

+ 0 - 2
packages/subprocess/win32-process/verify/abi-probe.cpp

@@ -21,7 +21,6 @@ int wmain()
   P(STARTF_USESTDHANDLES);
   P(HANDLE_FLAG_INHERIT);
   P(INFINITE);
-  P(WAIT_TIMEOUT);
   P(STD_INPUT_HANDLE);
   P(STD_OUTPUT_HANDLE);
   P(STD_ERROR_HANDLE);
@@ -43,7 +42,6 @@ int wmain()
   static_assert(CREATE_SUSPENDED == 0x4, "suspended process flag");
   static_assert(STARTF_USESTDHANDLES == 0x100, "std handles flag");
   static_assert(HANDLE_FLAG_INHERIT == 0x1, "inherit flag");
-  static_assert(WAIT_TIMEOUT == 258, "zero-time wait timeout");
   static_assert(sizeof(JOBOBJECT_BASIC_ACCOUNTING_INFORMATION) == 48, "job accounting size");
   static_assert(offsetof(JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, ActiveProcesses) == 40, "active process offset");
   static_assert(JobObjectBasicAccountingInformation == 1, "basic accounting class");