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,
};
#[derive(Clone, Copy, Default)]
pub struct SpinLoopWaker;
impl EventLoopWaker for SpinLoopWaker {
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
Box::new(SpinLoopWaker)
}
fn wake(&self) {
}
}
#[derive(Default)]
struct WebdriverBridge {
load_status_senders: HashMap<WebViewId, GenericSender<WebDriverLoadStatus>>,
completed_loads: std::collections::HashSet<WebViewId>,
pending_traversals: HashMap<TraversalId, GenericSender<WebDriverLoadStatus>>,
pending_input_events: HashMap<InputEventId, Sender<()>>,
handled_input_events: std::collections::HashSet<InputEventId>,
script_interrupt: Option<GenericSender<WebDriverJSResult>>,
embedder_controls: HashMap<WebViewId, Vec<EmbedderControl>>,
}
static WEBDRIVER_BRIDGE: LazyLock<Mutex<WebdriverBridge>> =
LazyLock::new(|| Mutex::new(WebdriverBridge::default()));
fn with_bridge<R>(f: impl FnOnce(&mut WebdriverBridge) -> R) -> Option<R> {
WEBDRIVER_BRIDGE
.lock()
.ok()
.map(|mut bridge| f(&mut bridge))
}
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);
}
},
);
}
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);
}
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);
}
});
}
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 {
bridge.handled_input_events.insert(event_id);
}
});
}
pub fn show_embedder_control(webview_id: WebViewId, control: EmbedderControl) {
with_bridge(|bridge| {
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);
}
}
});
}
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());
});
}
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,
}
}
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)
}
pub struct WebDriverHost {
receiver: Receiver<WebDriverCommandMsg>,
}
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");
}
}
}
fn webdriver_register_traversal(
traversal_id: TraversalId,
load_status_sender: GenericSender<WebDriverLoadStatus>,
) {
with_bridge(|bridge| {
bridge
.pending_traversals
.insert(traversal_id, load_status_sender);
});
}
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:?}");
}
}
}
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);
}
}
});
}
#[cfg(unix)]
mod signal_exit {
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
static SIGNAL_EXIT_REQUESTED: AtomicBool = AtomicBool::new(false);
static INSTALLED: AtomicBool = AtomicBool::new(false);
const SIGINT: i32 = 2;
const SIGTERM: i32 = 15;
fn request_signal_exit() {
SIGNAL_EXIT_REQUESTED.store(true, Ordering::Relaxed);
}
pub fn install() {
if INSTALLED.swap(true, Ordering::AcqRel) {
return;
}
let mut failed = false;
for signal in [SIGINT, SIGTERM] {
if unsafe { signal_hook_registry::register(signal, request_signal_exit) }.is_err() {
failed = true;
}
}
if failed {
log::error!("[webdriver] SIGINT/SIGTERM registration failed — signal exit degraded");
eprintln!("bao: webdriver signal handler registration failed — signal exit degraded");
return;
}
spawn_watchdog();
}
const POLL: Duration = Duration::from_millis(50);
const EXIT_GRACE: Duration = Duration::from_secs(5);
fn spawn_watchdog() {
let spawned = std::thread::Builder::new()
.name("bao-webdriver-sigwatch".to_string())
.spawn(|| {
while !SIGNAL_EXIT_REQUESTED.load(Ordering::Relaxed) {
std::thread::sleep(POLL);
}
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");
unsafe { libc::_exit(0) };
});
if spawned.is_err() {
log::error!("[webdriver] signal-exit watchdog spawn failed — 5s bound unavailable");
eprintln!("bao: webdriver signal watchdog spawn failed");
}
}
pub fn exit_requested() -> bool {
SIGNAL_EXIT_REQUESTED.load(Ordering::Relaxed)
}
}
impl WebDriverHost {
pub fn start(port: u16) -> Self {
#[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 }
}
pub fn drain(&self, runtime: &crate::BrowserRuntime) {
#[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(..) => {
runtime.servo().execute_webdriver_command(msg);
}
WebDriverCommandMsg::NewWindow(..) => {
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) => {
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);
}
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) {
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) => {
match script_command {
WebDriverScriptCommand::ExecuteScriptWithCallback(_, response_sender) => {
with_bridge(|bridge| {
bridge.script_interrupt = Some(response_sender.clone());
});
}
WebDriverScriptCommand::AddLoadStatusSender(
webview_id,
load_status_sender,
) => {
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));
}
}
}
WebDriverCommandMsg::SelectWebViewForInteraction(webview_id) => {
if let Some((_, page)) = runtime.page_for_webview(webview_id) {
page.webdriver_touch();
}
}
WebDriverCommandMsg::GetWebViewSelectedForInteraction(response_sender) => {
let _ =
response_sender.send(runtime.webdriver_webview_ids().into_iter().next());
}
}
}
}
}
pub use crate::prefs::{apply_pref_overrides, apply_pref_overrides_to, load_prefs_file};