#![allow(dead_code, unused_imports)]
mod cdp_handler;
pub mod cdp_memory;
mod config;
mod delegate;
mod error;
mod page;
mod page_pool;
mod phase_watch;
mod permission;
mod runtime_bridge;
mod screenshot;
mod ws_registry;
pub use config::{BaoConfig, BrowserConfig, PageConfig};
pub use cdp_handler::handle_bridge_command;
pub use delegate::{
crash_safe_teardown_worker, is_javascript_mime_type, AutoCloseWorker, BaoServoDelegate,
BaoWebViewDelegate, BaoWebViewState, DedicatedWorkerGlobalScopeState,
ServiceWorkerFetchInterceptMode, ServiceWorkerGlobalScopeState, ServiceWorkerHandle,
ServiceWorkerRegistrationId, ServiceWorkerRegistrationState, ServiceWorkerRegistrationTracking,
ServiceWorkerScopeConfig, SharedWorkerChannelBridge, SharedWorkerConnectEvent,
SharedWorkerGlobalScopeState, SharedWorkerHandle, SharedWorkerId, SharedWorkerPortChannel,
SharedWorkerPortEndpoints, SharedWorkerPortRef, SharedWorkerScopeConfig,
StructuredClonePayload, WorkerChannelBridge, WorkerChannelEndpoints, WorkerErrorEvent,
WorkerGlobalScopeState, WorkerHandle, WorkerId, WorkerLifecycleState, WorkerLocation,
WorkerMessageDirection, WorkerMessageEvent, WorkerNavigator, WorkerNetworkInformation,
WorkerScopeConfig, WorkerScriptLoadError, WorkerScriptLoadResult, WorkerScriptLoadState,
WorkerScriptLoader, WorkerScriptSource, WorkerScriptType, WorkerStructuredMessage,
WorkerTeardownPath, WorkerTeardownResult,
};
pub use error::BrowserError;
pub use page::{PageHandle, PageState};
pub use page_pool::PagePool;
pub use permission::{Permission, PermissionDenied, PermissionGuard};
pub use runtime_bridge::{
register_worker_scope_callback_native, BridgeChannel, BridgeCommand, BridgeReceiver,
BridgeResponse, EvaluateResult, RuntimeBridge, WorkerScopeInitFn,
};
pub use screenshot::{encode_image, ScreenshotFormat};
pub use ws_registry::BaoWsRegistry;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use servo::{Opts, Preferences, Servo, ServoBuilder};
use bao_cdp::domains::ServoTargetProvider;
use bao_cdp::servo_bridge::bridge_channel;
use bao_cdp_client::bridge::{translate, ServoEvent};
use cdp_server::{CdpServer, EventBroadcaster, EventSender, ServerConfig};
static BAO_SERVO_OPTS_INIT: std::sync::LazyLock<()> = std::sync::LazyLock::new(|| {
servo::opts::initialize_options(Opts {
force_isolate_event_loops: true,
disable_script_debugger: true,
..Opts::default()
});
});
pub struct BaoRuntime {
servo: Rc<Servo>,
delegate: Rc<BaoServoDelegate>,
page_pool: Rc<PagePool>,
cdp_port: Option<u16>,
cdp_bridge: Option<std::sync::Arc<cdp_memory::MemoryCdpBridge>>,
cdp_bridge_rx: Option<bao_cdp::servo_bridge::BridgeReceiver>,
cdp_bridge_token: Option<usize>,
}
impl BaoRuntime {
pub fn new(config: BaoConfig) -> Result<Self, BrowserError> {
config.validate().map_err(BrowserError::Init)?;
servo::opts::initialize_options(Opts {
force_isolate_event_loops: true,
disable_script_debugger: true,
ignore_certificate_errors: config.ignore_certificate_errors,
..Opts::default()
});
std::sync::LazyLock::force(&BAO_SERVO_OPTS_INIT);
let desired_opts = Opts {
force_isolate_event_loops: true,
ignore_certificate_errors: config.ignore_certificate_errors,
disable_script_debugger: true,
..Opts::default()
};
let servo_already_initialized = servo::opts::is_initialized();
let mut preferences = Preferences::default();
preferences.dom_indexeddb_enabled = true;
preferences.dom_offscreen_canvas_enabled = true;
preferences.dom_serviceworker_enabled = true;
preferences.dom_webgl2_enabled = true;
let servo: Rc<Servo> = Rc::new(if servo_already_initialized {
ServoBuilder::default()
.opts(servo::opts::get().clone())
.preferences(preferences)
.build()
} else {
ServoBuilder::default()
.opts(desired_opts)
.preferences(preferences)
.build()
});
let delegate = Rc::new(BaoServoDelegate::new());
servo.set_delegate(Rc::clone(&delegate) as Rc<dyn servo::ServoDelegate>);
servo::register_bao_event_loop_pump(Box::new(|cx_ptr| {
bun_runtime::timers::pump_embedder_thread(cx_ptr as *mut mozjs::jsapi::JSContext);
}));
servo::register_bao_realm_discard_cancel(Box::new(|cx_ptr, global_ptr| {
bun_runtime::timers::cancel_timers_for_global(
cx_ptr as *mut mozjs::jsapi::JSContext,
global_ptr as *mut mozjs::jsapi::JSObject,
);
}));
bun_runtime::timers::register_bao_settings_runner(|cx, global, f| {
servo::bao_run_in_script_settings(
cx as *mut std::ffi::c_void,
global as *mut std::ffi::c_void,
f,
);
});
bun_runtime::fetch_async::set_thread_wakeup_bridge(|| {
servo::bao_current_thread_wake_fn().map(|wake| {
wake as bun_runtime::fetch_async::ThreadWakeup
})
});
servo::set_webviewless_resource_handler(Some(Arc::new(
|_request| servo::BaoWebviewlessResourceVerdict::PassThrough,
)));
let page_pool = Rc::new(PagePool::new(
Rc::clone(&servo),
Rc::clone(&delegate),
&config,
));
phase_watch::spawn_watchdog();
let (cdp_bridge, cdp_bridge_rx) = cdp_memory::MemoryCdpBridge::new("");
let cdp_bridge_token =
bao_cdp_client::browser::set_process_memory_bridge(
cdp_bridge.clone() as std::sync::Arc<dyn bao_cdp_client::transport::InMemoryBridge>,
);
Ok(BaoRuntime {
servo,
delegate,
page_pool,
cdp_port: config.cdp_port,
cdp_bridge: Some(cdp_bridge),
cdp_bridge_rx: Some(cdp_bridge_rx),
cdp_bridge_token: Some(cdp_bridge_token),
})
}
pub fn page_pool(&self) -> &Rc<PagePool> {
&self.page_pool
}
pub fn create_page(&self, config: &PageConfig) -> Result<PageHandle, BrowserError> {
let page = self.page_pool.create_page(config)?;
if let Some(bridge) = &self.cdp_bridge {
bridge.set_default_target(page.id().to_string());
}
Ok(page)
}
pub fn create_worker(
&self,
page: &PageHandle,
script: &str,
) -> Result<WorkerHandle, BrowserError> {
self.create_worker_with_url(page, script)
}
pub fn create_worker_with_url(
&self,
page: &PageHandle,
url: &str,
) -> Result<WorkerHandle, BrowserError> {
let webview_state = page.webview_state();
let scope_config = webview_state.borrow().worker_scope_config.clone();
let worker_id = crate::delegate::WorkerId(url.to_string());
let handle = WorkerHandle::new(url.to_string());
let webview_id = page
.webview_id()
.ok_or_else(|| BrowserError::Init("page has no webview".into()))?;
runtime_bridge::register_worker_scope_callback_native(
webview_id,
scope_config.stealth_profile.clone(),
Some(handle.worker_global_addr_arc()),
);
let _endpoints = webview_state
.borrow_mut()
.create_worker_channel(worker_id.clone());
let scope_state =
crate::delegate::DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &scope_config);
webview_state
.borrow_mut()
.register_dedicated_worker_scope(worker_id.clone(), scope_state);
webview_state.borrow_mut().track_worker(handle.clone());
let new_worker_js = format!(
"(function() {{ var w = new Worker({}); return ''; }})();",
serde_json::Value::String(url.to_string())
);
page.evaluate_js_web(&new_worker_js).map_err(|e| {
BrowserError::Init(format!(
"Failed to dispatch new Worker({:?}) via servo DOM: {}",
url, e
))
})?;
log::debug!(
"[bao] dispatched new Worker({:?}) via servo DOM (tracked via AutoCloseWorker, DEC-WK-001 native path)",
url
);
Ok(handle)
}
pub fn spin_event_loop(&self) {
self.servo.spin_event_loop();
}
pub fn set_console_log_channel(&self, tx: std::sync::mpsc::Sender<cdp_server::ConsoleMessage>) {
self.delegate.set_console_log_tx(tx.clone());
let stats = self.page_pool.stats();
for id in 1..=(stats.active + stats.idle) {
if let Some(page) = self.page_pool.get_page(id) {
page.webview_state().borrow_mut().console_log_tx = Some(tx.clone());
}
}
}
pub fn set_event_channel(&self, tx: std::sync::mpsc::SyncSender<ServoEvent>) {
self.delegate.set_event_tx(tx.clone());
let stats = self.page_pool.stats();
for id in 1..=(stats.active + stats.idle) {
if let Some(page) = self.page_pool.get_page(id) {
page.webview_state().borrow_mut().event_tx = Some(tx.clone());
}
}
}
pub fn run(&self) -> Result<(), BrowserError> {
let max_wait = Duration::from_secs(300);
let start = std::time::Instant::now();
while start.elapsed() < max_wait {
self.servo.spin_event_loop();
self.page_pool.check_idle_pages();
if let Some(rx) = &self.cdp_bridge_rx {
rx.drain(|cmd| cdp_handler::handle_bridge_command(cmd, &self.page_pool));
}
std::thread::yield_now();
}
let _stats = self.page_pool.stats();
Ok(())
}
pub fn pump_cdp(&self, duration: std::time::Duration) {
let start = std::time::Instant::now();
while start.elapsed() < duration {
self.servo.spin_event_loop();
self.page_pool.check_idle_pages();
if let Some(rx) = &self.cdp_bridge_rx {
rx.drain(|cmd| cdp_handler::handle_bridge_command(cmd, &self.page_pool));
}
std::thread::yield_now();
}
}
pub fn run_with_bridge(
&self,
bridge_rx: bao_cdp::servo_bridge::BridgeReceiver,
servo_event_rx: std::sync::mpsc::Receiver<ServoEvent>,
broadcaster: Arc<EventBroadcaster>,
) -> Result<(), BrowserError> {
let max_wait = Duration::from_secs(3600);
let start = std::time::Instant::now();
while start.elapsed() < max_wait {
self.servo.spin_event_loop();
self.page_pool.check_idle_pages();
bridge_rx.drain(|cmd| cdp_handler::handle_bridge_command(cmd, &self.page_pool));
while let Ok(servo_event) = servo_event_rx.try_recv() {
let cdp_events = translate(servo_event);
for cdp_event in cdp_events {
broadcaster.send_event(&cdp_event.method, cdp_event.params);
}
}
std::thread::yield_now();
}
Ok(())
}
}
impl Drop for BaoRuntime {
fn drop(&mut self) {
self.page_pool.close_all();
if let Some(token) = self.cdp_bridge_token.take() {
bao_cdp_client::browser::clear_process_memory_bridge(token);
}
}
}
fn register_worker_interfaces_ready_injector_native(
webview_id: servo::WebViewId,
profile: Option<bao_stealth::StealthProfile>,
) {
let injector: servo::EmbedderWorkerInjector = std::sync::Arc::new(move |cx_ptr, global_ptr| {
let raw_cx = cx_ptr as *mut mozjs::jsapi::JSContext;
let raw_global = global_ptr as *mut mozjs::jsapi::JSObject;
if raw_cx.is_null() || raw_global.is_null() {
log::warn!(
"[register_worker_interfaces_ready_injector_native] NULL cx/global — \
skipping interfaces-ready install (REQ-BRW-004 per-Worker delivery)"
);
return;
}
let Some(ref profile) = profile else {
return;
};
unsafe {
use mozjs::context::JSContext;
use mozjs::realm::AutoRealm;
use std::ptr::NonNull;
let cx_nn = NonNull::new_unchecked(raw_cx);
let mut cx = JSContext::from_ptr(cx_nn);
let _worker_realm = AutoRealm::new(&mut cx, NonNull::new_unchecked(raw_global));
bao_stealth::engine_props::set_profile_for_global(raw_global as usize, profile);
bao_stealth::engine_props::install_stealth_props(raw_cx, raw_global);
}
});
servo::register_worker_interfaces_ready_injector(webview_id, injector);
}
pub fn run_browser(config: BrowserConfig) -> Result<(), BrowserError> {
let _stealth = config.stealth_profile.is_some();
let url = config.url.clone();
let bao_config: BaoConfig = config.into();
let cdp_port = bao_config.cdp_port;
let runtime = BaoRuntime::new(bao_config)?;
let page_config = PageConfig {
url: url.clone(),
stealth_profile: None,
..Default::default()
};
let page = runtime.create_page(&page_config)?;
if let Some(ref page_url) = url {
log::debug!("[bao] navigating to {}", page_url);
}
if let Some(port) = cdp_port {
let (bridge_tx, bridge_rx) = bridge_channel(Duration::from_secs(30));
let (console_tx, console_rx) = std::sync::mpsc::channel::<cdp_server::ConsoleMessage>();
runtime.set_console_log_channel(console_tx);
let (event_subscriber, servo_event_rx) = bao_cdp_client::bridge::EventSubscriber::new();
runtime.set_event_channel(event_subscriber.sender());
let registry = Arc::new(BaoWsRegistry::new(bridge_tx.clone()));
let config = ServerConfig::builder().host("127.0.0.1").port(port).build();
let target_id = page.id().to_string();
let mut server = CdpServer::with_registry(config, registry);
let provider = Arc::new(ServoTargetProvider::new(
bridge_tx,
target_id,
"127.0.0.1".into(),
port,
));
server.set_target_provider(provider);
server.set_console_receiver(console_rx);
let broadcaster = server.broadcaster();
let stop_flag = server.stop_handle();
let server_thread = std::thread::spawn(move || {
let _ = server.run();
});
let result = runtime.run_with_bridge(bridge_rx, servo_event_rx, broadcaster);
stop_flag.store(true, std::sync::atomic::Ordering::Release);
let _ = server_thread.join();
return result;
}
runtime.run()
}
#[cfg(test)]
mod test_link_seams {
#[test]
fn link_higher_tier_seams() {
bao_bundler::force_link_test_seams();
}
}