/** A self-contained program sent to the target, owning Firefox and its native WebSocket. */ export async function run(): Promise { // owns firefox and its profile, so both go once its stdin closes: when berry closes the session, or // however berry exits. it ignores the terminal's signals and leaves shutting down to berry const WATCHDOG = ` profile=$(mktemp -d "\${TMPDIR:-/tmp}/berry-agent-browser-XXXXXXXX") || exit 1 exec 3<&0 firefox --profile "$profile" "$@" 3<&- /dev/null & watcher=$! wait $browser kill -KILL $watcher 2>/dev/null rm -rf "$profile" `; const child = new Deno.Command("sh", { args: [ "-c", WATCHDOG, "berry-agent-browser", "--headless", "--no-remote", "--remote-debugging-port", "0", "about:blank", ], stdin: "piped", stdout: "null", stderr: "piped", }).spawn(); const output = Deno.stdout.writable.getWriter(); const encoder = new TextEncoder(); const stopped = Promise.withResolvers(); let ended = false; let failure: unknown; let socket: WebSocket | undefined; const stop = (error?: unknown) => { if (ended) return; ended = true; failure = error; stopped.resolve(); }; const send = (text: string) => output.write(encoder.encode(text + "\n")); void (async () => { let buffered = ""; for await (const chunk of Deno.stdin.readable.pipeThrough(new TextDecoderStream())) { buffered += chunk; let newline: number; while ((newline = buffered.indexOf("\n")) >= 0) { if (socket?.readyState !== WebSocket.OPEN) { throw new Error("Firefox connection is not open."); } socket.send(buffered.slice(0, newline)); buffered = buffered.slice(newline + 1); } } stop(); })().catch(stop); try { await Promise.race([ (async () => { const endpoint = await new Promise((resolve, reject) => { const timeout = setTimeout( () => reject(new Error("Firefox did not start within 10 seconds.")), 10_000, ); let seen = ""; void (async () => { try { // drain even after finding the endpoint, or firefox can block on stderr for await (const chunk of child.stderr.pipeThrough(new TextDecoderStream())) { seen = (seen + chunk).slice(-16_384); const match = seen.match(/WebDriver BiDi listening on (ws:\/\/[^\r\n]+)\r?\n/); if (match) { clearTimeout(timeout); resolve(`${match[1]}/session`); } } reject(new Error(seen.trim() || "Firefox exited before starting.")); } catch (error) { reject(error); } finally { clearTimeout(timeout); } })(); }); if (ended) return; const ws = socket = new WebSocket(endpoint); ws.addEventListener("message", (event) => { try { void send(JSON.stringify(JSON.parse(String(event.data)))).catch(stop); } catch (error) { stop(error); } }); ws.addEventListener("close", () => stop(new Error("Firefox connection closed."))); await new Promise((resolve, reject) => { const timeout = setTimeout( () => reject(new Error("Firefox connection timed out.")), 10_000, ); ws.addEventListener("open", () => { clearTimeout(timeout); resolve(); }, { once: true }); ws.addEventListener("error", () => { clearTimeout(timeout); reject(new Error("Could not connect to Firefox.")); }, { once: true }); ws.addEventListener("close", () => { clearTimeout(timeout); reject(new Error("Firefox connection closed.")); }, { once: true }); }); await send(JSON.stringify({ type: "ready" })); })(), stopped.promise, ]); await stopped.promise; } catch (error) { stop(error); } finally { if (failure) { await send(JSON.stringify({ type: "error", message: String(failure) })).catch(() => {}); } socket?.close(); await child.stdin.close().catch(() => {}); await child.status; await output.close().catch(() => {}); } Deno.exit(failure ? 1 : 0); }