UNPKG

browser-debugger-cli

Version:

DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.

162 lines 6.31 kB
/** * Worker Launcher - Spawns worker process and waits for ready signal * * This module provides the `launchSessionInWorker()` function that: * 1. Spawns the worker process with JSON config as argv * 2. Waits for worker_ready signal on stdout * 3. Returns worker and Chrome metadata * 4. Handles spawn errors and timeouts */ import { spawn } from 'child_process'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { createLogger } from '../ui/logging/index.js'; import { daemonSpawningWorker, daemonWorkerSpawned, daemonWorkerReady, daemonParseError, } from '../ui/messages/debug.js'; import { getErrorMessage } from '../utils/errors.js'; import { filterDefined } from '../utils/objects.js'; import { validateUrl } from '../utils/url.js'; const log = createLogger('daemon'); /** * Type guard to validate worker_ready message structure. */ function isWorkerReadyMessage(obj) { if (typeof obj !== 'object' || obj === null) { return false; } if (!('type' in obj && obj.type === 'worker_ready')) { return false; } return ('workerPid' in obj && typeof obj.workerPid === 'number' && 'chromePid' in obj && typeof obj.chromePid === 'number' && 'port' in obj && typeof obj.port === 'number' && 'target' in obj && typeof obj.target === 'object' && obj.target !== null && 'url' in obj.target); } /** * Error thrown when worker fails to start. */ export class WorkerStartError extends Error { code; details; constructor(message, code, details) { super(message); this.code = code; this.details = details; this.name = 'WorkerStartError'; } } /** * Launch a new worker process and wait for ready signal. * * @param url - Target URL to navigate to * @param options - Worker configuration options * @returns Worker metadata from ready signal * @throws WorkerStartError if worker fails to start */ export async function launchSessionInWorker(url, options = {}) { const validation = validateUrl(url); if (!validation.valid) { throw new WorkerStartError(validation.error ?? 'Invalid URL', 'SPAWN_FAILED', validation.suggestion); } const config = filterDefined({ url, port: options.port ?? 9222, timeout: options.timeout, telemetry: options.telemetry, includeAll: options.includeAll, userDataDir: options.userDataDir, maxBodySize: options.maxBodySize, headless: options.headless, chromeWsUrl: options.chromeWsUrl, }); const currentDir = dirname(fileURLToPath(import.meta.url)); const workerPath = join(currentDir, 'worker.js'); log.debug(daemonSpawningWorker(workerPath, config)); let worker; try { worker = spawn('node', [workerPath, JSON.stringify(config)], { stdio: ['pipe', 'pipe', 'pipe'], // Enable stdin for IPC detached: true, env: process.env, }); } catch (error) { throw new WorkerStartError('Failed to spawn worker process', 'SPAWN_FAILED', getErrorMessage(error)); } if (worker.pid) { log.debug(daemonWorkerSpawned(worker.pid)); } return new Promise((resolve, reject) => { let stdoutBuffer = ''; let stderrBuffer = ''; let resolved = false; const readyTimeout = setTimeout(() => { if (!resolved) { resolved = true; worker.kill('SIGKILL'); reject(new WorkerStartError('Worker did not send ready signal within 40 seconds', 'READY_TIMEOUT', `stderr: ${stderrBuffer}`)); } }, 40000); if (worker.stdout) { worker.stdout.on('data', (chunk) => { stdoutBuffer += chunk.toString('utf-8'); const lines = stdoutBuffer.split('\n'); stdoutBuffer = lines.pop() ?? ''; // Keep incomplete line in buffer for (const line of lines) { if (!line.trim()) continue; try { const parsed = JSON.parse(line); if (!isWorkerReadyMessage(parsed)) { continue; } if (!resolved) { resolved = true; clearTimeout(readyTimeout); log.debug(daemonWorkerReady(parsed.workerPid, parsed.chromePid)); // NOTE: Don't unref() - we need to keep the worker reference for IPC resolve({ workerPid: parsed.workerPid, chromePid: parsed.chromePid, port: parsed.port, targetUrl: parsed.target.url, ...(parsed.target.title && { targetTitle: parsed.target.title }), workerProcess: worker, // Return worker process for IPC }); } } catch (error) { log.debug(daemonParseError(line)); log.debug(`JSON parse error: ${getErrorMessage(error)}`); } } }); } if (worker.stderr) { worker.stderr.on('data', (chunk) => { stderrBuffer += chunk.toString('utf-8'); process.stderr.write(chunk); }); } worker.on('exit', (code, signal) => { if (!resolved) { resolved = true; clearTimeout(readyTimeout); reject(new WorkerStartError(`Worker process exited before sending ready signal (code: ${code}, signal: ${signal})`, 'WORKER_CRASH', `stderr: ${stderrBuffer}`)); } }); worker.on('error', (error) => { if (!resolved) { resolved = true; clearTimeout(readyTimeout); reject(new WorkerStartError('Worker process spawn error', 'SPAWN_FAILED', error.message)); } }); }); } //# sourceMappingURL=startSession.js.map