import * as net from 'net'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { BrowserManager } from './browser.js'; import { parseCommand, serializeResponse, errorResponse } from './protocol.js'; import { executeCommand } from './actions.js'; // Platform detection const isWindows = process.platform === 'win32'; // Session support - each session gets its own socket/pid let currentSession = process.env.AGENT_BROWSER_SESSION || 'default'; /** * Set the current session */ export function setSession(session: string): void { currentSession = session; } /** * Get the current session */ export function getSession(): string { return currentSession; } /** * Get port number for TCP mode (Windows) * Uses a hash of the session name to get a consistent port */ function getPortForSession(session: string): number { let hash = 0; for (let i = 0; i < session.length; i++) { hash = (hash << 5) - hash + session.charCodeAt(i); hash |= 0; } // Port range 49152-65535 (dynamic/private ports) return 49152 + (Math.abs(hash) % 16383); } /** * Get the socket path for the current session (Unix) or port (Windows) */ export function getSocketPath(session?: string): string { const sess = session ?? currentSession; if (isWindows) { return String(getPortForSession(sess)); } return path.join(os.tmpdir(), `agent-browser-${sess}.sock`); } /** * Get the port file path for Windows (stores the port number) */ export function getPortFile(session?: string): string { const sess = session ?? currentSession; return path.join(os.tmpdir(), `agent-browser-${sess}.port`); } /** * Get the PID file path for the current session */ export function getPidFile(session?: string): string { const sess = session ?? currentSession; return path.join(os.tmpdir(), `agent-browser-${sess}.pid`); } /** * Check if daemon is running for the current session */ export function isDaemonRunning(session?: string): boolean { const pidFile = getPidFile(session); if (!fs.existsSync(pidFile)) return false; try { const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); // Check if process exists (works on both Unix and Windows) process.kill(pid, 0); return true; } catch { // Process doesn't exist, clean up stale files cleanupSocket(session); return false; } } /** * Get connection info for the current session * Returns { type: 'unix', path: string } or { type: 'tcp', port: number } */ export function getConnectionInfo( session?: string ): { type: 'unix'; path: string } | { type: 'tcp'; port: number } { const sess = session ?? currentSession; if (isWindows) { return { type: 'tcp', port: getPortForSession(sess) }; } return { type: 'unix', path: path.join(os.tmpdir(), `agent-browser-${sess}.sock`) }; } /** * Clean up socket and PID file for the current session */ export function cleanupSocket(session?: string): void { const pidFile = getPidFile(session); try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); if (isWindows) { const portFile = getPortFile(session); if (fs.existsSync(portFile)) fs.unlinkSync(portFile); } else { const socketPath = getSocketPath(session); if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath); } } catch { // Ignore cleanup errors } } /** * Start the daemon server */ export async function startDaemon(): Promise { // Clean up any stale socket cleanupSocket(); const browser = new BrowserManager(); let shuttingDown = false; const server = net.createServer((socket) => { let buffer = ''; socket.on('data', async (data) => { buffer += data.toString(); // Process complete lines while (buffer.includes('\n')) { const newlineIdx = buffer.indexOf('\n'); const line = buffer.substring(0, newlineIdx); buffer = buffer.substring(newlineIdx + 1); if (!line.trim()) continue; try { const parseResult = parseCommand(line); if (!parseResult.success) { const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error); socket.write(serializeResponse(resp) + '\n'); continue; } // Auto-launch browser if not already launched and this isn't a launch command if ( !browser.isLaunched() && parseResult.command.action !== 'launch' && parseResult.command.action !== 'close' ) { await browser.launch({ id: 'auto', action: 'launch', headless: true, executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH, }); } // Handle close command specially if (parseResult.command.action === 'close') { const response = await executeCommand(parseResult.command, browser); socket.write(serializeResponse(response) + '\n'); if (!shuttingDown) { shuttingDown = true; setTimeout(() => { server.close(); cleanupSocket(); process.exit(0); }, 100); } return; } const response = await executeCommand(parseResult.command, browser); socket.write(serializeResponse(response) + '\n'); } catch (err) { const message = err instanceof Error ? err.message : String(err); socket.write(serializeResponse(errorResponse('error', message)) + '\n'); } } }); socket.on('error', () => { // Client disconnected, ignore }); }); const pidFile = getPidFile(); // Write PID file before listening fs.writeFileSync(pidFile, process.pid.toString()); if (isWindows) { // Windows: use TCP socket on localhost const port = getPortForSession(currentSession); const portFile = getPortFile(); fs.writeFileSync(portFile, port.toString()); server.listen(port, '127.0.0.1', () => { // Daemon is ready on TCP port }); } else { // Unix: use Unix domain socket const socketPath = getSocketPath(); server.listen(socketPath, () => { // Daemon is ready }); } server.on('error', (err) => { console.error('Server error:', err); cleanupSocket(); process.exit(1); }); // Handle shutdown signals const shutdown = async () => { if (shuttingDown) return; shuttingDown = true; await browser.close(); server.close(); cleanupSocket(); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); process.on('SIGHUP', shutdown); // Handle unexpected errors - always cleanup process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); cleanupSocket(); process.exit(1); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled rejection:', reason); cleanupSocket(); process.exit(1); }); // Cleanup on normal exit process.on('exit', () => { cleanupSocket(); }); // Keep process alive process.stdin.resume(); } // Run daemon if this is the entry point if (process.argv[1]?.endsWith('daemon.js') || process.env.AGENT_BROWSER_DAEMON === '1') { startDaemon().catch((err) => { console.error('Daemon error:', err); cleanupSocket(); process.exit(1); }); }