| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- import koffi from 'koffi'
- import { describe, expect, it, vi } from 'vitest'
- import {
- closeHandleChecked,
- isJobEmpty,
- pollProcessExit,
- probeCurrentTokenJobSupport,
- spawnCurrentTokenJobProcess,
- terminateJob,
- Win32Error,
- } from '../src/index.ts'
- import {
- CREATE_SUSPENDED,
- CREATE_UNICODE_ENVIRONMENT,
- JOBOBJECT_BASIC_ACCOUNTING_ACTIVE_PROCESSES_OFFSET,
- JOBOBJECT_BASIC_ACCOUNTING_SIZE,
- JobObjectBasicAccountingInformation,
- WAIT_TIMEOUT,
- } from '../src/abi.ts'
- import { PROCESS_INFORMATION, STARTUPINFOW } from '../src/ffi.ts'
- import type {
- CurrentTokenProcessSpawnOptions,
- CurrentTokenProcessBindings,
- NativePtr,
- } from '../src/index.ts'
- function options(
- overrides: Partial<CurrentTokenProcessSpawnOptions> = {},
- ): CurrentTokenProcessSpawnOptions {
- return {
- command: 'probe.exe',
- applicationName: 'C:\\resolved\\probe.exe',
- args: [],
- cwd: 'C:\\work',
- env: {},
- stdio: { stdin: 4, stdout: 5, stderr: 6 },
- ...overrides,
- }
- }
- function api(overrides: Partial<CurrentTokenProcessBindings> = {}): CurrentTokenProcessBindings {
- return {
- createJobObjectW: vi.fn(() => 50n),
- setInformationJobObject: vi.fn(() => 1),
- queryInformationJobObject: vi.fn((_job: NativePtr, _cls: number, information: Buffer) => {
- information.writeUInt32LE(0, JOBOBJECT_BASIC_ACCOUNTING_ACTIVE_PROCESSES_OFFSET)
- return 1
- }),
- getStdHandle: vi.fn((selector: number) => BigInt(100 - selector)),
- uvGetOsfhandle: vi.fn((fileDescriptor: number) => BigInt(100 + fileDescriptor)),
- setHandleInformation: vi.fn(() => 1),
- createProcessW: vi.fn((_app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
- koffi.encode(info, PROCESS_INFORMATION, {
- hProcess: 60n,
- hThread: 61n,
- dwProcessId: 1234,
- dwThreadId: 5678,
- })
- return 1
- }),
- assignProcessToJobObject: vi.fn(() => 1),
- resumeThread: vi.fn(() => 0),
- terminateProcess: vi.fn(() => 1),
- terminateJobObject: vi.fn(() => 1),
- waitForSingleObject: vi.fn(() => 0),
- getExitCodeProcess: vi.fn((_process, slot) => {
- koffi.encode(slot, 'uint32', 42)
- return 1
- }),
- closeHandle: vi.fn(() => 1),
- getLastError: vi.fn(() => 5),
- formatMessageW: vi.fn(() => 0),
- ...overrides,
- } as unknown as CurrentTokenProcessBindings
- }
- describe('ordinary Job process operations', () => {
- it('creates suspended, assigns the Job, and resumes before returning', () => {
- const events: string[] = []
- const createProcessW = vi.fn((
- _app: unknown,
- _line: unknown,
- _pa: unknown,
- _ta: unknown,
- _inherit: unknown,
- _flags: unknown,
- _env: unknown,
- _cwd: unknown,
- _startup: unknown,
- info: NativePtr,
- ) => {
- events.push('create')
- koffi.encode(info, PROCESS_INFORMATION, { hProcess: 60n, hThread: 61n, dwProcessId: 1234, dwThreadId: 5678 })
- return 1
- })
- const bindings = api({
- createProcessW,
- assignProcessToJobObject: vi.fn(() => { events.push('assign'); return 1 }),
- resumeThread: vi.fn(() => { events.push('resume'); return 0 }),
- closeHandle: vi.fn((handle: NativePtr) => { events.push(`close:${handle}`); return 1 }),
- })
- expect(spawnCurrentTokenJobProcess(bindings, options({
- args: ['literal $VALUE', 'a b'],
- env: {
- ZED: 'last',
- '=C:': 'C:\\work',
- alpha: 'first',
- ALPHA: 'same-folded-key',
- _A: 'underscore',
- },
- }))).toEqual({ pid: 1234, process: 60n, job: 50n })
- const environment = createProcessW.mock.calls[0]?.[6] as Buffer
- expect(createProcessW).toHaveBeenCalledWith(
- 'C:\\resolved\\probe.exe',
- 'probe.exe "literal $VALUE" "a b"',
- null,
- null,
- 1,
- CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT,
- environment,
- 'C:\\work',
- expect.anything(),
- expect.anything(),
- )
- expect(environment.toString('utf16le')).toBe(
- '=C:=C:\\work\0alpha=first\0ALPHA=same-folded-key\0ZED=last\0_A=underscore\0\0',
- )
- expect(events.indexOf('create')).toBeLessThan(events.indexOf('assign'))
- expect(events.indexOf('assign')).toBeLessThan(events.indexOf('resume'))
- expect(events).toContain('close:61')
- })
- it('reports CreateProcessW failure without replaying another creator', () => {
- const bindings = api({ createProcessW: vi.fn(() => 0) })
- let caught: unknown
- try {
- spawnCurrentTokenJobProcess(bindings, options({ command: 'missing.exe' }))
- } catch (error) {
- caught = error
- }
- expect(caught).toMatchObject({ api: 'CreateProcessW', win32Code: 5 })
- })
- it('resolves the target carrier descriptors and restores their handle flags', () => {
- let startup: Record<string, unknown> | undefined
- const setHandleInformation = vi.fn(() => 1)
- const uvGetOsfhandle = vi.fn(
- (fileDescriptor: number) => BigInt(100 + fileDescriptor) as NativePtr,
- )
- const bindings = api({
- setHandleInformation,
- uvGetOsfhandle,
- createProcessW: vi.fn((_app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, infoPtr, processInfo) => {
- startup = koffi.decode(infoPtr, STARTUPINFOW) as Record<string, unknown>
- koffi.encode(processInfo, PROCESS_INFORMATION, {
- hProcess: 60n,
- hThread: 61n,
- dwProcessId: 1234,
- dwThreadId: 5678,
- })
- return 1
- }),
- })
- expect(spawnCurrentTokenJobProcess(bindings, options())).toEqual({ pid: 1234, process: 60n, job: 50n })
- expect(startup).toMatchObject({ hStdInput: 104n, hStdOutput: 105n, hStdError: 106n })
- expect(uvGetOsfhandle).toHaveBeenNthCalledWith(1, 4)
- expect(uvGetOsfhandle).toHaveBeenNthCalledWith(2, 5)
- expect(uvGetOsfhandle).toHaveBeenNthCalledWith(3, 6)
- expect(setHandleInformation.mock.calls).toEqual([
- [104n, 1, 1], [105n, 1, 1], [106n, 1, 1],
- [104n, 1, 0], [105n, 1, 0], [106n, 1, 0],
- ])
- })
- it('polls direct exit and Job emptiness without blocking', () => {
- const queryInformationJobObject = vi.fn((_job: NativePtr, _cls: number, information: Buffer) => {
- information.writeUInt32LE(1, JOBOBJECT_BASIC_ACCOUNTING_ACTIVE_PROCESSES_OFFSET)
- return 1
- })
- const running = api({
- waitForSingleObject: vi.fn(() => WAIT_TIMEOUT),
- queryInformationJobObject,
- })
- expect(pollProcessExit(running, 60n as NativePtr)).toBeUndefined()
- expect(isJobEmpty(running, 50n as NativePtr)).toBe(false)
- expect(queryInformationJobObject).toHaveBeenCalledWith(
- 50n,
- JobObjectBasicAccountingInformation,
- expect.objectContaining({ length: JOBOBJECT_BASIC_ACCOUNTING_SIZE }),
- JOBOBJECT_BASIC_ACCOUNTING_SIZE,
- null,
- )
- const exited = api()
- expect(pollProcessExit(exited, 60n as NativePtr)).toBe(42)
- expect(isJobEmpty(exited, 50n as NativePtr)).toBe(true)
- })
- it('reports wait and exit-code query failures', () => {
- const processWait = api({ waitForSingleObject: vi.fn(() => 0xFFFFFFFF) })
- expect(() => pollProcessExit(processWait, 60n as NativePtr)).toThrow(Win32Error)
- const exitCode = api({ getExitCodeProcess: vi.fn(() => 0) })
- expect(() => pollProcessExit(exitCode, 60n as NativePtr)).toThrow(Win32Error)
- const jobQuery = api({ queryInformationJobObject: vi.fn(() => 0) })
- expect(() => isJobEmpty(jobQuery, 50n as NativePtr)).toThrow(Win32Error)
- })
- it('checks Job termination and caller-owned handle closure', () => {
- const terminateJobObject = vi.fn(() => 1)
- const bindings = api({ terminateJobObject })
- expect(() => { terminateJob(bindings, 50n as NativePtr, 1) }).not.toThrow()
- expect(() => { closeHandleChecked(bindings, 50n as NativePtr, 'test Job') }).not.toThrow()
- expect(terminateJobObject).toHaveBeenCalledWith(50n, 1)
- const failing = api({ terminateJobObject: vi.fn(() => 0) })
- expect(() => { terminateJob(failing, 50n as NativePtr, 1) }).toThrow(Win32Error)
- const closeFailure = api({ closeHandle: vi.fn(() => 0) })
- expect(() => { closeHandleChecked(closeFailure, 50n as NativePtr, 'test Job') }).toThrow(Win32Error)
- })
- it('probes an unnamed Job and closes its handle', () => {
- const closeHandle = vi.fn(() => 1)
- const bindings = api({ closeHandle })
- expect(() => { probeCurrentTokenJobSupport(bindings) }).not.toThrow()
- expect(closeHandle).toHaveBeenCalledExactlyOnceWith(50n)
- })
- it('rejects invalid carrier handles before target creation', () => {
- const expectFailure = (invalid: NativePtr | null): void => {
- const closeHandle = vi.fn(() => 1)
- const createProcessW = vi.fn(() => 1)
- expect(() => spawnCurrentTokenJobProcess(api({
- closeHandle,
- createProcessW,
- uvGetOsfhandle: vi.fn(() => invalid),
- }), options())).toThrow('uv_get_osfhandle returned an invalid handle for target stdin fd 4')
- expect(closeHandle).toHaveBeenCalledWith(50n)
- expect(createProcessW).not.toHaveBeenCalled()
- }
- for (const invalid of [null, 0n, 0xffff_ffff_ffff_ffffn, 0xffff_ffff_ffff_fffen]) {
- expectFailure(invalid as NativePtr | null)
- }
- })
- })
|