import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'; import type { RobotHotspotAdapter, RobotHotspotAdapterCandidate } from './adapter'; import { RobotHotspotAdapterError, abortableDelay, throwIfAborted } from './adapter'; const WORKER_KIND = 'makelore-robot-hotspot-macos'; const LOCATION_NOT_DETERMINED = 0; const LOCATION_RESTRICTED = 1; const LOCATION_DENIED = 2; const LOCATION_AUTHORIZED_ALWAYS = 3; const LOCATION_AUTHORIZED_WHEN_IN_USE = 4; const CORE_WLAN_PERMISSION_DENIED = -3930; const CORE_WLAN_UNSUPPORTED = -3903; const SCAN_RETRY_DELAY_MS = 250; type StableReason = 'unsupported' | 'permission_denied' | 'scan_failed' | 'connect_failed'; type ObjcId = unknown; interface WorkerCandidate { ssid: string; signalPercent: number; connected: boolean; open: boolean; } export interface MacosScanNetwork { ssid: string | null; rssi: number; open: boolean; } export function scanMacosCandidatesWithRetry( readNetworks: () => MacosScanNetwork[], currentSsid: string | null, waitBeforeRetry: () => void, ): WorkerCandidate[] { let networks = readNetworks(); if (networks.length === 0 || networks.every((network) => !network.ssid)) { waitBeforeRetry(); networks = readNetworks(); } if (networks.length > 0 && networks.every((network) => !network.ssid)) { throw new RobotHotspotAdapterError('permission_denied'); } return networks.flatMap((network): WorkerCandidate[] => { if (!network.open || !network.ssid) return []; return [{ ssid: network.ssid, signalPercent: Math.max(0, Math.min(100, Math.round((network.rssi + 100) * 2))), connected: currentSsid === network.ssid, open: true, }]; }); } type WorkerRequest = { id: number; operation: 'scan' } | { id: number; operation: 'connect'; ssid: string }; type WorkerResponse = { id: number; ok: true; result: WorkerCandidate[] | { ssid: string } } | { id: number; ok: false; reason: StableReason }; export interface MacosWorkerLike { postMessage(value: WorkerRequest): void; once(event: 'message', listener: (value: WorkerResponse) => void): this; once(event: 'error', listener: (error: Error) => void): this; once(event: 'exit', listener: (code: number) => void): this; off(event: 'message', listener: (value: WorkerResponse) => void): this; off(event: 'error', listener: (error: Error) => void): this; off(event: 'exit', listener: (code: number) => void): this; terminate(): Promise; } interface WorkerLifecycle { terminate?: () => Promise; exited?: () => void; } export function requestMacosWorker(worker: MacosWorkerLike, request: WorkerRequest, signal: AbortSignal, lifecycle: WorkerLifecycle = {}): Promise { const terminate = lifecycle.terminate ?? (() => worker.terminate()); if (signal.aborted) { void terminate(); return Promise.reject(signal.reason); } return new Promise((resolve, reject) => { const cleanup = () => { worker.off('message', onMessage); worker.off('error', onError); worker.off('exit', onExit); signal.removeEventListener('abort', onAbort); }; const onMessage = (response: WorkerResponse) => { if (response.id !== request.id) return; cleanup(); if (response.ok) resolve(response.result as T); else reject(new RobotHotspotAdapterError(response.reason)); }; const onError = () => { cleanup(); void terminate(); reject(new RobotHotspotAdapterError(request.operation === 'scan' ? 'scan_failed' : 'connect_failed')); }; const onExit = () => { cleanup(); lifecycle.exited?.(); reject(new RobotHotspotAdapterError(request.operation === 'scan' ? 'scan_failed' : 'connect_failed')); }; const onAbort = () => { cleanup(); void terminate(); reject(signal.reason); }; worker.once('message', onMessage); worker.once('error', onError); worker.once('exit', onExit); signal.addEventListener('abort', onAbort, { once: true }); try { worker.postMessage(request); } catch { cleanup(); void terminate(); reject(new RobotHotspotAdapterError(request.operation === 'scan' ? 'scan_failed' : 'connect_failed')); } }); } type WorkerRequestInput = { operation: 'scan' } | { operation: 'connect'; ssid: string }; export class MacosWorkerController { private worker: MacosWorkerLike | undefined; private requestId = 0; private terminationBarrier: Promise = Promise.resolve(); private readonly terminations = new WeakMap>(); constructor(private readonly createWorker: () => MacosWorkerLike) {} private retire(target: MacosWorkerLike): Promise { let termination = this.terminations.get(target as object); if (!termination) { termination = target.terminate(); this.terminations.set(target as object, termination); const previousBarrier = this.terminationBarrier; const settled = termination.then(() => undefined, () => undefined); this.terminationBarrier = Promise.all([previousBarrier, settled]).then(() => undefined); } if (this.worker === target) this.worker = undefined; return termination; } private markExited(target: MacosWorkerLike): void { if (this.worker === target) this.worker = undefined; } private getWorker(): MacosWorkerLike { if (this.worker) return this.worker; const target = this.createWorker(); target.once('error', () => { void this.retire(target); }); target.once('exit', () => { this.markExited(target); }); this.worker = target; return target; } async request(value: WorkerRequestInput, signal: AbortSignal): Promise { await this.terminationBarrier; throwIfAborted(signal); const target = this.getWorker(); return requestMacosWorker(target, { ...value, id: ++this.requestId } as WorkerRequest, signal, { terminate: () => this.retire(target), exited: () => this.markExited(target), }); } async ready(signal: AbortSignal): Promise { await this.terminationBarrier; throwIfAborted(signal); } clear(): void { if (this.worker) void this.retire(this.worker); } } async function runCoreWlanWorker(): Promise { if (!parentPort) return; const koffiModule = await import('koffi'); const koffi = (koffiModule as typeof koffiModule & { default: typeof koffiModule }).default; const objc = koffi.load('/usr/lib/libobjc.A.dylib'); koffi.load('/System/Library/Frameworks/Foundation.framework/Foundation', { global: true }); koffi.load('/System/Library/Frameworks/CoreWLAN.framework/CoreWLAN', { global: true }); const objcGetClass = objc.func('objc_getClass', 'void *', ['str']); const selRegisterName = objc.func('sel_registerName', 'void *', ['str']); const sendId = objc.func('objc_msgSend', 'void *', ['void *', 'void *']); const sendBool = objc.func('objc_msgSend', 'bool', ['void *', 'void *']); const sendLong = objc.func('objc_msgSend', 'long', ['void *', 'void *']); const sendVoid = objc.func('objc_msgSend', 'void', ['void *', 'void *']); const sendObjectAtIndex = objc.func('objc_msgSend', 'void *', ['void *', 'void *', 'size_t']); const sendSupportsSecurity = objc.func('objc_msgSend', 'bool', ['void *', 'void *', 'long']); const sendObjectError = objc.func('objc_msgSend', 'void *', ['void *', 'void *', 'void *', koffi.out(koffi.pointer('void *'))]); const sendAssociate = objc.func('objc_msgSend', 'bool', ['void *', 'void *', 'void *', 'void *', koffi.out(koffi.pointer('void *'))]); const sendUtf8 = objc.func('objc_msgSend', 'str', ['void *', 'void *']); const selector = (name: string): ObjcId => selRegisterName(name); const cls = (name: string): ObjcId => objcGetClass(name); const selectors = { alloc: selector('alloc'), init: selector('init'), drain: selector('drain'), sharedWiFiClient: selector('sharedWiFiClient'), interface: selector('interface'), powerOn: selector('powerOn'), serviceActive: selector('serviceActive'), scan: selector('scanForNetworksWithName:error:'), associate: selector('associateToNetwork:password:error:'), ssid: selector('ssid'), rssiValue: selector('rssiValue'), supportsSecurity: selector('supportsSecurity:'), allObjects: selector('allObjects'), count: selector('count'), objectAtIndex: selector('objectAtIndex:'), UTF8String: selector('UTF8String'), code: selector('code'), }; const pollSleep = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)); function withPool(body: () => T): T { const pool = sendId(sendId(cls('NSAutoreleasePool'), selectors.alloc), selectors.init); try { return body(); } finally { if (pool) sendVoid(pool, selectors.drain); } } function fail(error: ObjcId, operation: 'scan' | 'connect'): never { const code = error ? Number(sendLong(error, selectors.code)) : null; if (code === CORE_WLAN_PERMISSION_DENIED) throw new RobotHotspotAdapterError('permission_denied'); if (code === CORE_WLAN_UNSUPPORTED) throw new RobotHotspotAdapterError('unsupported'); throw new RobotHotspotAdapterError(operation === 'scan' ? 'scan_failed' : 'connect_failed'); } function stringValue(value: ObjcId): string | null { if (!value) return null; const result = sendUtf8(value, selectors.UTF8String); return typeof result === 'string' ? result : null; } function wifiInterface(operation: 'scan' | 'connect'): ObjcId { const client = sendId(cls('CWWiFiClient'), selectors.sharedWiFiClient); const wifi = client && sendId(client, selectors.interface); if (!wifi) throw new RobotHotspotAdapterError('unsupported'); if (!sendBool(wifi, selectors.powerOn) || !sendBool(wifi, selectors.serviceActive)) { throw new RobotHotspotAdapterError(operation === 'scan' ? 'scan_failed' : 'connect_failed'); } return wifi; } function scanNetworks(operation: 'scan' | 'connect'): ObjcId[] { const wifi = wifiInterface(operation); const error = [null] as unknown[]; const set = sendObjectError(wifi, selectors.scan, null, error); if (!set) fail(error[0], operation); const array = sendId(set, selectors.allObjects); const count = Math.min(Number(sendLong(array, selectors.count)), 512); return Array.from({ length: count }, (_, index) => sendObjectAtIndex(array, selectors.objectAtIndex, index)); } function scan(): WorkerCandidate[] { return withPool(() => { const wifi = wifiInterface('scan'); const current = stringValue(sendId(wifi, selectors.ssid)); const readNetworks = (): MacosScanNetwork[] => scanNetworks('scan').map((network) => ({ ssid: stringValue(sendId(network, selectors.ssid)), rssi: Number(sendLong(network, selectors.rssiValue)), open: sendSupportsSecurity(network, selectors.supportsSecurity, 0), })); return scanMacosCandidatesWithRetry( readNetworks, current, () => { Atomics.wait(pollSleep, 0, 0, SCAN_RETRY_DELAY_MS); }, ); }); } function connect(ssid: string): { ssid: string } { return withPool(() => { const wifi = wifiInterface('connect'); const network = scanNetworks('connect').find((item) => stringValue(sendId(item, selectors.ssid)) === ssid && sendSupportsSecurity(item, selectors.supportsSecurity, 0)); if (!network) throw new RobotHotspotAdapterError('connect_failed'); const error = [null] as unknown[]; if (!sendAssociate(wifi, selectors.associate, network, null, error)) fail(error[0], 'connect'); const deadline = Date.now() + 10_000; while (Date.now() < deadline) { if (stringValue(sendId(wifi, selectors.ssid)) === ssid) return { ssid }; Atomics.wait(pollSleep, 0, 0, 150); } throw new RobotHotspotAdapterError('connect_failed'); }); } parentPort.on('message', (request: WorkerRequest) => { try { const result = request.operation === 'scan' ? scan() : connect(request.ssid); parentPort.postMessage({ id: request.id, ok: true, result } satisfies WorkerResponse); } catch (error) { const reason = error instanceof RobotHotspotAdapterError ? error.reason : request.operation === 'scan' ? 'scan_failed' : 'connect_failed'; parentPort.postMessage({ id: request.id, ok: false, reason } satisfies WorkerResponse); } }); } if (!isMainThread && (workerData as { kind?: string } | null)?.kind === WORKER_KIND) { void runCoreWlanWorker().catch(() => { process.exitCode = 1; }); } export async function createMacosRobotHotspotAdapter(): Promise { if (process.platform !== 'darwin') throw new RobotHotspotAdapterError('unsupported'); const koffiModule = await import('koffi'); const koffi = (koffiModule as typeof koffiModule & { default: typeof koffiModule }).default; const objc = koffi.load('/usr/lib/libobjc.A.dylib'); koffi.load('/System/Library/Frameworks/Foundation.framework/Foundation', { global: true }); koffi.load('/System/Library/Frameworks/CoreLocation.framework/CoreLocation', { global: true }); const objcGetClass = objc.func('objc_getClass', 'void *', ['str']); const selRegisterName = objc.func('sel_registerName', 'void *', ['str']); const sendId = objc.func('objc_msgSend', 'void *', ['void *', 'void *']); const sendLong = objc.func('objc_msgSend', 'long', ['void *', 'void *']); const sendVoid = objc.func('objc_msgSend', 'void', ['void *', 'void *']); const selector = (name: string): ObjcId => selRegisterName(name); const cls = (name: string): ObjcId => objcGetClass(name); const selectors = { alloc: selector('alloc'), init: selector('init'), release: selector('release'), authorizationStatus: selector('authorizationStatus'), requestWhenInUseAuthorization: selector('requestWhenInUseAuthorization'), }; let verifiedSsid: string | null = null; const workerController = new MacosWorkerController(() => new Worker(__filename, { workerData: { kind: WORKER_KIND } })); async function ensureLocationAuthorization(signal: AbortSignal): Promise { const manager = sendId(sendId(cls('CLLocationManager'), selectors.alloc), selectors.init); if (!manager) throw new RobotHotspotAdapterError('permission_denied'); try { let status = Number(sendLong(manager, selectors.authorizationStatus)); if (status === LOCATION_NOT_DETERMINED) { sendVoid(manager, selectors.requestWhenInUseAuthorization); const deadline = Date.now() + 8_000; while (status === LOCATION_NOT_DETERMINED && Date.now() < deadline) { await abortableDelay(100, signal); status = Number(sendLong(manager, selectors.authorizationStatus)); } } if (status === LOCATION_RESTRICTED || status === LOCATION_DENIED || status === LOCATION_NOT_DETERMINED) { throw new RobotHotspotAdapterError('permission_denied'); } if (status !== LOCATION_AUTHORIZED_ALWAYS && status !== LOCATION_AUTHORIZED_WHEN_IN_USE) { throw new RobotHotspotAdapterError('permission_denied'); } } finally { sendVoid(manager, selectors.release); } } return { async scan(signal) { await workerController.ready(signal); await ensureLocationAuthorization(signal); throwIfAborted(signal); verifiedSsid = null; const candidates = await workerController.request({ operation: 'scan' }, signal); return candidates.map((candidate): RobotHotspotAdapterCandidate => ({ ...candidate, nativeKey: candidate.ssid })); }, async connect(candidate, signal) { await workerController.ready(signal); await ensureLocationAuthorization(signal); throwIfAborted(signal); const result = await workerController.request<{ ssid: string }>({ operation: 'connect', ssid: candidate.ssid }, signal); verifiedSsid = result.ssid; }, async currentSsid(signal) { throwIfAborted(signal); return verifiedSsid; }, clear() { verifiedSsid = null; workerController.clear(); }, }; }