process-allocation-failure.spec.ts 4.9 KB

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