process.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. /** Typed Win32 process operations over the shared binding table. */
  2. import koffi from 'koffi'
  3. import * as abi from './abi.ts'
  4. import { inheritedControlStdio } from './control-stdio.ts'
  5. import {
  6. allocProcessInfo,
  7. allocPtrSlot,
  8. allocStartupInfo,
  9. allocUint32,
  10. decodeProcessInfo,
  11. decodePtr,
  12. decodeUint32,
  13. encodeStartupInfo,
  14. isNullPtr,
  15. throwLastError,
  16. throwWin32,
  17. } from './ffi.ts'
  18. import type { CurrentTokenProcessBindings, NativePtr, Win32ProcessBindings } from './ffi.ts'
  19. /**
  20. * Quote one argument according to CommandLineToArgvW parsing.
  21. * @param argument - one argv entry.
  22. * @returns bare or quoted command-line segment.
  23. */
  24. export function quoteArg(argument: string): string {
  25. if (argument === '') return '""'
  26. if (!/[\s"]/u.test(argument)) return argument
  27. let quoted = '"'
  28. for (let index = 0; index < argument.length; index++) {
  29. let backslashes = 0
  30. while (index < argument.length && argument.charAt(index) === '\\') {
  31. backslashes += 1
  32. index += 1
  33. }
  34. if (index === argument.length) {
  35. quoted += '\\'.repeat(backslashes * 2)
  36. } else if (argument.charAt(index) === '"') {
  37. quoted += '\\'.repeat(backslashes * 2 + 1) + '"'
  38. } else {
  39. quoted += '\\'.repeat(backslashes) + argument.charAt(index)
  40. }
  41. }
  42. return quoted + '"'
  43. }
  44. /**
  45. * Build the mutable command line accepted by CreateProcessAsUserW.
  46. * @param program - executable argv entry.
  47. * @param args - remaining argv entries.
  48. * @returns joined Win32 command line.
  49. */
  50. export function buildCommandLine(program: string, args: readonly string[]): string {
  51. return [program, ...args].map(quoteArg).join(' ')
  52. }
  53. function compareWindowsEnvironmentKeys(
  54. [left]: readonly [string, string],
  55. [right]: readonly [string, string],
  56. ): number {
  57. const foldedLeft = left.toUpperCase()
  58. const foldedRight = right.toUpperCase()
  59. return foldedLeft < foldedRight ? -1 : foldedLeft > foldedRight ? 1 : 0
  60. }
  61. function encodeWindowsEnvironment(env: Readonly<Record<string, string>>): Buffer {
  62. const entries = Object.entries(env).sort(compareWindowsEnvironmentKeys)
  63. const strings = entries.map(([key, value]) => `${key}=${value}`)
  64. return Buffer.from(`${strings.join('\0')}\0\0`, 'utf16le')
  65. }
  66. interface ProcessSpawnOptions {
  67. /** Executable argv entry passed through CreateProcess. */
  68. command: string
  69. /** Arguments excluding the executable. */
  70. args: readonly string[]
  71. /** Existing child working directory. */
  72. cwd: string
  73. }
  74. /** Ordinary process creation inputs used by the local Win32 runner. */
  75. export interface CurrentTokenProcessSpawnOptions extends ProcessSpawnOptions {
  76. /** Resolved executable path passed separately from the preserved argv entry. */
  77. applicationName: string
  78. /** Complete target environment passed without mutating the runner. */
  79. env: Readonly<Record<string, string>>
  80. /** Runner CRT descriptors carrying target stdin, stdout, and stderr. */
  81. stdio: CurrentTokenStdioFileDescriptors
  82. }
  83. /** Runner CRT descriptors whose OS handles become the target standard handles. */
  84. export interface CurrentTokenStdioFileDescriptors {
  85. stdin: number
  86. stdout: number
  87. stderr: number
  88. /** Optional carrier and target descriptor for the inherited control pipe. */
  89. control?: 7
  90. }
  91. /** Restricted-token process creation inputs owned by the Windows ACL sandbox. */
  92. export interface RestrictedProcessSpawnOptions extends ProcessSpawnOptions {
  93. /** Restricted primary token supplied by sandbox policy. */
  94. token: NativePtr
  95. /** Optional control pipe inherited at the same descriptor in the payload. */
  96. controlFileDescriptor?: 7
  97. }
  98. /** Piped child resources whose process and read handles remain caller-owned. */
  99. export interface SpawnedPipedProcess {
  100. /** Direct child process id. */
  101. pid: number
  102. /** Process handle closed by waitForProcessExit. */
  103. process: NativePtr
  104. /** Stdout pipe read end closed by drainPipe. */
  105. stdoutRead: NativePtr
  106. /** Stderr pipe read end closed by drainPipe. */
  107. stderrRead: NativePtr
  108. }
  109. /** Suspended child assigned to one caller-owned kill-on-close Job before resume. */
  110. export interface SpawnedJobProcess {
  111. /** Direct child process id. */
  112. pid: number
  113. /** Process handle closed by waitForProcessExit. */
  114. process: NativePtr
  115. /** Job handle closed by the lifecycle owner. */
  116. job: NativePtr
  117. }
  118. interface PipePair {
  119. read: NativePtr
  120. write: NativePtr
  121. }
  122. function freeNative(pointer: NativePtr | undefined): void {
  123. if (pointer !== undefined) koffi.free(pointer)
  124. }
  125. function closeBestEffort(api: Win32ProcessBindings, handle: NativePtr | null | undefined): void {
  126. if (!isNullPtr(handle)) api.closeHandle(handle)
  127. }
  128. function createPipe(api: Win32ProcessBindings, owned: Set<NativePtr>): PipePair {
  129. const readSlot = allocPtrSlot()
  130. let writeSlot: NativePtr | undefined
  131. try {
  132. writeSlot = allocPtrSlot()
  133. if (api.createPipe(readSlot, writeSlot, null, 0) === 0) throwLastError(api, 'CreatePipe')
  134. const read = decodePtr(readSlot)
  135. const write = decodePtr(writeSlot)
  136. if (read === null || write === null) {
  137. closeBestEffort(api, read)
  138. closeBestEffort(api, write)
  139. throwLastError(api, 'CreatePipe', 'null pipe handle')
  140. }
  141. owned.add(read)
  142. owned.add(write)
  143. return { read, write }
  144. } finally {
  145. freeNative(writeSlot)
  146. koffi.free(readSlot)
  147. }
  148. }
  149. function closeOwned(api: Win32ProcessBindings, owned: Set<NativePtr>, handle: NativePtr): void {
  150. /* v8 ignore next -- each successfully decoded pipe end is uniquely owned. */
  151. if (!owned.delete(handle)) return
  152. api.closeHandle(handle)
  153. }
  154. function closeAllOwned(api: Win32ProcessBindings, owned: Set<NativePtr>): void {
  155. for (const handle of owned) api.closeHandle(handle)
  156. owned.clear()
  157. }
  158. function createRestrictedProcess(
  159. api: Win32ProcessBindings,
  160. options: RestrictedProcessSpawnOptions,
  161. commandLine: string,
  162. creationFlags: number,
  163. startupInfo: NativePtr,
  164. processInfo: NativePtr,
  165. ): number {
  166. // The sandbox mutates its process environment before this call. Passing an
  167. // explicit block through Koffi makes CreateProcessAsUserW reject the request
  168. // with ERROR_INVALID_PARAMETER, so lpEnvironment remains NULL.
  169. return api.createProcessAsUserW(
  170. options.token,
  171. null,
  172. commandLine,
  173. null,
  174. null,
  175. 1,
  176. creationFlags,
  177. null,
  178. options.cwd,
  179. startupInfo,
  180. processInfo,
  181. )
  182. }
  183. /**
  184. * Spawn a process with anonymous-pipe stdout/stderr and immediate stdin EOF.
  185. * @param api - active binding table.
  186. * @param options - command, cwd, args, and restricted primary token.
  187. * @returns caller-owned process and pipe read handles.
  188. */
  189. export function spawnPipedProcess(
  190. api: Win32ProcessBindings,
  191. options: RestrictedProcessSpawnOptions,
  192. ): SpawnedPipedProcess {
  193. const owned = new Set<NativePtr>()
  194. let startupInfo: NativePtr | undefined
  195. let processInfo: NativePtr | undefined
  196. try {
  197. const stdIn = createPipe(api, owned)
  198. const stdOut = createPipe(api, owned)
  199. const stdErr = createPipe(api, owned)
  200. for (const [handle, label] of [
  201. [stdIn.read, 'stdin read end'],
  202. [stdOut.write, 'stdout write end'],
  203. [stdErr.write, 'stderr write end'],
  204. ] as const) {
  205. if (api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, abi.HANDLE_FLAG_INHERIT) === 0) {
  206. throwLastError(api, 'SetHandleInformation', label)
  207. }
  208. }
  209. startupInfo = allocStartupInfo()
  210. encodeStartupInfo(startupInfo, {
  211. cb: abi.STARTUPINFOW_SIZE,
  212. dwFlags: abi.STARTF_USESTDHANDLES,
  213. hStdInput: stdIn.read,
  214. hStdOutput: stdOut.write,
  215. hStdError: stdErr.write,
  216. })
  217. processInfo = allocProcessInfo()
  218. const created = createRestrictedProcess(
  219. api,
  220. options,
  221. buildCommandLine(options.command, options.args),
  222. 0,
  223. startupInfo,
  224. processInfo,
  225. )
  226. if (created === 0) {
  227. const win32Code = api.getLastError()
  228. throwWin32(api, 'CreateProcessAsUserW', win32Code, `command: ${options.command}, cwd: ${options.cwd}`)
  229. }
  230. const info = decodeProcessInfo(processInfo)
  231. if (info.hProcess === null || info.hThread === null) {
  232. if (info.hProcess !== null) api.terminateProcess(info.hProcess, 1)
  233. closeBestEffort(api, info.hThread)
  234. closeBestEffort(api, info.hProcess)
  235. throw new Error(`CreateProcessAsUserW succeeded but returned null process/thread handles (pid ${info.dwProcessId})`)
  236. }
  237. closeOwned(api, owned, stdIn.read)
  238. closeOwned(api, owned, stdIn.write)
  239. closeOwned(api, owned, stdOut.write)
  240. closeOwned(api, owned, stdErr.write)
  241. closeBestEffort(api, info.hThread)
  242. owned.delete(stdOut.read)
  243. owned.delete(stdErr.read)
  244. return {
  245. pid: info.dwProcessId,
  246. process: info.hProcess,
  247. stdoutRead: stdOut.read,
  248. stderrRead: stdErr.read,
  249. }
  250. } catch (error) {
  251. closeAllOwned(api, owned)
  252. throw error
  253. } finally {
  254. freeNative(processInfo)
  255. freeNative(startupInfo)
  256. }
  257. }
  258. /**
  259. * Drain one anonymous pipe until the writer closes it.
  260. * @param api - active binding table.
  261. * @param handle - caller-owned pipe read end.
  262. * @returns complete bytes read before EOF; the handle is always closed.
  263. * @throws when a Win32 pipe operation fails.
  264. */
  265. export async function drainPipe(
  266. api: Win32ProcessBindings,
  267. handle: NativePtr,
  268. ): Promise<Buffer> {
  269. const chunks: Buffer[] = []
  270. let countSlot: NativePtr | undefined
  271. try {
  272. countSlot = allocUint32()
  273. for (;;) {
  274. const peeked = api.peekNamedPipe(handle, null, 0, null, countSlot, null)
  275. if (peeked === 0) {
  276. const win32Code = api.getLastError()
  277. if (win32Code === abi.ERROR_BROKEN_PIPE || win32Code === abi.ERROR_NO_DATA) break
  278. throwLastError(api, 'PeekNamedPipe', `drain failure after ${chunks.length} chunk(s)`)
  279. }
  280. const available = decodeUint32(countSlot)
  281. if (available > 0) {
  282. const chunk = Buffer.alloc(available)
  283. if (api.readFile(handle, chunk, chunk.length, countSlot, null) === 0) {
  284. throwLastError(api, 'ReadFile', `drain failure after ${chunks.length} chunk(s)`)
  285. }
  286. chunks.push(chunk.subarray(0, decodeUint32(countSlot)))
  287. }
  288. await new Promise<void>(resolve => setTimeout(resolve, 1))
  289. }
  290. return Buffer.concat(chunks)
  291. } finally {
  292. freeNative(countSlot)
  293. api.closeHandle(handle)
  294. }
  295. }
  296. /**
  297. * Wait for a process and always close its handle.
  298. * @param api - active binding table.
  299. * @param process - caller-owned process handle.
  300. * @returns direct process exit code.
  301. */
  302. export function waitForProcessExit(api: Win32ProcessBindings, process: NativePtr): number {
  303. let exitCodeSlot: NativePtr | undefined
  304. try {
  305. if (api.waitForSingleObject(process, abi.INFINITE) === 0xFFFFFFFF) {
  306. throwLastError(api, 'WaitForSingleObject')
  307. }
  308. exitCodeSlot = allocUint32()
  309. if (api.getExitCodeProcess(process, exitCodeSlot) === 0) throwLastError(api, 'GetExitCodeProcess')
  310. return decodeUint32(exitCodeSlot)
  311. } finally {
  312. freeNative(exitCodeSlot)
  313. api.closeHandle(process)
  314. }
  315. }
  316. function createKillOnCloseJob(api: Win32ProcessBindings): NativePtr {
  317. const job = api.createJobObjectW(null, null)
  318. if (isNullPtr(job)) throwLastError(api, 'CreateJobObjectW')
  319. const information = Buffer.alloc(abi.JOBOBJECT_EXTENDED_LIMIT_SIZE)
  320. information.writeUInt32LE(
  321. abi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
  322. abi.JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET,
  323. )
  324. if (api.setInformationJobObject(
  325. job,
  326. abi.JobObjectExtendedLimitInformation,
  327. information,
  328. information.length,
  329. ) === 0) {
  330. const win32Code = api.getLastError()
  331. api.closeHandle(job)
  332. throwWin32(api, 'SetInformationJobObject', win32Code)
  333. }
  334. return job
  335. }
  336. interface ProcessStandardHandles {
  337. stdin: NativePtr
  338. stdout: NativePtr
  339. stderr: NativePtr
  340. control?: { fileDescriptor: 7; handle: NativePtr }
  341. }
  342. // Koffi exposes PVOID as an unsigned 64-bit bigint on supported Windows hosts.
  343. const UV_INVALID_OS_FILE_HANDLE = 0xffff_ffff_ffff_ffffn
  344. const UV_INVALID_FILE_DESCRIPTOR = 0xffff_ffff_ffff_fffen
  345. function inheritedStandardHandles(api: Win32ProcessBindings, controlFileDescriptor?: 7): ProcessStandardHandles {
  346. const get = (selector: number, label: string): NativePtr => {
  347. const handle = api.getStdHandle(selector)
  348. if (!isNullPtr(handle)) return handle
  349. throwLastError(api, 'GetStdHandle', `null ${label} handle`)
  350. }
  351. return {
  352. stdin: get(abi.STD_INPUT_HANDLE, 'stdin'),
  353. stdout: get(abi.STD_OUTPUT_HANDLE, 'stdout'),
  354. stderr: get(abi.STD_ERROR_HANDLE, 'stderr'),
  355. ...controlFileDescriptor === undefined ? {} : {
  356. control: { fileDescriptor: controlFileDescriptor, handle: descriptorHandle(api, controlFileDescriptor, 'control') },
  357. },
  358. }
  359. }
  360. function descriptorHandle(api: Win32ProcessBindings, fileDescriptor: number, label: string): NativePtr {
  361. const handle = api.uvGetOsfhandle(fileDescriptor)
  362. if (
  363. isNullPtr(handle)
  364. || handle === UV_INVALID_OS_FILE_HANDLE
  365. || handle === UV_INVALID_FILE_DESCRIPTOR
  366. ) {
  367. throw new Error(`uv_get_osfhandle returned an invalid handle for target ${label} fd ${String(fileDescriptor)}`)
  368. }
  369. return handle
  370. }
  371. function targetCarrierHandles(
  372. api: CurrentTokenProcessBindings,
  373. descriptors: CurrentTokenStdioFileDescriptors,
  374. ): ProcessStandardHandles {
  375. return {
  376. stdin: descriptorHandle(api, descriptors.stdin, 'stdin'),
  377. stdout: descriptorHandle(api, descriptors.stdout, 'stdout'),
  378. stderr: descriptorHandle(api, descriptors.stderr, 'stderr'),
  379. ...descriptors.control === undefined ? {} : {
  380. control: { fileDescriptor: descriptors.control, handle: descriptorHandle(api, descriptors.control, 'control') },
  381. },
  382. }
  383. }
  384. /** Shared suspended-create, Job-assignment, and resume lifecycle. */
  385. function spawnJobProcess(
  386. api: Win32ProcessBindings,
  387. options: ProcessSpawnOptions,
  388. resolveStdio: () => ProcessStandardHandles,
  389. createName: 'CreateProcessAsUserW' | 'CreateProcessW',
  390. create: (startupInfo: NativePtr, processInfo: NativePtr) => number,
  391. ): SpawnedJobProcess {
  392. const job = createKillOnCloseJob(api)
  393. const enabled: NativePtr[] = []
  394. let startupInfo: NativePtr | undefined
  395. let processInfo: NativePtr | undefined
  396. let controlDescriptorBlock: { pointer: NativePtr; length: number } | undefined
  397. let created = 0
  398. let createFailureCode = 0
  399. try {
  400. const stdio = resolveStdio()
  401. const inherited: Array<readonly [NativePtr, string]> = [
  402. [stdio.stdin, 'stdin'],
  403. [stdio.stdout, 'stdout'],
  404. [stdio.stderr, 'stderr'],
  405. ]
  406. if (stdio.control !== undefined) inherited.push([stdio.control.handle, 'control'])
  407. for (const [handle, label] of inherited) {
  408. if (api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, abi.HANDLE_FLAG_INHERIT) === 0) {
  409. throwLastError(api, 'SetHandleInformation', `${label} (enable inherit)`)
  410. }
  411. enabled.push(handle)
  412. }
  413. const controlBytes = stdio.control === undefined
  414. ? undefined
  415. : inheritedControlStdio(api, { ...stdio, control: stdio.control })
  416. if (controlBytes !== undefined) {
  417. controlDescriptorBlock = { pointer: koffi.alloc('uint8', controlBytes.length) as NativePtr, length: controlBytes.length }
  418. koffi.encode(controlDescriptorBlock.pointer, 'uint8', controlBytes, controlBytes.length)
  419. }
  420. startupInfo = allocStartupInfo()
  421. encodeStartupInfo(startupInfo, {
  422. cb: abi.STARTUPINFOW_SIZE,
  423. dwFlags: abi.STARTF_USESTDHANDLES,
  424. hStdInput: stdio.stdin,
  425. hStdOutput: stdio.stdout,
  426. hStdError: stdio.stderr,
  427. ...controlDescriptorBlock === undefined ? {} : {
  428. cbReserved2: controlDescriptorBlock.length,
  429. lpReserved2: controlDescriptorBlock.pointer,
  430. },
  431. })
  432. processInfo = allocProcessInfo()
  433. created = create(startupInfo, processInfo)
  434. if (created === 0) createFailureCode = api.getLastError()
  435. } catch (error) {
  436. freeNative(processInfo)
  437. api.closeHandle(job)
  438. throw error
  439. } finally {
  440. freeNative(startupInfo)
  441. freeNative(controlDescriptorBlock?.pointer)
  442. for (const handle of enabled) {
  443. // The runner spawns nothing else; cleanup failure must not mask the child.
  444. api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, 0)
  445. }
  446. }
  447. if (created === 0) {
  448. freeNative(processInfo)
  449. api.closeHandle(job)
  450. throwWin32(
  451. api,
  452. createName,
  453. createFailureCode,
  454. `command: ${options.command}, cwd: ${options.cwd}`,
  455. )
  456. }
  457. let info: ReturnType<typeof decodeProcessInfo>
  458. try {
  459. info = decodeProcessInfo(processInfo)
  460. } finally {
  461. freeNative(processInfo)
  462. }
  463. if (info.hProcess === null || info.hThread === null) {
  464. if (info.hProcess !== null) api.terminateProcess(info.hProcess, 1)
  465. api.closeHandle(job)
  466. closeBestEffort(api, info.hThread)
  467. closeBestEffort(api, info.hProcess)
  468. throw new Error(`${createName} succeeded but returned null process/thread handles (pid ${info.dwProcessId})`)
  469. }
  470. if (api.assignProcessToJobObject(job, info.hProcess) === 0) {
  471. const win32Code = api.getLastError()
  472. api.terminateProcess(info.hProcess, 1)
  473. closeBestEffort(api, info.hThread)
  474. closeBestEffort(api, info.hProcess)
  475. api.closeHandle(job)
  476. throwWin32(api, 'AssignProcessToJobObject', win32Code, `pid ${info.dwProcessId}`)
  477. }
  478. if (api.resumeThread(info.hThread) === 0xFFFFFFFF) {
  479. const win32Code = api.getLastError()
  480. closeBestEffort(api, info.hThread)
  481. closeBestEffort(api, info.hProcess)
  482. api.closeHandle(job)
  483. throwWin32(api, 'ResumeThread', win32Code, `pid ${info.dwProcessId}`)
  484. }
  485. closeBestEffort(api, info.hThread)
  486. return { pid: info.dwProcessId, process: info.hProcess, job }
  487. }
  488. /**
  489. * Spawn a restricted-token process suspended, assign its Job, then resume it.
  490. * @param api - active binding table.
  491. * @param options - command, cwd, args, and restricted primary token.
  492. * @returns caller-owned process and Job handles after successful resume.
  493. * @remarks Node clears stdio handle inheritability at startup through
  494. * uv_disable_stdio_inheritance. This operation temporarily restores the bits
  495. * required by STARTF_USESTDHANDLES. Restoring them afterward is best-effort:
  496. * failure must not replace the already-created child's outcome.
  497. */
  498. export function spawnInheritedJobProcess(
  499. api: Win32ProcessBindings,
  500. options: RestrictedProcessSpawnOptions,
  501. ): SpawnedJobProcess {
  502. const commandLine = buildCommandLine(options.command, options.args)
  503. return spawnJobProcess(api, options, () => inheritedStandardHandles(api, options.controlFileDescriptor), 'CreateProcessAsUserW', (startupInfo, processInfo) =>
  504. createRestrictedProcess(
  505. api,
  506. options,
  507. commandLine,
  508. abi.CREATE_SUSPENDED,
  509. startupInfo,
  510. processInfo,
  511. ))
  512. }
  513. /**
  514. * Spawn an ordinary process suspended, assign its Job, then resume it.
  515. * @param api - active binding table.
  516. * @param options - command, cwd, argv, and target carrier descriptors.
  517. * @returns caller-owned process and Job handles after successful resume.
  518. */
  519. export function spawnCurrentTokenJobProcess(
  520. api: CurrentTokenProcessBindings,
  521. options: CurrentTokenProcessSpawnOptions,
  522. ): SpawnedJobProcess {
  523. const commandLine = buildCommandLine(options.command, options.args)
  524. const environment = encodeWindowsEnvironment(options.env)
  525. return spawnJobProcess(api, options, () => targetCarrierHandles(api, options.stdio), 'CreateProcessW', (startupInfo, processInfo) =>
  526. api.createProcessW(
  527. options.applicationName,
  528. commandLine,
  529. null,
  530. null,
  531. 1,
  532. abi.CREATE_SUSPENDED | abi.CREATE_UNICODE_ENVIRONMENT,
  533. environment,
  534. options.cwd,
  535. startupInfo,
  536. processInfo,
  537. ))
  538. }
  539. /**
  540. * Verify that an unnamed kill-on-close Job can be created and released now.
  541. * @param api - active binding table.
  542. */
  543. export function probeCurrentTokenJobSupport(api: CurrentTokenProcessBindings): void {
  544. const job = createKillOnCloseJob(api)
  545. closeHandleChecked(api, job, 'current-token Job capability probe')
  546. }
  547. /**
  548. * Poll one process handle without blocking the runner event loop.
  549. * @param api - active binding table.
  550. * @param process - caller-owned process handle.
  551. * @returns the direct exit code when signalled, or undefined while running.
  552. */
  553. export function pollProcessExit(api: Win32ProcessBindings, process: NativePtr): number | undefined {
  554. const waitResult = api.waitForSingleObject(process, 0)
  555. if (waitResult === abi.WAIT_TIMEOUT) return undefined
  556. if (waitResult === 0xFFFFFFFF) throwLastError(api, 'WaitForSingleObject')
  557. const exitCodeSlot = allocUint32()
  558. try {
  559. if (api.getExitCodeProcess(process, exitCodeSlot) === 0) throwLastError(api, 'GetExitCodeProcess')
  560. return decodeUint32(exitCodeSlot)
  561. } finally {
  562. koffi.free(exitCodeSlot)
  563. }
  564. }
  565. /**
  566. * Return whether a Job has no active processes.
  567. * @param api - active binding table.
  568. * @param job - caller-owned Job handle.
  569. * @returns true once the Job reports zero active processes.
  570. */
  571. export function isJobEmpty(api: Win32ProcessBindings, job: NativePtr): boolean {
  572. const information = Buffer.alloc(abi.JOBOBJECT_BASIC_ACCOUNTING_SIZE)
  573. if (api.queryInformationJobObject(
  574. job,
  575. abi.JobObjectBasicAccountingInformation,
  576. information,
  577. information.length,
  578. null,
  579. ) === 0) {
  580. throwLastError(api, 'QueryInformationJobObject', 'active process count')
  581. }
  582. return information.readUInt32LE(abi.JOBOBJECT_BASIC_ACCOUNTING_ACTIVE_PROCESSES_OFFSET) === 0
  583. }
  584. /**
  585. * Terminate every process in a Job.
  586. * @param api - active binding table.
  587. * @param job - caller-owned Job handle.
  588. * @param exitCode - direct Windows exit code assigned to members.
  589. */
  590. export function terminateJob(api: Win32ProcessBindings, job: NativePtr, exitCode: number): void {
  591. if (api.terminateJobObject(job, exitCode) === 0) throwLastError(api, 'TerminateJobObject')
  592. }
  593. /**
  594. * Close a caller-owned handle and report a labelled Win32 failure.
  595. * @param api - active binding table.
  596. * @param handle - handle to close.
  597. * @param detail - lifecycle label for diagnostics.
  598. */
  599. export function closeHandleChecked(api: Win32ProcessBindings, handle: NativePtr, detail: string): void {
  600. if (api.closeHandle(handle) === 0) throwLastError(api, 'CloseHandle', detail)
  601. }