///| Chrome BiDi Pipe Transport (JS target)
///|
///| Launches Chrome with --remote-debugging-pipe + --enable-features=WebDriverBiDi
///| and communicates via stdio fd 3 (write) and fd 4 (read) using null-delimited JSON.
///|
/// Launch Chrome with BiDi pipe and return a connected transport.
pub fn Transport::chrome_bidi_pipe(headless? : Bool = true) -> Transport {
ChromePipe(ChromePipeState::new(headless))
}
///|
async fn ensure_chrome_pipe_connected(
state : ChromePipeState,
) -> Result[Int, TransportError] {
if state.closed.val {
return Err(Closed)
}
match state.proc_id.val {
Some(id) => Ok(id)
None => {
let id = @js_async.Promise::wait(ffi_launch_chrome_pipe(state.headless)) catch {
e => return Err(NetworkError(message=e.to_string()))
}
if id < 0 {
Err(NetworkError(message="Chrome not found. Set CHROME_PATH env var."))
} else {
state.proc_id.val = Some(id)
Ok(id)
}
}
}
}
// =============================================================================
// FFI: Chrome process management with BiDi pipe (ESM-compatible)
// =============================================================================
///|
extern "js" fn ffi_launch_chrome_pipe(headless : Bool) -> @js_async.Promise[Int] =
#| async (headless) => {
#| const { spawn } = await import('node:child_process');
#| const { existsSync } = await import('node:fs');
#| const os = await import('node:os');
#|
#| const candidates = [
#| process.env.CHROME_PATH,
#| '/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 chromePath = null;
#| for (const p of candidates) {
#| if (existsSync(p)) { chromePath = p; break; }
#| }
#| if (!chromePath) return -1;
#|
#| const g = globalThis;
#| if (!g.__chrome_pipes) g.__chrome_pipes = { nextId: 1, procs: new Map() };
#|
#| const id = g.__chrome_pipes.nextId++;
#| const args = [
#| headless ? '--headless=new' : '',
#| '--remote-debugging-pipe',
#| '--enable-features=WebDriverBiDi',
#| '--no-first-run',
#| '--no-default-browser-check',
#| '--disable-background-networking',
#| '--disable-default-apps',
#| '--disable-extensions',
#| '--disable-hang-monitor',
#| '--disable-popup-blocking',
#| '--disable-sync',
#| '--disable-translate',
#| '--metrics-recording-only',
#| '--safebrowsing-disable-auto-update',
#| '--user-data-dir=' + os.tmpdir() + '/mbt-bidi-' + process.pid + '-' + id,
#| ].filter(Boolean);
#|
#| const proc = spawn(chromePath, args, {
#| stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
#| });
#|
#| const writePipe = proc.stdio[3];
#| const readPipe = proc.stdio[4];
#|
#| const queue = [];
#| const waiters = [];
#| let buf = '';
#|
#| readPipe.setEncoding('utf8');
#| readPipe.on('data', (chunk) => {
#| buf += chunk;
#| let idx;
#| while ((idx = buf.indexOf('\0')) !== -1) {
#| const msg = buf.slice(0, idx);
#| buf = buf.slice(idx + 1);
#| if (msg.length > 0) {
#| if (waiters.length > 0) {
#| waiters.shift()(msg);
#| } else {
#| queue.push(msg);
#| }
#| }
#| }
#| });
#|
#| readPipe.on('end', () => {
#| while (waiters.length > 0) waiters.shift()(null);
#| });
#|
#| g.__chrome_pipes.procs.set(id, { proc, writePipe, readPipe, queue, waiters });
#| return id;
#| }
///|
extern "js" fn ffi_chrome_pipe_send(id : Int, message : String) -> Unit =
#| (id, message) => {
#| const entry = globalThis.__chrome_pipes?.procs.get(id);
#| if (!entry) throw new Error('Chrome pipe not found');
#| entry.writePipe.write(message + '\0');
#| }
///|
extern "js" fn ffi_chrome_pipe_recv(id : Int) -> @js_async.Promise[String?] =
#| (id) => {
#| const entry = globalThis.__chrome_pipes?.procs.get(id);
#| if (!entry) throw new Error('Chrome pipe not found');
#| if (entry.queue.length > 0) return Promise.resolve(entry.queue.shift());
#| return new Promise((resolve) => entry.waiters.push(resolve));
#| }
///|
extern "js" fn ffi_chrome_pipe_close(id : Int) -> Unit =
#| (id) => {
#| const entry = globalThis.__chrome_pipes?.procs.get(id);
#| if (!entry) return;
#| try { entry.proc.kill('SIGTERM'); } catch {}
#| globalThis.__chrome_pipes.procs.delete(id);
#| }