cookie-import-native-job.ts
161 lines · browse/src/cookie-import-native-job.ts at 3f79d81
1import { randomUUID } from 'node:crypto';
3const NATIVE_COOKIE_STAGES = [
4 'platform', 'ffi_import', 'ffi_open', 'job_create', 'job_limits', 'job_open',
5 'process_open', 'job_assign', 'job_assigned', 'job_joined', 'process_close', 'job_query', 'job_terminate', 'job_close',
6 'worker_boot', 'supervisor_input', 'runtime_check', 'member_start', 'member_input', 'member_decoded', 'member_exit',
7 'node_start', 'node_spawned', 'node_exit', 'node_input', 'node_load', 'browser_launch', 'cookie_read', 'browser_close',
8] as const;
10export interface NativeCookieDiagnostic {
11 stage: typeof NATIVE_COOKIE_STAGES[number];
12 win32Error?: number;
13 lastStage?: typeof NATIVE_COOKIE_STAGES[number];
14 exitCode?: number;
15 nodeExitCode?: number;
16 signal?: string;
17 memberMode?: boolean;
18 stderrBytes?: number;
19}
21export class NativeCookieJobError extends Error {
22 readonly diagnostic: NativeCookieDiagnostic;
24 constructor(stage: NativeCookieDiagnostic['stage'], win32Error?: number) {
25 super(stage === 'platform' ? 'native_supervision_unavailable' : 'native_supervision_failed');
26 this.name = 'NativeCookieJobError';
27 this.diagnostic = { stage, ...(Number.isInteger(win32Error) && win32Error! >= 0 && win32Error! <= 0xffffffff ? { win32Error } : {}) };
28 }
29}
31export function nativeCookieDiagnostic(error: unknown, fallback: NativeCookieDiagnostic['stage']): NativeCookieDiagnostic {
32 return error instanceof NativeCookieJobError ? error.diagnostic : { stage: fallback };
33}
35export function parseNativeCookieDiagnostic(value: unknown): NativeCookieDiagnostic | undefined {
36 if (!value || typeof value !== 'object') return undefined;
37 const candidate = value as NativeCookieDiagnostic;
38 if (!NATIVE_COOKIE_STAGES.includes(candidate.stage)) return undefined;
39 const diagnostic = new NativeCookieJobError(candidate.stage, candidate.win32Error).diagnostic;
40 if (NATIVE_COOKIE_STAGES.includes(candidate.lastStage!)) diagnostic.lastStage = candidate.lastStage;
41 for (const field of ['exitCode', 'nodeExitCode', 'stderrBytes'] as const) {
42 const value = candidate[field];
43 if (typeof value === 'number' && Number.isInteger(value) && value >= (field === 'stderrBytes' ? 0 : -0x80000000) && value <= 0xffffffff) diagnostic[field] = value;
44 }
45 if (['SIGTERM', 'SIGKILL', 'SIGINT', 'SIGSEGV', 'SIGABRT', 'SIGBREAK', 'SIGHUP'].includes(candidate.signal!)) diagnostic.signal = candidate.signal;
46 if (typeof candidate.memberMode === 'boolean') diagnostic.memberMode = candidate.memberMode;
47 return diagnostic;
48}
50export interface NativeCookieJob {
51 name: string;
52 terminate(): void;
53 activeProcesses(): number;
54 close(): void;
55}
57export async function createNativeCookieJob(): Promise<NativeCookieJob> {
58 if (process.platform !== 'win32' || !['x64', 'arm64'].includes(process.arch)) {
59 throw new NativeCookieJobError('platform');
60 }
61 const { api, ptr, closeLibrary } = await openKernel();
62 const name = `Local\\gstack-cookie-${randomUUID()}`;
63 const wideName = Buffer.from(`${name}\0`, 'utf16le');
64 let stage: NativeCookieDiagnostic['stage'] = 'job_create';
65 let handle: number | bigint = 0;
66 let closed = false;
67 const close = () => {
68 if (closed) return;
69 closed = true;
70 try {
71 if (handle && !api.CloseHandle(handle)) throw new NativeCookieJobError('job_close', api.GetLastError());
72 } finally {
73 closeLibrary();
74 }
75 };
76 try {
77 handle = api.CreateJobObjectW(null, ptr(wideName));
78 const createError = api.GetLastError();
79 if (!handle || createError === 183) throw new NativeCookieJobError('job_create', createError);
80 const limits = Buffer.alloc(144);
81 limits.writeUInt32LE(0x2000, 16);
82 stage = 'job_limits';
83 if (!api.SetInformationJobObject(handle, 9, ptr(limits), limits.byteLength)) {
84 throw new NativeCookieJobError(stage, api.GetLastError());
85 }
86 return {
87 name,
88 terminate() {
89 if (closed) throw new NativeCookieJobError('job_terminate');
90 if (!api.TerminateJobObject(handle, 1)) throw new NativeCookieJobError('job_terminate', api.GetLastError());
91 },
92 activeProcesses() {
93 const accounting = Buffer.alloc(48);
94 if (closed) throw new NativeCookieJobError('job_query');
95 if (!api.QueryInformationJobObject(handle, 1, ptr(accounting), accounting.byteLength, null)) throw new NativeCookieJobError('job_query', api.GetLastError());
96 return accounting.readUInt32LE(40);
97 },
98 close,
99 };
100 } catch (error) {
101 try { close(); } catch {}
102 throw error instanceof NativeCookieJobError ? error : new NativeCookieJobError(stage);
103 }
104}
106export async function joinNativeCookieJob(name: string, observe?: (stage: NativeCookieDiagnostic['stage']) => void): Promise<void> {
107 if (process.platform !== 'win32' || !/^Local\\gstack-cookie-[0-9a-f-]{36}$/.test(name)) {
108 throw new NativeCookieJobError('job_open');
109 }
110 observe?.('ffi_import');
111 const { api, ptr, closeLibrary } = await openKernel();
112 const wideName = Buffer.from(`${name}\0`, 'utf16le');
113 let stage: NativeCookieDiagnostic['stage'] = 'job_open';
114 let handle: number | bigint = 0;
115 let currentProcess: number | bigint = 0;
116 try {
117 observe?.(stage);
118 handle = api.OpenJobObjectW(1, 0, ptr(wideName));
119 if (!handle) throw new NativeCookieJobError(stage, api.GetLastError());
120 stage = 'process_open';
121 observe?.(stage);
122 currentProcess = api.OpenProcess(0x0101, 0, process.pid);
123 if (!currentProcess) throw new NativeCookieJobError(stage, api.GetLastError());
124 stage = 'job_assign';
125 observe?.(stage);
126 if (!api.AssignProcessToJobObject(handle, currentProcess)) throw new NativeCookieJobError(stage, api.GetLastError());
127 observe?.('job_assigned');
128 } catch (error) {
129 throw error instanceof NativeCookieJobError ? error : new NativeCookieJobError(stage);
130 } finally {
131 let closeError: NativeCookieJobError | undefined;
132 observe?.('process_close');
133 if (currentProcess && !api.CloseHandle(currentProcess)) closeError = new NativeCookieJobError('process_close', api.GetLastError());
134 observe?.('job_close');
135 if (handle && !api.CloseHandle(handle)) closeError ??= new NativeCookieJobError('job_close', api.GetLastError());
136 closeLibrary();
137 if (closeError) throw closeError;
138 }
139}
141async function openKernel() {
142 let stage: NativeCookieDiagnostic['stage'] = 'ffi_import';
143 try {
144 const { dlopen, FFIType, ptr } = await import('bun:ffi');
145 stage = 'ffi_open';
146 const library = dlopen('kernel32.dll', {
147 CreateJobObjectW: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.u64 },
148 OpenJobObjectW: { args: [FFIType.u32, FFIType.i32, FFIType.ptr], returns: FFIType.u64 },
149 SetInformationJobObject: { args: [FFIType.u64, FFIType.u32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
150 QueryInformationJobObject: { args: [FFIType.u64, FFIType.u32, FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
151 AssignProcessToJobObject: { args: [FFIType.u64, FFIType.u64], returns: FFIType.i32 },
152 OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.u64 },
153 TerminateJobObject: { args: [FFIType.u64, FFIType.u32], returns: FFIType.i32 },
154 CloseHandle: { args: [FFIType.u64], returns: FFIType.i32 },
155 GetLastError: { args: [], returns: FFIType.u32 },
156 });
157 return { api: library.symbols, ptr, closeLibrary: () => library.close() };
158 } catch {
159 throw new NativeCookieJobError(stage);
160 }
161}