process-allocation-failure.spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import koffi from 'koffi'
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import {
  4. drainPipe,
  5. spawnInheritedJobProcess,
  6. spawnPipedProcess,
  7. waitForProcessExit,
  8. } from '../src/index.ts'
  9. import * as ffi from '../src/ffi.ts'
  10. import { processInformationType } from '../src/ffi.ts'
  11. import * as koffiLoader from '../src/koffi.ts'
  12. import type { NativePtr, Win32ProcessBindings } from '../src/ffi.ts'
  13. vi.mock('../src/ffi.ts', { spy: true })
  14. vi.mock('../src/koffi.ts', { spy: true })
  15. const PVOID = koffi.pointer('void')
  16. beforeEach(() => {
  17. vi.mocked(koffiLoader.requireKoffi).mockReturnValue(koffi)
  18. })
  19. afterEach(() => {
  20. vi.restoreAllMocks()
  21. })
  22. describe('spawnInheritedJobProcess allocation cleanup', () => {
  23. it('frees startup info when process-info allocation throws', () => {
  24. const api = {
  25. createJobObjectW: vi.fn(() => 50n),
  26. setInformationJobObject: vi.fn(() => 1),
  27. getStdHandle: vi.fn((selector: number) => BigInt(100 - selector)),
  28. setHandleInformation: vi.fn(() => 1),
  29. closeHandle: vi.fn(() => 1),
  30. getLastError: vi.fn(() => 5),
  31. formatMessageW: vi.fn(() => 0),
  32. } as unknown as Win32ProcessBindings
  33. const free = vi.spyOn(koffi, 'free')
  34. vi.mocked(ffi.allocProcessInfo).mockImplementationOnce(() => { throw new Error('process-info allocation failed') })
  35. expect(() => spawnInheritedJobProcess(api, {
  36. command: 'cmd.exe',
  37. args: [],
  38. cwd: 'C:\\',
  39. token: 70n as NativePtr,
  40. })).toThrow('process-info allocation failed')
  41. expect(free).toHaveBeenCalledOnce()
  42. })
  43. it('frees process info after a successful inherited spawn', () => {
  44. const api = {
  45. createJobObjectW: vi.fn(() => 50n),
  46. setInformationJobObject: vi.fn(() => 1),
  47. getStdHandle: vi.fn((selector: number) => BigInt(100 - selector)),
  48. setHandleInformation: vi.fn(() => 1),
  49. createProcessAsUserW: vi.fn((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
  50. koffi.encode(info, processInformationType(), {
  51. hProcess: 60n,
  52. hThread: 61n,
  53. dwProcessId: 1234,
  54. dwThreadId: 5678,
  55. })
  56. return 1
  57. }),
  58. assignProcessToJobObject: vi.fn(() => 1),
  59. resumeThread: vi.fn(() => 0),
  60. closeHandle: vi.fn(() => 1),
  61. getLastError: vi.fn(() => 5),
  62. formatMessageW: vi.fn(() => 0),
  63. } as unknown as Win32ProcessBindings
  64. const free = vi.spyOn(koffi, 'free')
  65. expect(spawnInheritedJobProcess(api, {
  66. command: 'cmd.exe',
  67. args: [],
  68. cwd: 'C:\\',
  69. token: 70n as NativePtr,
  70. })).toEqual({ pid: 1234, process: 60n, job: 50n })
  71. expect(free).toHaveBeenCalledTimes(2)
  72. })
  73. })
  74. describe('shared process allocation cleanup', () => {
  75. it('frees pipe slots and process structs after a successful piped spawn', () => {
  76. let nextHandle = 10n
  77. const api = {
  78. createPipe: vi.fn((readSlot: NativePtr, writeSlot: NativePtr) => {
  79. koffi.encode(readSlot, PVOID, nextHandle++)
  80. koffi.encode(writeSlot, PVOID, nextHandle++)
  81. return 1
  82. }),
  83. setHandleInformation: vi.fn(() => 1),
  84. createProcessAsUserW: vi.fn((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
  85. koffi.encode(info, processInformationType(), {
  86. hProcess: 60n,
  87. hThread: 61n,
  88. dwProcessId: 1234,
  89. dwThreadId: 5678,
  90. })
  91. return 1
  92. }),
  93. closeHandle: vi.fn(() => 1),
  94. getLastError: vi.fn(() => 5),
  95. formatMessageW: vi.fn(() => 0),
  96. } as unknown as Win32ProcessBindings
  97. const free = vi.spyOn(koffi, 'free')
  98. expect(spawnPipedProcess(api, {
  99. command: 'cmd.exe',
  100. args: [],
  101. cwd: 'C:\\',
  102. token: 70n as NativePtr,
  103. })).toMatchObject({ pid: 1234, process: 60n })
  104. expect(free).toHaveBeenCalledTimes(8)
  105. })
  106. it('reuses one drain count slot and frees it at EOF', async () => {
  107. let peeks = 0
  108. const api = {
  109. peekNamedPipe: vi.fn((_pipe, _buffer, _size, _read, totalAvail: NativePtr) => {
  110. peeks += 1
  111. if (peeks > 1) return 0
  112. koffi.encode(totalAvail, 'uint32', 1)
  113. return 1
  114. }),
  115. readFile: vi.fn((_file, buffer: Buffer, _count, readSlot: NativePtr) => {
  116. buffer[0] = 0x61
  117. koffi.encode(readSlot, 'uint32', 1)
  118. return 1
  119. }),
  120. getLastError: vi.fn(() => 109),
  121. closeHandle: vi.fn(() => 1),
  122. } as unknown as Win32ProcessBindings
  123. const alloc = vi.spyOn(koffi, 'alloc')
  124. const free = vi.spyOn(koffi, 'free')
  125. await expect(drainPipe(api, 70n as NativePtr)).resolves.toEqual(Buffer.from('a'))
  126. expect(alloc).toHaveBeenCalledOnce()
  127. expect(free).toHaveBeenCalledOnce()
  128. })
  129. it('frees the exit-code slot after reading a process result', () => {
  130. const api = {
  131. waitForSingleObject: vi.fn(() => 0),
  132. getExitCodeProcess: vi.fn((_process, exitCode: NativePtr) => {
  133. koffi.encode(exitCode, 'uint32', 42)
  134. return 1
  135. }),
  136. closeHandle: vi.fn(() => 1),
  137. } as unknown as Win32ProcessBindings
  138. const free = vi.spyOn(koffi, 'free')
  139. expect(waitForProcessExit(api, 60n as NativePtr)).toBe(42)
  140. expect(free).toHaveBeenCalledOnce()
  141. })
  142. })