bao-browser 0.4.0

A Rust-native programmable browser runtime built on Servo and SpiderMonkey
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
// WPT official-toolchain wiring (REQ-BRW-002): the bao binary as the
// WebDriver endpoint upstream wptrunner's `servo` product drives.
//
// Upstream shape (servoshell ports/servoshell/webdriver.rs +
// running_app_state.rs, 2026-09-27 baseline): the `webdriver_server`
// component owns the whole WebDriver HTTP protocol; the embedder only pumps
// `WebDriverCommandMsg` off a channel and answers the embedder-side commands
// (window/page lifecycle, load-status plumbing, dialogs, screenshots) against
// its webviews. This module is that embedder face, mapped onto bao's
// PagePool/PageHandle model — zero protocol code lives here.
//
// The delegate callbacks (load complete / traversal complete / input handled /
// embedder controls) fire on script threads with no back-reference to the
// pump, so the shared bookkeeping lives in a process-global registry (the
// same shape as bao's WEBVIEW_ID_BY_PAGE and servo's R53-A per-WebViewId
// registries). The mutex is held only across map moves and channel sends —
// never across a servo call.

use std::collections::HashMap;
use std::sync::{Arc, LazyLock, Mutex};

use bao_ipc::{Receiver, Sender};
use servo::{
    EmbedderControl, EmbedderControlId, EventLoopWaker, GenericSender, InputEvent, InputEventId,
    NewWindowTypeHint, Preferences, SimpleDialog, TraversalId, WebDriverCommandMsg,
    WebDriverJSResult, WebDriverLoadStatus, WebDriverScriptCommand, WebDriverUserPrompt,
    WebDriverUserPromptAction, WebViewId,
};

/// Poll-loop no-op waker: bao's `run_browser` event loop re-polls
/// `servo.spin_event_loop()` on a bounded 1ms cadence, so a wake signal has
/// no scheduler to interrupt — the next poll drains the webdriver channel on
/// its own (≤1ms latency). Fills `start_server`'s required waker argument
/// without a platform event loop bao does not have.
#[derive(Clone, Copy, Default)]
pub struct SpinLoopWaker;

impl EventLoopWaker for SpinLoopWaker {
    fn clone_box(&self) -> Box<dyn EventLoopWaker> {
        Box::new(SpinLoopWaker)
    }

    fn wake(&self) {
        // No-op: the owning loop re-polls on its fixed cadence (see type doc).
    }
}

/// Cross-thread bookkeeping shared between the command pump (this module, on
/// the event-loop thread) and the servo delegate callbacks (script threads).
#[derive(Default)]
struct WebdriverBridge {
    /// Per-webview load-status channels handed over by `webdriver_server`
    /// (LoadUrl / Refresh / NewWindow / AddLoadStatusSender). Fulfilled by
    /// the delegate's `notify_load_status_changed(Complete)` hook.
    load_status_senders: HashMap<WebViewId, GenericSender<WebDriverLoadStatus>>,
    /// Loads whose `Complete` edge fired with no waiter registered — the
    /// edge is dropped otherwise (e26 livelock root, 2026-10-03): a
    /// `NewWindow` webview's initial about:blank load completes while the
    /// pump thread is still inside `create_webdriver_page` (the pipeline
    /// pumps through `wait_for_pipeline_ready`'s internal spin), so the
    /// Complete reaches [`notify_load_complete`] BEFORE the drain arm
    /// inserts the session's load-status sender. Without this latch the
    /// edge is lost, `webdriver_server`'s `wait_document_ready` then waits
    /// out the full page-load timeout (300s default) for a second Complete
    /// that never comes, and the serial dispatcher wedges every subsequent
    /// WebDriver command in the process (wptrunner: browser killed →
    /// CRASH; probability rises with concurrency because contention
    /// randomizes whether the load or the create wins the race).
    completed_loads: std::collections::HashSet<WebViewId>,
    /// go_back/go_forward traversal → completion channel, keyed by the
    /// `TraversalId` the delegate's `notify_traversal_complete` reports.
    pending_traversals: HashMap<TraversalId, GenericSender<WebDriverLoadStatus>>,
    /// Input events whose DOM handling the delegate must acknowledge
    /// (`notify_input_event_handled`).
    pending_input_events: HashMap<InputEventId, Sender<()>>,
    /// Input events whose handled edge fired with no waiter registered —
    /// replayed by the dispatch arm's insert (see
    /// [`notify_input_event_handled`]).
    handled_input_events: std::collections::HashSet<InputEventId>,
    /// The single in-flight WebDriver script evaluation's response channel —
    /// servoshell's interrupt face for "user prompt during script
    /// evaluation" (a dialog resolves the evaluation with null).
    script_interrupt: Option<GenericSender<WebDriverJSResult>>,
    /// Embedder controls (dialogs, pickers) servo asked the embedder to show
    /// while WebDriver owns the session. webdriver_server's prompt endpoints
    /// read this as "the current user prompt".
    embedder_controls: HashMap<WebViewId, Vec<EmbedderControl>>,
}

static WEBDRIVER_BRIDGE: LazyLock<Mutex<WebdriverBridge>> =
    LazyLock::new(|| Mutex::new(WebdriverBridge::default()));

/// Run `f` over the bridge; `None` = the mutex was poisoned (a delegate
/// callback panicked — the session is dead anyway, dropping the command is
/// the honest degradation).
fn with_bridge<R>(f: impl FnOnce(&mut WebdriverBridge) -> R) -> Option<R> {
    WEBDRIVER_BRIDGE
        .lock()
        .ok()
        .map(|mut bridge| f(&mut bridge))
}

// ─── Delegate hook faces (called from delegate.rs on script threads) ───────

/// `WebViewDelegate::notify_load_status_changed` — resolve the webview's
/// pending WebDriver load-status waiters on `Complete`.
///
/// A Complete with no registered waiter is LATCHED, not dropped (see
/// [`WebdriverBridge::completed_loads`] for the livelock this closes): the
/// waiter's registration site replays it.
pub fn notify_load_complete(webview_id: WebViewId) {
    with_bridge(
        |bridge| match bridge.load_status_senders.remove(&webview_id) {
            Some(sender) => {
                let _ = sender.send(WebDriverLoadStatus::Complete);
                bridge.completed_loads.remove(&webview_id);
            }
            None => {
                bridge.completed_loads.insert(webview_id);
            }
        },
    );
}

/// Register a load-status waiter for `webview_id`, resolving immediately if
/// that webview's Complete already fired (latched) — and otherwise clearing
/// any stale latch so only the NEW navigation's edge can satisfy the wait
/// (LoadUrl inserts BEFORE `navigate()`, so its own Complete cannot have
/// fired yet; anything latched predates this registration).
fn register_load_status_sender(
    bridge: &mut WebdriverBridge,
    webview_id: WebViewId,
    sender: GenericSender<WebDriverLoadStatus>,
    replay_latched: bool,
) {
    if replay_latched && bridge.completed_loads.remove(&webview_id) {
        let _ = sender.send(WebDriverLoadStatus::Complete);
        return;
    }
    bridge.completed_loads.remove(&webview_id);
    bridge.load_status_senders.insert(webview_id, sender);
}

/// `WebViewDelegate::notify_traversal_complete` — resolve the pending
/// go_back/go_forward waiter.
pub fn notify_traversal_complete(traversal_id: TraversalId) {
    with_bridge(|bridge| {
        if let Some(sender) = bridge.pending_traversals.remove(&traversal_id) {
            let _ = sender.send(WebDriverLoadStatus::Complete);
        }
    });
}

/// `WebViewDelegate::notify_input_event_handled` — acknowledge the pending
/// WebDriver input event.
///
/// The handled edge can arrive before the dispatch arm registers the
/// event's sender: the ScriptThread finishes the key handling and reports
/// `InputEventsHandled` (routed to the delegate independently of the pump
/// thread) while the pump is still between `webdriver_dispatch_input_event`
/// and the `pending_input_events` insert. Without the latch the edge is
/// dropped and `wait_for_input_event_handled`'s unbounded `recv` wedges the
/// serial dispatcher forever — the input-event twin of the e26 load-status
/// livelock (same lost-edge race, same latch-replay fix; WPT editing red
/// face 2026-10-09: keydown-hides-editor tests TIMEOUT under load).
pub fn notify_input_event_handled(event_id: InputEventId) {
    with_bridge(|bridge| {
        if let Some(sender) = bridge.pending_input_events.remove(&event_id) {
            let _ = sender.send(());
        } else {
            // Edge before waiter — latch it for replay at insert time.
            // Events dispatched without a response sender (the non-blocking
            // touch path) may latch without ever being drained: one leaked
            // `usize` per such event, the same bounded-growth trade-off as
            // `completed_loads`.
            bridge.handled_input_events.insert(event_id);
        }
    });
}

/// `WebViewDelegate::show_embedder_control` under an active WebDriver
/// session: stash the control for the prompt endpoints, interrupt any
/// in-flight script evaluation (WebDriver spec: a prompt during script
/// evaluation resolves it with null) and report the load as Blocked (a
/// dialog blocks page load).
pub fn show_embedder_control(webview_id: WebViewId, control: EmbedderControl) {
    with_bridge(|bridge| {
        // Only a user prompt (a script-initiated dialog) interrupts an
        // in-flight script evaluation (servoshell: `show_embedder_control`'s
        // webdriver branch gates `interrupt_webdriver_script_evaluation` on
        // `SimpleDialog` — the WebDriver spec's "user prompt during script
        // evaluation resolves it with null"). Interrupting on other controls
        // breaks the wptrunner testdriver protocol: `InputMethod` fires every
        // time an editable element is focused, and its phantom null resolves
        // the pending async-script poll out-of-band — the page's next
        // testdriver message then lands in the dead evaluation's channel and
        // `performActions` never runs (keydown-hides-editor test family,
        // 2026-10-09 e154 forensics).
        if matches!(control, EmbedderControl::SimpleDialog(..)) {
            if let Some(response_sender) = &bridge.script_interrupt {
                let _ = response_sender.send(Ok(servo::JSValue::Null));
            }
        }

        let is_blocking = matches!(
            &control,
            EmbedderControl::SimpleDialog(..)
                | EmbedderControl::FilePicker { .. }
                | EmbedderControl::SelectElement { .. }
        );
        bridge
            .embedder_controls
            .entry(webview_id)
            .or_default()
            .push(control);

        if is_blocking {
            if let Some(sender) = bridge.load_status_senders.get(&webview_id) {
                let _ = sender.send(WebDriverLoadStatus::Blocked);
            }
        }
    });
}

/// `WebViewDelegate::hide_embedder_control` under an active WebDriver session.
pub fn hide_embedder_control(webview_id: WebViewId, control_id: EmbedderControlId) {
    with_bridge(|bridge| {
        if let Some(controls) = bridge.embedder_controls.get_mut(&webview_id) {
            controls.retain(|control| control.id() != control_id);
        }
        bridge
            .embedder_controls
            .retain(|_, controls| !controls.is_empty());
    });
}

/// Current prompt type for a webview, from the newest stashed control
/// (<https://w3c.github.io/webdriver/#dfn-handle-any-user-prompts> Step 3).
fn current_active_dialog_webdriver_type(
    controls: &[EmbedderControl],
) -> Option<WebDriverUserPrompt> {
    match controls.last()? {
        EmbedderControl::SimpleDialog(SimpleDialog::Alert(..)) => Some(WebDriverUserPrompt::Alert),
        EmbedderControl::SimpleDialog(SimpleDialog::Confirm(..)) => {
            Some(WebDriverUserPrompt::Confirm)
        }
        EmbedderControl::SimpleDialog(SimpleDialog::Prompt(..)) => {
            Some(WebDriverUserPrompt::Prompt)
        }
        EmbedderControl::FilePicker { .. } => Some(WebDriverUserPrompt::File),
        EmbedderControl::SelectElement { .. } => Some(WebDriverUserPrompt::Default),
        _ => None,
    }
}

/// Respond to the newest stashed `SimpleDialog` (accept/dismiss/ignore) and
/// return its message text — the HandleUserPrompt embedder contract.
fn respond_to_active_simple_dialog(
    controls: &mut Vec<EmbedderControl>,
    action: WebDriverUserPromptAction,
) -> Result<String, ()> {
    let Some(EmbedderControl::SimpleDialog(simple_dialog)) = controls.last() else {
        return Err(());
    };
    let result_text = simple_dialog.message().to_owned();
    if action == WebDriverUserPromptAction::Ignore {
        return Ok(result_text);
    }
    let Some(EmbedderControl::SimpleDialog(simple_dialog)) = controls.pop() else {
        return Err(());
    };
    match action {
        WebDriverUserPromptAction::Accept => simple_dialog.confirm(),
        WebDriverUserPromptAction::Dismiss => simple_dialog.dismiss(),
        WebDriverUserPromptAction::Ignore => unreachable!("returned early above"),
    }
    Ok(result_text)
}

/// Host state owned by the pump loop: the receiving end of the embedder
/// channel `webdriver_server::start_server` was handed.
pub struct WebDriverHost {
    receiver: Receiver<WebDriverCommandMsg>,
}

/// NewWindow arm: headless tab/window hints coincide, so both create a
/// fresh about:blank page, answer the sender, register the optional
/// load-status waiter (replay_latched = true: the new webview is
/// brand-new, so a latched Complete can only be its initial about:blank
/// load — exactly the edge this waiter exists to observe; dropping it
/// here is the e26 livelock, see `WebdriverBridge::completed_loads`),
/// and wait for the initial pipeline.
fn webdriver_handle_new_window(runtime: &crate::BrowserRuntime, msg: WebDriverCommandMsg) {
    let WebDriverCommandMsg::NewWindow(_type_hint, response_sender, load_status_sender) = msg
    else {
        return;
    };
    let new_page = url::Url::parse("about:blank")
        .ok()
        .and_then(|url| runtime.create_webdriver_page(url));
    match new_page {
        Some((webview_id, page)) => {
            let _ = response_sender.send(webview_id);
            if let Some(load_status_sender) = load_status_sender {
                with_bridge(|bridge| {
                    register_load_status_sender(bridge, webview_id, load_status_sender, true);
                });
            }
            page.wait_for_pipeline_ready(std::time::Duration::from_secs(15))
                .ok();
        }
        None => {
            log::error!("[webdriver] NewWindow: page creation failed");
            // Sender drop = the HTTP surface reports failure.
        }
    }
}

/// GoBack/GoForward arm tail: register the traversal's load-status sender
/// with the bridge (resolved by `notify_traversal_complete`).
fn webdriver_register_traversal(
    traversal_id: TraversalId,
    load_status_sender: GenericSender<WebDriverLoadStatus>,
) {
    with_bridge(|bridge| {
        bridge
            .pending_traversals
            .insert(traversal_id, load_status_sender);
    });
}

/// InputEvent arm: dispatch into the page and wire the response channel
/// to the handled/pending race — the handled edge can race ahead of this
/// insert, in which case it is replayed so the dispatcher's `recv`
/// resolves instead of waiting on a notification that already fired.
fn webdriver_handle_input_event(
    runtime: &crate::BrowserRuntime,
    webview_id: WebViewId,
    input_event: InputEvent,
    response_sender: Option<Sender<()>>,
) {
    match runtime.page_for_webview(webview_id) {
        Some((_, page)) => {
            let event_id = page.webdriver_dispatch_input_event(input_event);
            if let (Some(event_id), Some(response_sender)) = (event_id, response_sender) {
                with_bridge(|bridge| {
                    if bridge.handled_input_events.remove(&event_id) {
                        let _ = response_sender.send(());
                    } else {
                        bridge
                            .pending_input_events
                            .insert(event_id, response_sender);
                    }
                });
            }
        }
        None => {
            log::error!("[webdriver] InputEvent for unknown webview {webview_id:?}");
        }
    }
}

/// SendAlertText arm: update the active prompt dialog's current value.
fn webdriver_handle_send_alert_text(webview_id: WebViewId, text: String) {
    with_bridge(|bridge| {
        if let Some(controls) = bridge.embedder_controls.get_mut(&webview_id) {
            if let Some(EmbedderControl::SimpleDialog(SimpleDialog::Prompt(
                ref mut prompt_dialog,
            ))) = controls.last_mut()
            {
                prompt_dialog.set_current_value(&text);
            }
        }
    });
}

// ─── Host-process signal exit (REQ-CLI-002, e184) ───────────────────────────
//
// The registry WebDriver HTTP server (`webdriver 0.54::server::start`, spawned
// by `webdriver_server::start_server` on its "WebDriverHttpServer" thread)
// registers its own tokio SIGINT/SIGTERM streams and, on either signal, shuts
// the HTTP face down and quits its dispatcher — e184 RED probe verified the
// tokio path actually fires (TERM closed the listener socket). What nothing in
// the process did was END THE PROCESS: bao's pump loop (`BrowserRuntime::run`,
// webdriver mode) only stops on `exit_scheduled`, which no signal touched — so
// every TERM/INT left an orphaned browser process spinning a core (the e177
// residual-process class).
//
// The fix is a bao-owned SIGINT/SIGTERM face driving the existing graceful
// exit: the handler flips a process-global flag, `drain` (called by the pump
// every iteration) translates it into `runtime.schedule_exit()`, and a
// watchdog hard-bounds the whole teardown at 5s (liveness over perfect
// reclamation — the W27 bounded-join precedent) so a wedged teardown can never
// resurrect the residual-process class.
//
// # Why `signal-hook-registry` and not a raw `libc::sigaction`
//
// Registration must coexist with the registry server's tokio handler no matter
// which registers first. A raw `sigaction` REPLACES the previous disposition:
// bao's handler installed here (before `start_server` spawns the server
// thread) would be silently overwritten ~350ms later when the server thread's
// `ShutdownSignal::new` routes through `signal_hook_registry::register` and
// installs its trampoline. `signal-hook-registry` (the exact crate tokio
// itself goes through) APPENDS to a per-signal action list under one shared
// trampoline, so both actions run on every delivery, order-independently.
// The action body is async-signal-safe by construction: one atomic store,
// nothing else.
#[cfg(unix)]
mod signal_exit {
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::time::Duration;

    /// Set from the signal handler; consumed by
    /// [`WebDriverHost::drain`] → `runtime.schedule_exit()` and by the
    /// watchdog's liveness bound.
    static SIGNAL_EXIT_REQUESTED: AtomicBool = AtomicBool::new(false);

    /// One-time install guard (a second runtime in the process must not
    /// double-register the actions).
    static INSTALLED: AtomicBool = AtomicBool::new(false);

    /// POSIX signal numbers, stable across every unix target bao builds for
    /// (kept literal so the handler face carries no libc surface of its own).
    const SIGINT: i32 = 2;
    const SIGTERM: i32 = 15;

    /// Signal-handler body. Async-signal-safe: one relaxed atomic store —
    /// the pump reads it within its 1ms iteration and does the real work in
    /// normal context.
    fn request_signal_exit() {
        SIGNAL_EXIT_REQUESTED.store(true, Ordering::Relaxed);
    }

    /// Install the bao-side SIGINT/SIGTERM actions. Called from
    /// `WebDriverHost::start` BEFORE `webdriver_server::start_server`, so the
    /// trampoline exists before the server thread's tokio registration appends
    /// to it (see the module doc: with the chaining registry, ordering cannot
    /// break either side).
    pub fn install() {
        if INSTALLED.swap(true, Ordering::AcqRel) {
            return;
        }
        let mut failed = false;
        for signal in [SIGINT, SIGTERM] {
            // SAFETY: `register` chains the action onto signal-hook's
            // process-global registry; the action is async-signal-safe (one
            // atomic store). The id is deliberately dropped without
            // unregistering — the face lives for the whole process, and
            // unregistering would leave the trampoline installed with no
            // actor behind it.
            if unsafe { signal_hook_registry::register(signal, request_signal_exit) }.is_err() {
                failed = true;
            }
        }
        if failed {
            // Fail-loud degradation, never a silent fake success: the signal
            // face is exactly what this module exists for.
            log::error!("[webdriver] SIGINT/SIGTERM registration failed — signal exit degraded");
            eprintln!("bao: webdriver signal handler registration failed — signal exit degraded");
            return;
        }
        spawn_watchdog();
    }

    /// Watchdog poll cadence: both phases poll this flag; 50ms is noise
    /// against the 5s bound and against the pump's 1ms exit latency.
    const POLL: Duration = Duration::from_millis(50);
    /// Whole-teardown liveness bound (contract: signal → process gone ≤5s).
    const EXIT_GRACE: Duration = Duration::from_secs(5);

    /// Liveness bound: once a signal asked for the exit, the graceful path
    /// (pump break → runtime drop → process exit) must finish within
    /// [`EXIT_GRACE`]; past that, hard-exit the way the harness's SIGKILL
    /// fallback would — reclaiming the process beats perfect reclamation.
    fn spawn_watchdog() {
        let spawned = std::thread::Builder::new()
            .name("bao-webdriver-sigwatch".to_string())
            .spawn(|| {
                // Armed phase: idle until the first signal asks for the exit.
                while !SIGNAL_EXIT_REQUESTED.load(Ordering::Relaxed) {
                    std::thread::sleep(POLL);
                }
                // Grace phase: the pump's drain turns the flag into
                // `schedule_exit()` within one iteration; everything after is
                // the existing teardown.
                let start = std::time::Instant::now();
                while start.elapsed() < EXIT_GRACE {
                    std::thread::sleep(POLL);
                }
                log::error!("[webdriver] signal-initiated exit exceeded the 5s bound — hard exit");
                eprintln!("bao: webdriver signal exit exceeded the 5s bound — hard exit");
                // SAFETY: liveness last resort — skips atexit/teardown exactly
                // like the SIGKILL this replaces.
                unsafe { libc::_exit(0) };
            });
        if spawned.is_err() {
            // Degraded but loud: the drain face still exits the pump; only
            // the hard bound is lost.
            log::error!("[webdriver] signal-exit watchdog spawn failed — 5s bound unavailable");
            eprintln!("bao: webdriver signal watchdog spawn failed");
        }
    }

    /// Pump-side consumer: `true` once a signal asked for the exit.
    pub fn exit_requested() -> bool {
        SIGNAL_EXIT_REQUESTED.load(Ordering::Relaxed)
    }
}

impl WebDriverHost {
    /// Start the upstream WebDriver HTTP server for this process and return
    /// the host that must be drained from the event loop.
    pub fn start(port: u16) -> Self {
        // Bao-side SIGINT/SIGTERM face (see the `signal_exit` module doc):
        // installed before the registry server spawns so the shared
        // trampoline is ours first and the server thread's tokio registration
        // appends to it.
        #[cfg(unix)]
        signal_exit::install();
        let preferences = servo::prefs::get().clone();
        let (embedder_sender, receiver) = bao_ipc::unbounded();
        webdriver_server::start_server(port, embedder_sender, Box::new(SpinLoopWaker), preferences);
        Self { receiver }
    }

    /// Drain every queued command, answering embedder-side ones against the
    /// runtime's pages and forwarding engine-side ones into servo
    /// (`Servo::execute_webdriver_command` — the constellation/script-thread
    /// face servo owns). Port of servoshell's `handle_webdriver_messages`.
    pub fn drain(&self, runtime: &crate::BrowserRuntime) {
        // Signal exit face (REQ-CLI-002 e184): a delivered TERM/INT flipped
        // the flag from the signal handler; translate it into the pump's
        // existing exit path — the run loop reads `exit_scheduled` at its
        // next 1ms iteration.
        #[cfg(unix)]
        if signal_exit::exit_requested() {
            runtime.schedule_exit();
        }
        while let Ok(msg) = self.receiver.try_recv() {
            match msg {
                WebDriverCommandMsg::ResetAllCookies(sender) => {
                    runtime.servo().site_data_manager().clear_cookies(None);
                    let _ = sender.send(());
                }
                WebDriverCommandMsg::Shutdown => {
                    runtime.schedule_exit();
                }
                WebDriverCommandMsg::IsWebViewOpen(webview_id, sender) => {
                    let _ = sender.send(runtime.page_for_webview(webview_id).is_some());
                }
                WebDriverCommandMsg::IsBrowsingContextOpen(..) => {
                    // Engine-side: the constellation owns browsing contexts.
                    runtime.servo().execute_webdriver_command(msg);
                }
                WebDriverCommandMsg::NewWindow(..) => {
                    // bao is headless: tab/window hints coincide.
                    webdriver_handle_new_window(runtime, msg);
                }
                WebDriverCommandMsg::CloseWebView(webview_id, response_sender) => {
                    if let Some((_, page)) = runtime.page_for_webview(webview_id) {
                        let _ = runtime.page_pool().close_page(page.id());
                    }
                    with_bridge(|bridge| {
                        bridge.load_status_senders.remove(&webview_id);
                        bridge.completed_loads.remove(&webview_id);
                    });
                    let _ = response_sender.send(());
                }
                WebDriverCommandMsg::FocusWebView(webview_id) => {
                    // Headless: focusing is bookkeeping only; bump the idle
                    // clock so idle reaping never closes a driven page.
                    if let Some((_, page)) = runtime.page_for_webview(webview_id) {
                        page.webdriver_touch();
                    }
                }
                WebDriverCommandMsg::FocusBrowsingContext(..) => {
                    runtime.servo().execute_webdriver_command(msg);
                }
                WebDriverCommandMsg::GetAllWebViews(response_sender) => {
                    let _ = response_sender.send(runtime.webdriver_webview_ids());
                }
                WebDriverCommandMsg::GetWindowRect(webview_id, response_sender) => {
                    let rect = runtime
                        .page_for_webview(webview_id)
                        .map(|(_, page)| page.webdriver_window_rect());
                    let _ = response_sender.send(rect.unwrap_or_default());
                }
                WebDriverCommandMsg::MaximizeWebView(webview_id, response_sender) => {
                    let rect = runtime
                        .page_for_webview(webview_id)
                        .map(|(_, page)| {
                            page.webdriver_touch();
                            page.webdriver_window_rect()
                        })
                        .unwrap_or_default();
                    let _ = response_sender.send(rect);
                }
                WebDriverCommandMsg::SetWindowRect(webview_id, requested_rect, response_sender) => {
                    let rect = runtime
                        .page_for_webview(webview_id)
                        .map(|(_, page)| page.webdriver_set_window_rect(requested_rect))
                        .unwrap_or_default();
                    let _ = response_sender.send(rect);
                }
                WebDriverCommandMsg::GetViewportSize(webview_id, response_sender) => {
                    let size = runtime
                        .page_for_webview(webview_id)
                        .map(|(_, page)| page.webdriver_viewport_size())
                        .unwrap_or_default();
                    let _ = response_sender.send(size);
                }
                // Only received at session start: the focused webview is the
                // initial page's webview.
                WebDriverCommandMsg::GetFocusedWebView(response_sender) => {
                    let _ =
                        response_sender.send(runtime.webdriver_webview_ids().into_iter().next());
                }
                WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => {
                    if let Some((_, page)) = runtime.page_for_webview(webview_id) {
                        // replay_latched = false: the sender is registered
                        // BEFORE `navigate()` fires, so its own Complete
                        // cannot have latched yet — a latch here predates this
                        // registration (boot load) and must not satisfy it.
                        with_bridge(|bridge| {
                            register_load_status_sender(
                                bridge,
                                webview_id,
                                load_status_sender,
                                false,
                            );
                        });
                        if let Err(error) = page.navigate(url.as_str()) {
                            log::error!("[webdriver] LoadUrl failed: {error}");
                        }
                    } else {
                        log::error!("[webdriver] LoadUrl for unknown webview {webview_id:?}");
                    }
                }
                WebDriverCommandMsg::Refresh(webview_id, load_status_sender) => {
                    if let Some((_, page)) = runtime.page_for_webview(webview_id) {
                        with_bridge(|bridge| {
                            register_load_status_sender(
                                bridge,
                                webview_id,
                                load_status_sender,
                                false,
                            );
                        });
                        if let Err(error) = page.reload() {
                            log::error!("[webdriver] Refresh failed: {error}");
                        }
                    }
                }
                WebDriverCommandMsg::GoBack(webview_id, load_status_sender) => {
                    if let Some((_, page)) = runtime.page_for_webview(webview_id) {
                        if let Some(traversal_id) = page.webdriver_go_back() {
                            webdriver_register_traversal(traversal_id, load_status_sender);
                        }
                    }
                }
                WebDriverCommandMsg::GoForward(webview_id, load_status_sender) => {
                    if let Some((_, page)) = runtime.page_for_webview(webview_id) {
                        if let Some(traversal_id) = page.webdriver_go_forward() {
                            webdriver_register_traversal(traversal_id, load_status_sender);
                        }
                    }
                }
                WebDriverCommandMsg::InputEvent(webview_id, input_event, response_sender) => {
                    webdriver_handle_input_event(runtime, webview_id, input_event, response_sender);
                }
                WebDriverCommandMsg::ScriptCommand(_, ref script_command) => {
                    // Embedder-side bookkeeping BEFORE handing the command to
                    // servo (mirrors servoshell's handle_webdriver_script_command):
                    // the in-flight evaluation's response channel is the
                    // interrupt face; Add/RemoveLoadStatusSender move the
                    // load-status plumbing.
                    match script_command {
                        WebDriverScriptCommand::ExecuteScriptWithCallback(_, response_sender) => {
                            with_bridge(|bridge| {
                                bridge.script_interrupt = Some(response_sender.clone());
                            });
                        }
                        WebDriverScriptCommand::AddLoadStatusSender(
                            webview_id,
                            load_status_sender,
                        ) => {
                            // replay_latched = false: registered BEFORE the
                            // click/navigation it tracks (element-click face),
                            // so a latch here is stale, not this navigation.
                            with_bridge(|bridge| {
                                register_load_status_sender(
                                    bridge,
                                    *webview_id,
                                    load_status_sender.clone(),
                                    false,
                                );
                            });
                        }
                        WebDriverScriptCommand::RemoveLoadStatusSender(webview_id) => {
                            with_bridge(|bridge| {
                                bridge.load_status_senders.remove(webview_id);
                            });
                        }
                        _ => {
                            with_bridge(|bridge| bridge.script_interrupt = None);
                        }
                    }
                    runtime.servo().execute_webdriver_command(msg);
                }
                WebDriverCommandMsg::CurrentUserPrompt(webview_id, response_sender) => {
                    let current = with_bridge(|bridge| {
                        bridge
                            .embedder_controls
                            .get(&webview_id)
                            .and_then(|controls| current_active_dialog_webdriver_type(controls))
                    });
                    let _ = response_sender.send(current.unwrap_or(None));
                }
                WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => {
                    let result = with_bridge(|bridge| {
                        let Some(controls) = bridge.embedder_controls.get_mut(&webview_id) else {
                            return Err(());
                        };
                        respond_to_active_simple_dialog(controls, action)
                    });
                    let _ = response_sender.send(result.unwrap_or(Err(())));
                }
                WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => {
                    let text = with_bridge(|bridge| {
                        bridge
                            .embedder_controls
                            .get(&webview_id)
                            .and_then(|controls| controls.last())
                            .and_then(|control| match control {
                                EmbedderControl::SimpleDialog(simple_dialog) => {
                                    Some(simple_dialog.message().to_owned())
                                }
                                _ => None,
                            })
                    });
                    let _ = response_sender.send(text.unwrap_or(None).ok_or(()));
                }
                WebDriverCommandMsg::SendAlertText(webview_id, text) => {
                    webdriver_handle_send_alert_text(webview_id, text);
                }
                WebDriverCommandMsg::TakeScreenshot(webview_id, rect, result_sender) => {
                    match runtime.page_for_webview(webview_id) {
                        Some((_, page)) => page.webdriver_take_screenshot(rect, result_sender),
                        None => {
                            log::error!(
                                "[webdriver] TakeScreenshot for unknown webview {webview_id:?}"
                            );
                            let _ = result_sender
                                .send(Err(servo::ScreenshotCaptureError::WebViewDoesNotExist));
                        }
                    }
                }
                // BAO PATCH (REQ-BRW-002 e106, main-session cross-boundary
                // authorization 2026-10-06): the vendor enum's
                // interaction-selection face (e99 snapshot swap a7272f16
                // replay follow-through — this exhaustive match was
                // non-exhaustive against it, masked until the variant
                // E0599s above were fixed). Headless: selection is
                // bookkeeping only — same shape as the FocusWebView arm
                // (bump the idle clock so idle reaping never closes a
                // driven page).
                WebDriverCommandMsg::SelectWebViewForInteraction(webview_id) => {
                    if let Some((_, page)) = runtime.page_for_webview(webview_id) {
                        page.webdriver_touch();
                    }
                }
                // Only received at session start: the webview selected for
                // interaction is the initial page's webview (same answer as
                // GetFocusedWebView above).
                WebDriverCommandMsg::GetWebViewSelectedForInteraction(response_sender) => {
                    let _ =
                        response_sender.send(runtime.webdriver_webview_ids().into_iter().next());
                }
            }
        }
    }
}

// Pref surface lives in `crate::prefs` (always compiled — the durable
// application point is core `BrowserRuntime::new`, outside the webdriver
// feature gate). Re-exported here to keep this module's public surface
// stable for the CLI entry (`bao_browser::webdriver_host::*`).
pub use crate::prefs::{apply_pref_overrides, apply_pref_overrides_to, load_prefs_file};