///| Browser process launcher (JS target)
///| Uses chromium-bidi's WebSocketServer for full BiDi protocol support.
///|
/// Launch chromium-bidi WebSocket server and create a BiDi session.
/// Returns the BiDi WebSocket URL (ws://localhost:PORT/session/ID).
extern "js" fn ffi_launch_browser_process(headless : Bool) -> @js_async.Promise[String] =
#| async (headless) => {
#| const { existsSync } = await import('node:fs');
#| const { WebSocketServer } = await import('chromium-bidi/bidiServer/WebSocketServer.js');
#| const cp = await import('node:child_process');
#|
#| const candidates = [
#| process.env.CHROME_PATH,
#| process.env.BROWSER_BIN,
#| '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
#| '/Applications/Chromium.app/Contents/MacOS/Chromium',
#| '/usr/bin/google-chrome',
#| '/usr/bin/google-chrome-stable',
#| '/usr/bin/chromium',
#| '/usr/bin/chromium-browser',
#| '/snap/bin/chromium',
#| ].filter(Boolean);
#|
#| let chromeBinary = null;
#| for (const p of candidates) {
#| if (existsSync(p)) { chromeBinary = p; break; }
#| }
#| if (!chromeBinary) {
#| throw new Error('Chrome/Chromium not found. Set CHROME_PATH env var.');
#| }
#| process.env.BROWSER_BIN = chromeBinary;
#|
#| const port = 9990 + Math.floor(Math.random() * 1000);
#| const server = new WebSocketServer(port, false);
#|
#| if (!globalThis.__pw_chrome_pids) globalThis.__pw_chrome_pids = new Set();
#|
#| // Register synchronous exit handler once
#| if (!globalThis.__pw_exit_registered) {
#| globalThis.__pw_exit_registered = true;
#| const killAll = () => {
#| for (const pid of (globalThis.__pw_chrome_pids || [])) {
#| try { process.kill(pid, 'SIGKILL'); } catch {}
#| }
#| };
#| process.on('exit', killAll);
#| process.on('SIGINT', () => { killAll(); process.exit(130); });
#| process.on('SIGTERM', () => { killAll(); process.exit(143); });
#| }
#|
#| await new Promise(r => setTimeout(r, 500));
#|
#| const sessionResp = await fetch(`http://127.0.0.1:${port}/session`, {
#| method: 'POST',
#| headers: { 'Content-Type': 'application/json' },
#| body: JSON.stringify({
#| capabilities: {
#| alwaysMatch: {
#| 'goog:chromeOptions': {
#| binary: chromeBinary,
#| args: headless ? ['--headless=new'] : [],
#| },
#| },
#| },
#| }),
#| });
#|
#| const sessionData = await sessionResp.json();
#| const wsUrl = sessionData.value?.capabilities?.webSocketUrl;
#| if (!wsUrl) {
#| throw new Error('Failed to create BiDi session: ' + JSON.stringify(sessionData));
#| }
#|
#| // Track Chrome PIDs spawned by chromium-bidi
#| try {
#| const output = cp.execSync(
#| "pgrep -f 'chrome.*--remote-debugging-pipe' 2>/dev/null || true",
#| { encoding: 'utf8' }
#| ).trim();
#| for (const line of output.split('\n')) {
#| const pid = parseInt(line, 10);
#| if (pid > 0) globalThis.__pw_chrome_pids.add(pid);
#| }
#| } catch {}
#|
#| return wsUrl;
#| }
///|
/// Close all BiDi servers and kill spawned Chrome processes
extern "js" fn ffi_kill_browser_processes() -> Unit =
#| () => {
#| for (const pid of (globalThis.__pw_chrome_pids || [])) {
#| try { process.kill(pid, 'SIGKILL'); } catch {}
#| }
#| if (globalThis.__pw_chrome_pids) globalThis.__pw_chrome_pids.clear();
#| }
///|
/// Launch browser process and return BiDi WS endpoint URL
async fn launch_browser_process(headless : Bool) -> String {
ffi_launch_browser_process(headless).wait()
}