process.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. /** Typed Win32 process operations over the shared binding table. */
  2. import * as abi from './abi.ts'
  3. import { inheritedControlStdio } from './control-stdio.ts'
  4. import {
  5. allocProcessInfo,
  6. allocPtrSlot,
  7. allocStartupInfo,
  8. allocUint32,
  9. decodeProcessInfo,
  10. decodePtr,
  11. decodeUint32,
  12. encodeStartupInfo,
  13. isNullPtr,
  14. throwLastError,
  15. throwWin32,
  16. } from './ffi.ts'
  17. import type { CurrentTokenProcessBindings, NativePtr, Win32ProcessBindings } from './ffi.ts'
  18. import { requireKoffi } from './koffi.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) requireKoffi().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. requireKoffi().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. * New console windows start hidden without changing console inheritance.
  186. * @param api - active binding table.
  187. * @param options - command, cwd, args, and restricted primary token.
  188. * @returns caller-owned process and pipe read handles.
  189. */
  190. export function spawnPipedProcess(
  191. api: Win32ProcessBindings,
  192. options: RestrictedProcessSpawnOptions,
  193. ): SpawnedPipedProcess {
  194. const owned = new Set<NativePtr>()
  195. let startupInfo: NativePtr | undefined
  196. let processInfo: NativePtr | undefined
  197. try {
  198. const stdIn = createPipe(api, owned)
  199. const stdOut = createPipe(api, owned)
  200. const stdErr = createPipe(api, owned)
  201. for (const [handle, label] of [
  202. [stdIn.read, 'stdin read end'],
  203. [stdOut.write, 'stdout write end'],
  204. [stdErr.write, 'stderr write end'],
  205. ] as const) {
  206. if (api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, abi.HANDLE_FLAG_INHERIT) === 0) {
  207. throwLastError(api, 'SetHandleInformation', label)
  208. }
  209. }
  210. startupInfo = allocStartupInfo()
  211. encodeStartupInfo(startupInfo, {
  212. cb: abi.STARTUPINFOW_SIZE,
  213. dwFlags: abi.STARTF_USESTDHANDLES | abi.STARTF_USESHOWWINDOW,
  214. wShowWindow: abi.SW_HIDE,
  215. hStdInput: stdIn.read,
  216. hStdOutput: stdOut.write,
  217. hStdError: stdErr.write,
  218. })
  219. processInfo = allocProcessInfo()
  220. const created = createRestrictedProcess(
  221. api,
  222. options,
  223. buildCommandLine(options.command, options.args),
  224. 0,
  225. startupInfo,
  226. processInfo,
  227. )
  228. if (created === 0) {
  229. const win32Code = api.getLastError()
  230. throwWin32(api, 'CreateProcessAsUserW', win32Code, `command: ${options.command}, cwd: ${options.cwd}`)
  231. }
  232. const info = decodeProcessInfo(processInfo)
  233. if (info.hProcess === null || info.hThread === null) {
  234. if (info.hProcess !== null) api.terminateProcess(info.hProcess, 1)
  235. closeBestEffort(api, info.hThread)
  236. closeBestEffort(api, info.hProcess)
  237. throw new Error(`CreateProcessAsUserW succeeded but returned null process/thread handles (pid ${info.dwProcessId})`)
  238. }
  239. closeOwned(api, owned, stdIn.read)
  240. closeOwned(api, owned, stdIn.write)
  241. closeOwned(api, owned, stdOut.write)
  242. closeOwned(api, owned, stdErr.write)
  243. closeBestEffort(api, info.hThread)
  244. owned.delete(stdOut.read)
  245. owned.delete(stdErr.read)
  246. return {
  247. pid: info.dwProcessId,
  248. process: info.hProcess,
  249. stdoutRead: stdOut.read,
  250. stderrRead: stdErr.read,
  251. }
  252. } catch (error) {
  253. closeAllOwned(api, owned)
  254. throw error
  255. } finally {
  256. freeNative(processInfo)
  257. freeNative(startupInfo)
  258. }
  259. }
  260. /**
  261. * Drain one anonymous pipe until the writer closes it.
  262. * @param api - active binding table.
  263. * @param handle - caller-owned pipe read end.
  264. * @returns complete bytes read before EOF; the handle is always closed.
  265. * @throws when a Win32 pipe operation fails.
  266. */
  267. export async function drainPipe(
  268. api: Win32ProcessBindings,
  269. handle: NativePtr,
  270. ): Promise<Buffer> {
  271. const chunks: Buffer[] = []
  272. let countSlot: NativePtr | undefined
  273. try {
  274. countSlot = allocUint32()
  275. for (;;) {
  276. const peeked = api.peekNamedPipe(handle, null, 0, null, countSlot, null)
  277. if (peeked === 0) {
  278. const win32Code = api.getLastError()
  279. if (win32Code === abi.ERROR_BROKEN_PIPE || win32Code === abi.ERROR_NO_DATA) break
  280. throwLastError(api, 'PeekNamedPipe', `drain failure after ${chunks.length} chunk(s)`)
  281. }
  282. const available = decodeUint32(countSlot)
  283. if (available > 0) {
  284. const chunk = Buffer.alloc(available)
  285. if (api.readFile(handle, chunk, chunk.length, countSlot, null) === 0) {
  286. throwLastError(api, 'ReadFile', `drain failure after ${chunks.length} chunk(s)`)
  287. }
  288. chunks.push(chunk.subarray(0, decodeUint32(countSlot)))
  289. }
  290. await new Promise<void>(resolve => setTimeout(resolve, 1))
  291. }
  292. return Buffer.concat(chunks)
  293. } finally {
  294. freeNative(countSlot)
  295. api.closeHandle(handle)
  296. }
  297. }
  298. /**
  299. * Wait for a process and always close its handle.
  300. * @param api - active binding table.
  301. * @param process - caller-owned process handle.
  302. * @returns direct process exit code.
  303. */
  304. export function waitForProcessExit(api: Win32ProcessBindings, process: NativePtr): number {
  305. let exitCodeSlot: NativePtr | undefined
  306. try {
  307. if (api.waitForSingleObject(process, abi.INFINITE) === 0xFFFFFFFF) {
  308. throwLastError(api, 'WaitForSingleObject')
  309. }
  310. exitCodeSlot = allocUint32()
  311. if (api.getExitCodeProcess(process, exitCodeSlot) === 0) throwLastError(api, 'GetExitCodeProcess')
  312. return decodeUint32(exitCodeSlot)
  313. } finally {
  314. freeNative(exitCodeSlot)
  315. api.closeHandle(process)
  316. }
  317. }
  318. function createKillOnCloseJob(api: Win32ProcessBindings): NativePtr {
  319. const job = api.createJobObjectW(null, null)
  320. if (isNullPtr(job)) throwLastError(api, 'CreateJobObjectW')
  321. const information = Buffer.alloc(abi.JOBOBJECT_EXTENDED_LIMIT_SIZE)
  322. information.writeUInt32LE(
  323. abi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
  324. abi.JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET,
  325. )
  326. if (api.setInformationJobObject(
  327. job,
  328. abi.JobObjectExtendedLimitInformation,
  329. information,
  330. information.length,
  331. ) === 0) {
  332. const win32Code = api.getLastError()
  333. api.closeHandle(job)
  334. throwWin32(api, 'SetInformationJobObject', win32Code)
  335. }
  336. return job
  337. }
  338. interface ProcessStandardHandles {
  339. stdin: NativePtr
  340. stdout: NativePtr
  341. stderr: NativePtr
  342. control?: { fileDescriptor: 7; handle: NativePtr }
  343. }
  344. // Koffi exposes PVOID as an unsigned 64-bit bigint on supported Windows hosts.
  345. const UV_INVALID_OS_FILE_HANDLE = 0xffff_ffff_ffff_ffffn
  346. const UV_INVALID_FILE_DESCRIPTOR = 0xffff_ffff_ffff_fffen
  347. function inheritedStandardHandles(api: Win32ProcessBindings, controlFileDescriptor?: 7): ProcessStandardHandles {
  348. const get = (selector: number, label: string): NativePtr => {
  349. const handle = api.getStdHandle(selector)
  350. if (!isNullPtr(handle)) return handle
  351. throwLastError(api, 'GetStdHandle', `null ${label} handle`)
  352. }
  353. return {
  354. stdin: get(abi.STD_INPUT_HANDLE, 'stdin'),
  355. stdout: get(abi.STD_OUTPUT_HANDLE, 'stdout'),
  356. stderr: get(abi.STD_ERROR_HANDLE, 'stderr'),
  357. ...controlFileDescriptor === undefined ? {} : {
  358. control: { fileDescriptor: controlFileDescriptor, handle: descriptorHandle(api, controlFileDescriptor, 'control') },
  359. },
  360. }
  361. }
  362. function descriptorHandle(api: Win32ProcessBindings, fileDescriptor: number, label: string): NativePtr {
  363. const handle = api.uvGetOsfhandle(fileDescriptor)
  364. if (
  365. isNullPtr(handle)
  366. || handle === UV_INVALID_OS_FILE_HANDLE
  367. || handle === UV_INVALID_FILE_DESCRIPTOR
  368. ) {
  369. throw new Error(`uv_get_osfhandle returned an invalid handle for target ${label} fd ${String(fileDescriptor)}`)
  370. }
  371. return handle
  372. }
  373. function targetCarrierHandles(
  374. api: CurrentTokenProcessBindings,
  375. descriptors: CurrentTokenStdioFileDescriptors,
  376. ): ProcessStandardHandles {
  377. return {
  378. stdin: descriptorHandle(api, descriptors.stdin, 'stdin'),
  379. stdout: descriptorHandle(api, descriptors.stdout, 'stdout'),
  380. stderr: descriptorHandle(api, descriptors.stderr, 'stderr'),
  381. ...descriptors.control === undefined ? {} : {
  382. control: { fileDescriptor: descriptors.control, handle: descriptorHandle(api, descriptors.control, 'control') },
  383. },
  384. }
  385. }
  386. /** Shared suspended-create, Job-assignment, and resume lifecycle. */
  387. function spawnJobProcess(
  388. api: Win32ProcessBindings,
  389. options: ProcessSpawnOptions,
  390. resolveStdio: () => ProcessStandardHandles,
  391. createName: 'CreateProcessAsUserW' | 'CreateProcessW',
  392. create: (startupInfo: NativePtr, processInfo: NativePtr) => number,
  393. ): SpawnedJobProcess {
  394. const job = createKillOnCloseJob(api)
  395. const enabled: NativePtr[] = []
  396. let startupInfo: NativePtr | undefined
  397. let processInfo: NativePtr | undefined
  398. let controlDescriptorBlock: { pointer: NativePtr; length: number } | undefined
  399. let created = 0
  400. let createFailureCode = 0
  401. try {
  402. const stdio = resolveStdio()
  403. const inherited: Array<readonly [NativePtr, string]> = [
  404. [stdio.stdin, 'stdin'],
  405. [stdio.stdout, 'stdout'],
  406. [stdio.stderr, 'stderr'],
  407. ]
  408. if (stdio.control !== undefined) inherited.push([stdio.control.handle, 'control'])
  409. for (const [handle, label] of inherited) {
  410. if (api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, abi.HANDLE_FLAG_INHERIT) === 0) {
  411. throwLastError(api, 'SetHandleInformation', `${label} (enable inherit)`)
  412. }
  413. enabled.push(handle)
  414. }
  415. const controlBytes = stdio.control === undefined
  416. ? undefined
  417. : inheritedControlStdio(api, { ...stdio, control: stdio.control })
  418. if (controlBytes !== undefined) {
  419. const koffi = requireKoffi()
  420. controlDescriptorBlock = { pointer: koffi.alloc('uint8', controlBytes.length) as NativePtr, length: controlBytes.length }
  421. koffi.encode(controlDescriptorBlock.pointer, 'uint8', controlBytes, controlBytes.length)
  422. }
  423. startupInfo = allocStartupInfo()
  424. encodeStartupInfo(startupInfo, {
  425. cb: abi.STARTUPINFOW_SIZE,
  426. // Preserve console inheritance: CREATE_NO_WINDOW can fail restricted-token DLL initialization.
  427. dwFlags: abi.STARTF_USESTDHANDLES | abi.STARTF_USESHOWWINDOW,
  428. wShowWindow: abi.SW_HIDE,
  429. hStdInput: stdio.stdin,
  430. hStdOutput: stdio.stdout,
  431. hStdError: stdio.stderr,
  432. ...controlDescriptorBlock === undefined ? {} : {
  433. cbReserved2: controlDescriptorBlock.length,
  434. lpReserved2: controlDescriptorBlock.pointer,
  435. },
  436. })
  437. processInfo = allocProcessInfo()
  438. created = create(startupInfo, processInfo)
  439. if (created === 0) createFailureCode = api.getLastError()
  440. } catch (error) {
  441. freeNative(processInfo)
  442. api.closeHandle(job)
  443. throw error
  444. } finally {
  445. freeNative(startupInfo)
  446. freeNative(controlDescriptorBlock?.pointer)
  447. for (const handle of enabled) {
  448. // The runner spawns nothing else; cleanup failure must not mask the child.
  449. api.setHandleInformation(handle, abi.HANDLE_FLAG_INHERIT, 0)
  450. }
  451. }
  452. if (created === 0) {
  453. freeNative(processInfo)
  454. api.closeHandle(job)
  455. throwWin32(
  456. api,
  457. createName,
  458. createFailureCode,
  459. `command: ${options.command}, cwd: ${options.cwd}`,
  460. )
  461. }
  462. let info: ReturnType<typeof decodeProcessInfo>
  463. try {
  464. info = decodeProcessInfo(processInfo)
  465. } finally {
  466. freeNative(processInfo)
  467. }
  468. if (info.hProcess === null || info.hThread === null) {
  469. if (info.hProcess !== null) api.terminateProcess(info.hProcess, 1)
  470. api.closeHandle(job)
  471. closeBestEffort(api, info.hThread)
  472. closeBestEffort(api, info.hProcess)
  473. throw new Error(`${createName} succeeded but returned null process/thread handles (pid ${info.dwProcessId})`)
  474. }
  475. if (api.assignProcessToJobObject(job, info.hProcess) === 0) {
  476. const win32Code = api.getLastError()
  477. api.terminateProcess(info.hProcess, 1)
  478. closeBestEffort(api, info.hThread)
  479. closeBestEffort(api, info.hProcess)
  480. api.closeHandle(job)
  481. throwWin32(api, 'AssignProcessToJobObject', win32Code, `pid ${info.dwProcessId}`)
  482. }
  483. if (api.resumeThread(info.hThread) === 0xFFFFFFFF) {
  484. const win32Code = api.getLastError()
  485. closeBestEffort(api, info.hThread)
  486. closeBestEffort(api, info.hProcess)
  487. api.closeHandle(job)
  488. throwWin32(api, 'ResumeThread', win32Code, `pid ${info.dwProcessId}`)
  489. }
  490. closeBestEffort(api, info.hThread)
  491. return { pid: info.dwProcessId, process: info.hProcess, job }
  492. }
  493. /**
  494. * Spawn a restricted-token process suspended with hidden initial windows, assign its Job, then resume it.
  495. * @param api - active binding table.
  496. * @param options - command, cwd, args, and restricted primary token.
  497. * @returns caller-owned process and Job handles after successful resume.
  498. * @remarks Node clears stdio handle inheritability at startup through
  499. * uv_disable_stdio_inheritance. This operation temporarily restores the bits
  500. * required by STARTF_USESTDHANDLES. Restoring them afterward is best-effort:
  501. * failure must not replace the already-created child's outcome.
  502. */
  503. export function spawnInheritedJobProcess(
  504. api: Win32ProcessBindings,
  505. options: RestrictedProcessSpawnOptions,
  506. ): SpawnedJobProcess {
  507. const commandLine = buildCommandLine(options.command, options.args)
  508. return spawnJobProcess(api, options, () => inheritedStandardHandles(api, options.controlFileDescriptor), 'CreateProcessAsUserW', (startupInfo, processInfo) =>
  509. createRestrictedProcess(
  510. api,
  511. options,
  512. commandLine,
  513. abi.CREATE_SUSPENDED,
  514. startupInfo,
  515. processInfo,
  516. ))
  517. }
  518. /**
  519. * Spawn an ordinary process suspended with hidden initial windows, assign its Job, then resume it.
  520. * @param api - active binding table.
  521. * @param options - command, cwd, argv, and target carrier descriptors.
  522. * @returns caller-owned process and Job handles after successful resume.
  523. */
  524. export function spawnCurrentTokenJobProcess(
  525. api: CurrentTokenProcessBindings,
  526. options: CurrentTokenProcessSpawnOptions,
  527. ): SpawnedJobProcess {
  528. const commandLine = buildCommandLine(options.command, options.args)
  529. const environment = encodeWindowsEnvironment(options.env)
  530. return spawnJobProcess(api, options, () => targetCarrierHandles(api, options.stdio), 'CreateProcessW', (startupInfo, processInfo) =>
  531. api.createProcessW(
  532. options.applicationName,
  533. commandLine,
  534. null,
  535. null,
  536. 1,
  537. abi.CREATE_SUSPENDED | abi.CREATE_UNICODE_ENVIRONMENT,
  538. environment,
  539. options.cwd,
  540. startupInfo,
  541. processInfo,
  542. ))
  543. }
  544. /**
  545. * Verify that an unnamed kill-on-close Job can be created and released now.
  546. * @param api - active binding table.
  547. */
  548. export function probeCurrentTokenJobSupport(api: CurrentTokenProcessBindings): void {
  549. const job = createKillOnCloseJob(api)
  550. closeHandleChecked(api, job, 'current-token Job capability probe')
  551. }
  552. /**
  553. * Poll one process handle without blocking the runner event loop.
  554. * @param api - active binding table.
  555. * @param process - caller-owned process handle.
  556. * @returns the direct exit code when signalled, or undefined while running.
  557. */
  558. export function pollProcessExit(api: Win32ProcessBindings, process: NativePtr): number | undefined {
  559. const waitResult = api.waitForSingleObject(process, 0)
  560. if (waitResult === abi.WAIT_TIMEOUT) return undefined
  561. if (waitResult === 0xFFFFFFFF) throwLastError(api, 'WaitForSingleObject')
  562. const exitCodeSlot = allocUint32()
  563. try {
  564. if (api.getExitCodeProcess(process, exitCodeSlot) === 0) throwLastError(api, 'GetExitCodeProcess')
  565. return decodeUint32(exitCodeSlot)
  566. } finally {
  567. requireKoffi().free(exitCodeSlot)
  568. }
  569. }
  570. /**
  571. * Return whether a Job has no active processes.
  572. * @param api - active binding table.
  573. * @param job - caller-owned Job handle.
  574. * @returns true once the Job reports zero active processes.
  575. */
  576. export function isJobEmpty(api: Win32ProcessBindings, job: NativePtr): boolean {
  577. const information = Buffer.alloc(abi.JOBOBJECT_BASIC_ACCOUNTING_SIZE)
  578. if (api.queryInformationJobObject(
  579. job,
  580. abi.JobObjectBasicAccountingInformation,
  581. information,
  582. information.length,
  583. null,
  584. ) === 0) {
  585. throwLastError(api, 'QueryInformationJobObject', 'active process count')
  586. }
  587. return information.readUInt32LE(abi.JOBOBJECT_BASIC_ACCOUNTING_ACTIVE_PROCESSES_OFFSET) === 0
  588. }
  589. /**
  590. * Terminate every process in a Job.
  591. * @param api - active binding table.
  592. * @param job - caller-owned Job handle.
  593. * @param exitCode - direct Windows exit code assigned to members.
  594. */
  595. export function terminateJob(api: Win32ProcessBindings, job: NativePtr, exitCode: number): void {
  596. if (api.terminateJobObject(job, exitCode) === 0) throwLastError(api, 'TerminateJobObject')
  597. }
  598. /**
  599. * Close a caller-owned handle and report a labelled Win32 failure.
  600. * @param api - active binding table.
  601. * @param handle - handle to close.
  602. * @param detail - lifecycle label for diagnostics.
  603. */
  604. export function closeHandleChecked(api: Win32ProcessBindings, handle: NativePtr, detail: string): void {
  605. if (api.closeHandle(handle) === 0) throwLastError(api, 'CloseHandle', detail)
  606. }