process.spec.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import koffi from 'koffi'
  2. import { describe, expect, it, vi } from 'vitest'
  3. import {
  4. Win32Error,
  5. drainPipe,
  6. spawnInheritedJobProcess,
  7. spawnPipedProcess,
  8. } from '../src/index.ts'
  9. import { CREATE_SUSPENDED } from '../src/abi.ts'
  10. import { PROCESS_INFORMATION } from '../src/ffi.ts'
  11. import type { NativePtr, Win32ProcessBindings } from '../src/index.ts'
  12. const PVOID = koffi.pointer('void')
  13. function inheritedApi(overrides: Partial<Win32ProcessBindings> = {}): {
  14. api: Win32ProcessBindings
  15. events: string[]
  16. createProcessAsUserW: ReturnType<typeof vi.fn>
  17. assignProcessToJobObject: ReturnType<typeof vi.fn>
  18. resumeThread: ReturnType<typeof vi.fn>
  19. } {
  20. const events: string[] = []
  21. const createProcessAsUserWImpl: Win32ProcessBindings['createProcessAsUserW'] =
  22. overrides.createProcessAsUserW
  23. ?? ((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
  24. events.push('create')
  25. koffi.encode(info, PROCESS_INFORMATION, {
  26. hProcess: 60n,
  27. hThread: 61n,
  28. dwProcessId: 1234,
  29. dwThreadId: 5678,
  30. })
  31. return 1
  32. })
  33. const createProcessAsUserW = vi.fn(createProcessAsUserWImpl)
  34. const assignProcessToJobObject = vi.fn(() => {
  35. events.push('assign')
  36. return 1
  37. })
  38. const resumeThread = vi.fn(() => {
  39. events.push('resume')
  40. return 1
  41. })
  42. const api = {
  43. createJobObjectW: vi.fn(() => 50n),
  44. setInformationJobObject: vi.fn(() => 1),
  45. getStdHandle: vi.fn((selector: number) => BigInt(100 - selector)),
  46. setHandleInformation: vi.fn((_handle: NativePtr, _mask: number, flags: number) => {
  47. events.push(flags === 0 ? 'restore' : 'inherit')
  48. return 1
  49. }),
  50. assignProcessToJobObject,
  51. resumeThread,
  52. terminateProcess: vi.fn(() => 1),
  53. closeHandle: vi.fn((handle: NativePtr) => { events.push(`close:${handle}`); return 1 }),
  54. getLastError: vi.fn(() => 5),
  55. formatMessageW: vi.fn(() => 0),
  56. ...overrides,
  57. createProcessAsUserW,
  58. } as unknown as Win32ProcessBindings
  59. return {
  60. api,
  61. events,
  62. createProcessAsUserW,
  63. assignProcessToJobObject,
  64. resumeThread,
  65. }
  66. }
  67. describe('spawnInheritedJobProcess', () => {
  68. const token = 70n as NativePtr
  69. it('creates suspended, assigns the Job, then resumes the restricted child', () => {
  70. const {
  71. api,
  72. events,
  73. createProcessAsUserW,
  74. assignProcessToJobObject,
  75. resumeThread,
  76. } = inheritedApi()
  77. const child = spawnInheritedJobProcess(api, {
  78. command: 'cmd.exe',
  79. args: ['/c', 'exit', '0'],
  80. cwd: 'C:\\work',
  81. token,
  82. })
  83. expect(child).toEqual({ pid: 1234, process: 60n, job: 50n })
  84. expect(events.indexOf('create')).toBeLessThan(events.indexOf('assign'))
  85. expect(events.indexOf('assign')).toBeLessThan(events.indexOf('resume'))
  86. expect(assignProcessToJobObject).toHaveBeenCalledWith(50n, 60n)
  87. expect(resumeThread).toHaveBeenCalledWith(61n)
  88. expect(createProcessAsUserW).toHaveBeenCalledWith(
  89. token,
  90. null,
  91. 'cmd.exe /c exit 0',
  92. null,
  93. null,
  94. 1,
  95. CREATE_SUSPENDED,
  96. null,
  97. 'C:\\work',
  98. expect.anything(),
  99. expect.anything(),
  100. )
  101. })
  102. it('restores already-enabled stdio and closes the Job when inheritance setup fails', () => {
  103. let calls = 0
  104. const closeHandle = vi.fn((_handle: NativePtr) => 1)
  105. const setHandleInformation = vi.fn((_handle: NativePtr, _mask: number, flags: number) => {
  106. if (flags === 0) return 1
  107. calls += 1
  108. return calls === 2 ? 0 : 1
  109. })
  110. const { api } = inheritedApi({ closeHandle, setHandleInformation })
  111. expect(() => spawnInheritedJobProcess(api, {
  112. command: 'cmd.exe',
  113. args: [],
  114. cwd: 'C:\\work',
  115. token,
  116. })).toThrow(Win32Error)
  117. expect(setHandleInformation).toHaveBeenCalledWith(expect.anything(), 1, 0)
  118. expect(closeHandle).toHaveBeenCalledWith(50n)
  119. })
  120. it('captures a GetStdHandle error before Job cleanup changes last-error', () => {
  121. let lastError = 123
  122. const { api } = inheritedApi({
  123. getStdHandle: vi.fn(() => 0n as NativePtr),
  124. getLastError: vi.fn(() => lastError),
  125. closeHandle: vi.fn(() => { lastError = 999; return 1 }),
  126. })
  127. let caught: unknown
  128. try {
  129. spawnInheritedJobProcess(api, { command: 'cmd.exe', args: [], cwd: 'C:\\work', token })
  130. } catch (error) {
  131. caught = error
  132. }
  133. expect(caught).toMatchObject({ api: 'GetStdHandle', win32Code: 123 })
  134. })
  135. it('captures a CreateProcess error before inheritance restoration changes last-error', () => {
  136. let lastError = 87
  137. const { api } = inheritedApi({
  138. createProcessAsUserW: vi.fn(() => 0),
  139. getLastError: vi.fn(() => lastError),
  140. setHandleInformation: vi.fn((_handle, _mask, flags) => {
  141. if (flags === 0) lastError = 999
  142. return 1
  143. }),
  144. closeHandle: vi.fn(() => { lastError = 998; return 1 }),
  145. })
  146. let caught: unknown
  147. try {
  148. spawnInheritedJobProcess(api, { command: 'cmd.exe', args: [], cwd: 'C:\\work', token })
  149. } catch (error) {
  150. caught = error
  151. }
  152. expect(caught).toMatchObject({ api: 'CreateProcessAsUserW', win32Code: 87 })
  153. })
  154. it('terminates the suspended process and closes the Job when CreateProcessAsUserW returns a null thread handle', () => {
  155. const closeHandle = vi.fn((_handle: NativePtr) => 1)
  156. const terminateProcess = vi.fn(() => 1)
  157. const { api } = inheritedApi({
  158. closeHandle,
  159. terminateProcess,
  160. createProcessAsUserW: vi.fn((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
  161. koffi.encode(info, PROCESS_INFORMATION, {
  162. hProcess: 60n,
  163. hThread: 0n,
  164. dwProcessId: 1234,
  165. dwThreadId: 0,
  166. })
  167. return 1
  168. }),
  169. })
  170. expect(() => spawnInheritedJobProcess(api, {
  171. command: 'cmd.exe',
  172. args: [],
  173. cwd: 'C:\\work',
  174. token,
  175. })).toThrow('null process/thread handles')
  176. expect(terminateProcess).toHaveBeenCalledWith(60n, 1)
  177. expect(closeHandle).toHaveBeenCalledWith(50n)
  178. expect(closeHandle).toHaveBeenCalledWith(60n)
  179. })
  180. })
  181. describe('wait and pipe cleanup', () => {
  182. const token = 70n as NativePtr
  183. it('waits when a pipe is temporarily empty before observing EOF', async () => {
  184. const closeHandle = vi.fn(() => 1)
  185. let peeks = 0
  186. const api = {
  187. peekNamedPipe: vi.fn((_handle, _buffer, _size, _read, available) => {
  188. peeks += 1
  189. if (peeks === 1) {
  190. koffi.encode(available, 'uint32', 0)
  191. return 1
  192. }
  193. return 0
  194. }),
  195. getLastError: vi.fn(() => 109),
  196. closeHandle,
  197. } as unknown as Win32ProcessBindings
  198. await expect(drainPipe(api, 80n as NativePtr)).resolves.toEqual(Buffer.alloc(0))
  199. expect(closeHandle).toHaveBeenCalledWith(80n)
  200. })
  201. it('terminates a piped child when CreateProcess returns a null thread handle', () => {
  202. let nextPipe = 10n
  203. const terminateProcess = vi.fn(() => 1)
  204. const closeHandle = vi.fn(() => 1)
  205. const api = {
  206. createPipe: vi.fn((readSlot, writeSlot) => {
  207. koffi.encode(readSlot, PVOID, nextPipe++)
  208. koffi.encode(writeSlot, PVOID, nextPipe++)
  209. return 1
  210. }),
  211. setHandleInformation: vi.fn(() => 1),
  212. createProcessAsUserW: vi.fn((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
  213. koffi.encode(info, PROCESS_INFORMATION, {
  214. hProcess: 60n,
  215. hThread: 0n,
  216. dwProcessId: 1234,
  217. dwThreadId: 0,
  218. })
  219. return 1
  220. }),
  221. terminateProcess,
  222. closeHandle,
  223. } as unknown as Win32ProcessBindings
  224. expect(() => spawnPipedProcess(api, {
  225. command: 'cmd.exe',
  226. args: [],
  227. cwd: 'C:\\work',
  228. token,
  229. })).toThrow('null process/thread handles')
  230. expect(terminateProcess).toHaveBeenCalledWith(60n, 1)
  231. })
  232. })