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
JavaScript
/**
* 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