#![allow(dead_code, unused_imports)]
mod browser_context;
mod cdp_handler;
pub mod cdp_memory;
mod config;
mod delegate;
mod error;
pub mod overlay;
mod page;
mod page_pool;
mod permission;
mod phase_watch;
pub mod prefs;
mod runtime_bridge;
pub mod screencast;
mod screenshot;
#[cfg(feature = "webdriver")]
pub mod webdriver_host;
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, servo_event_emitted_total,
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, BridgeRequest};
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()
});
});
static SERVO_EVENT_PUMPED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn servo_event_pumped_total() -> u64 {
use std::sync::atomic::Ordering;
SERVO_EVENT_PUMPED.load(Ordering::Relaxed)
}
pub fn drain_servo_events(
servo_event_rx: &bao_ipc::Receiver<ServoEvent>,
handle: impl FnMut(ServoEvent),
) {
drain_servo_events_starting(servo_event_rx, None, handle);
}
pub(crate) fn drain_servo_events_starting(
servo_event_rx: &bao_ipc::Receiver<ServoEvent>,
first: Option<ServoEvent>,
mut handle: impl FnMut(ServoEvent),
) {
let mut pending = first;
loop {
let servo_event = match pending.take() {
Some(servo_event) => servo_event,
None => match servo_event_rx.try_recv() {
Ok(servo_event) => servo_event,
Err(bao_ipc::TryRecvError::Empty | bao_ipc::TryRecvError::Disconnected) => break,
},
};
SERVO_EVENT_PUMPED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
handle(servo_event);
}
}
pub(crate) fn park_for_event(
servo_event_rx: Option<&bao_ipc::Receiver<ServoEvent>>,
budget: Duration,
) -> Option<ServoEvent> {
let Some(rx) = servo_event_rx else {
std::thread::sleep(budget);
return None;
};
match rx.recv_timeout(budget) {
Ok(servo_event) => Some(servo_event),
Err(bao_ipc::RecvTimeoutError::Timeout) => None,
Err(bao_ipc::RecvTimeoutError::Disconnected) => {
std::thread::sleep(budget);
None
}
}
}
pub(crate) fn park_for_event_or_bridge(
servo_event_rx: &bao_ipc::Receiver<ServoEvent>,
servo_live: &mut bool,
bridge_rx: &bao_ipc::Receiver<BridgeRequest>,
bridge_live: &mut bool,
budget: Duration,
) -> (Option<ServoEvent>, Option<BridgeRequest>) {
if !*servo_live && !*bridge_live {
std::thread::sleep(budget);
return (None, None);
}
let mut sel = bao_ipc::Select::new();
let servo_idx = servo_live.then(|| sel.recv(servo_event_rx));
let bridge_idx = bridge_live.then(|| sel.recv(bridge_rx));
match sel.select_timeout(budget) {
Ok(op) => {
if Some(op.index()) == servo_idx {
match op.recv(servo_event_rx) {
Ok(servo_event) => return (Some(servo_event), None),
Err(_) => *servo_live = false,
}
} else if Some(op.index()) == bridge_idx {
match op.recv(bridge_rx) {
Ok(request) => return (None, Some(request)),
Err(_) => *bridge_live = false,
}
}
(None, None)
}
Err(bao_ipc::SelectTimeoutError) => (None, None),
}
}
#[deprecated(
since = "0.4.0",
note = "renamed to `BrowserRuntime`; will be removed in 1.0"
)]
pub type BaoRuntime = BrowserRuntime;
pub struct BrowserRuntime {
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>,
#[cfg(feature = "webdriver")]
webdriver_host: std::cell::RefCell<Option<std::sync::Arc<webdriver_host::WebDriverHost>>>,
exit_scheduled: std::cell::Cell<bool>,
}
impl BrowserRuntime {
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,
certificate_path: config.certificate_path.clone(),
..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;
preferences.dom_sharedworker_enabled = true;
preferences.dom_intersection_observer_enabled = true;
preferences.dom_permissions_enabled = true;
preferences.dom_notification_enabled = true;
preferences.dom_exec_command_enabled = true;
preferences = crate::prefs::apply_pref_overrides_to(preferences, &config.pref_overrides)
.map_err(BrowserError::Init)?;
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,
);
}));
servo::register_bao_realm_liveness_probe(Box::new(|global_ptr| {
let discarded =
bun_runtime::timers::is_global_discarded(global_ptr as *mut mozjs::jsapi::JSObject);
if discarded {
bun_runtime::timers::post_discard_resolve_suppressed_fetch_add();
}
discarded
}));
servo::register_bao_execution_control_armer(Box::new(bao_execution_control_armer));
bun_runtime::timers::register_bao_settings_runner(|cx, global, f| {
bao_timer_settings_runner(
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,
));
page_pool.arm_self_weak(Rc::downgrade(&page_pool));
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>,
);
let (servo_event_tx, servo_event_rx) = bao_ipc::unbounded::<ServoEvent>();
let tap = cdp_bridge.event_tap();
delegate.install_runtime_event_drain(servo_event_rx, tap);
let runtime = BrowserRuntime {
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),
#[cfg(feature = "webdriver")]
webdriver_host: std::cell::RefCell::new(None),
exit_scheduled: std::cell::Cell::new(false),
};
runtime.set_event_channel(servo_event_tx);
Ok(runtime)
}
pub fn servo(&self) -> &Rc<Servo> {
&self.servo
}
pub fn schedule_exit(&self) {
self.exit_scheduled.set(true);
}
pub fn exit_scheduled(&self) -> bool {
self.exit_scheduled.get()
}
#[cfg(feature = "webdriver")]
pub fn install_webdriver_host(&self, port: u16) {
let host = std::sync::Arc::new(webdriver_host::WebDriverHost::start(port));
self.webdriver_host.borrow_mut().replace(host);
}
#[cfg(feature = "webdriver")]
fn webdriver_host(&self) -> Option<std::sync::Arc<webdriver_host::WebDriverHost>> {
self.webdriver_host.borrow().clone()
}
#[cfg(feature = "webdriver")]
pub fn webdriver_webview_ids(&self) -> Vec<servo::WebViewId> {
self.page_pool
.live_page_ids()
.into_iter()
.filter_map(|id| self.page_pool.get_page(id))
.filter_map(|page| page.webdriver_webview_id())
.collect()
}
#[cfg(feature = "webdriver")]
pub fn page_for_webview(&self, webview_id: servo::WebViewId) -> Option<(usize, PageHandle)> {
self.page_pool.live_page_ids().into_iter().find_map(|id| {
self.page_pool.get_page(id).and_then(|page| {
if page.webdriver_webview_id() == Some(webview_id) {
Some((id, page))
} else {
None
}
})
})
}
#[cfg(feature = "webdriver")]
pub fn create_webdriver_page(&self, url: url::Url) -> Option<(servo::WebViewId, PageHandle)> {
let config = PageConfig {
url: Some(url.to_string()),
..Default::default()
};
let page = self.create_page(&config).ok()?;
let webview_id = page.webdriver_webview_id()?;
Some((webview_id, page))
}
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();
self.delegate.drain_runtime_events();
}
pub fn set_console_log_channel(&self, tx: bao_ipc::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: bao_ipc::Sender<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 pump_servo_events_to_taps(&self) {
self.delegate.drain_runtime_events();
}
pub fn run(&self) -> Result<(), BrowserError> {
let max_wait = Duration::from_secs(300);
let start = std::time::Instant::now();
#[cfg(feature = "webdriver")]
let webdriver_mode = self.webdriver_host.borrow().is_some();
#[cfg(not(feature = "webdriver"))]
let webdriver_mode = false;
self.run_until(move || !webdriver_mode && start.elapsed() >= max_wait)
}
pub fn run_until(&self, mut done: impl FnMut() -> bool) -> Result<(), BrowserError> {
while !done() {
if self.exit_scheduled() {
break;
}
self.servo.spin_event_loop();
if let Some(rx) = &self.cdp_bridge_rx {
rx.drain(|cmd| cdp_handler::handle_bridge_command(cmd, &self.page_pool));
}
screencast::drive(&self.page_pool, None);
self.page_pool.paint_pages_needing_repaint();
self.page_pool.check_idle_pages();
self.page_pool.close_pending_pages();
self.page_pool.init_pending_pages();
self.delegate.drain_runtime_events();
#[cfg(feature = "webdriver")]
if let Some(host) = self.webdriver_host() {
host.drain(self);
}
self.delegate.park_runtime_events(Duration::from_millis(1));
}
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();
if let Some(rx) = &self.cdp_bridge_rx {
rx.drain(|cmd| cdp_handler::handle_bridge_command(cmd, &self.page_pool));
}
screencast::drive(&self.page_pool, None);
self.page_pool.paint_pages_needing_repaint();
self.page_pool.check_idle_pages();
self.page_pool.close_pending_pages();
self.page_pool.init_pending_pages();
self.delegate.drain_runtime_events();
self.delegate.park_runtime_events(Duration::from_millis(1));
}
}
pub fn run_with_bridge(
&self,
bridge_rx: bao_cdp::servo_bridge::BridgeReceiver,
servo_event_rx: bao_ipc::Receiver<ServoEvent>,
broadcaster: Arc<EventBroadcaster>,
event_demux: Option<Arc<BaoWsRegistry>>,
) -> Result<(), BrowserError> {
let max_wait = Duration::from_secs(3600);
let start = std::time::Instant::now();
let mut servo_queue_live = true;
let mut bridge_queue_live = true;
while start.elapsed() < max_wait {
self.servo.spin_event_loop();
bridge_rx.drain(|cmd| cdp_handler::handle_bridge_command(cmd, &self.page_pool));
let ws_sink = |target: &str, method: &str, params: serde_json::Value| match event_demux
.as_ref()
{
Some(registry) => {
registry.broadcast_for_target(broadcaster.as_ref(), target, method, params)
}
None => broadcaster.send_page_event(target, method, params),
};
screencast::drive(&self.page_pool, Some(&ws_sink));
self.page_pool.paint_pages_needing_repaint();
self.page_pool.check_idle_pages();
self.page_pool.close_pending_pages();
self.page_pool.init_pending_pages();
let wait_taps: Vec<std::sync::Arc<bao_cdp_client::bridge::CdpEventTap>> = [
event_demux.as_ref().and_then(|r| r.event_tap()),
self.cdp_bridge.as_ref().and_then(|b| b.event_tap()),
]
.into_iter()
.flatten()
.collect();
let translate_and_broadcast = |servo_event: ServoEvent| {
let target_id = servo_event.target_id().to_string();
let cdp_events = translate(servo_event);
for cdp_event in &cdp_events {
for tap in &wait_taps {
tap.observe(&target_id, &cdp_event.method, &cdp_event.params);
}
}
match event_demux.as_ref() {
Some(registry) => {
for cdp_event in cdp_events {
registry.broadcast_for_target(
broadcaster.as_ref(),
&target_id,
&cdp_event.method,
cdp_event.params,
);
}
}
None => {
for cdp_event in cdp_events {
broadcaster.send_event(&cdp_event.method, cdp_event.params);
}
}
}
};
drain_servo_events_starting(&servo_event_rx, None, translate_and_broadcast);
let (first, bridge_request) = park_for_event_or_bridge(
&servo_event_rx,
&mut servo_queue_live,
bridge_rx.select_receiver(),
&mut bridge_queue_live,
Duration::from_millis(1),
);
if let Some(request) = bridge_request {
bridge_rx.process_request(request, |cmd| {
cdp_handler::handle_bridge_command(cmd, &self.page_pool)
});
}
drain_servo_events_starting(&servo_event_rx, first, translate_and_broadcast);
}
Ok(())
}
}
impl Drop for BrowserRuntime {
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);
}
fn bao_timer_settings_runner(
cx: *mut std::ffi::c_void,
global: *mut std::ffi::c_void,
f: &mut dyn FnMut(),
) {
if crate::runtime_bridge::is_known_page_global(global) {
servo::bao_run_in_script_settings(cx, global, f);
} else {
f();
}
}
fn bao_execution_control_armer<'a>(
cx_ptr: *mut std::ffi::c_void,
timeout: Duration,
run: Box<dyn FnOnce() -> Box<dyn std::any::Any> + 'a>,
) -> (Box<dyn std::any::Any>, Option<String>) {
let raw_cx = cx_ptr as *mut mozjs::jsapi::JSContext;
let control = bao_engine::execution_control::ExecutionControl::for_context(raw_cx);
let (boxed, state) =
bao_engine::execution_control::run_raw_with_control(raw_cx, &control, Some(timeout), run);
let termination = match state {
bao_engine::execution_control::TerminalState::TimedOut
| bao_engine::execution_control::TerminalState::Cancelled => {
Some(bao_engine::execution_control::terminal_message(state))
}
_ => None,
};
(boxed, termination)
}
pub fn run_browser(config: BrowserConfig) -> Result<(), BrowserError> {
let _stealth = config.stealth_profile.is_some();
let url = config.url.clone();
let webdriver_port = config.webdriver_port;
let bao_config: BaoConfig = config.into();
let cdp_port = bao_config.cdp_port;
let runtime = BrowserRuntime::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);
}
#[cfg(feature = "webdriver")]
if let Some(port) = webdriver_port {
runtime.install_webdriver_host(port);
return runtime.run();
}
#[cfg(not(feature = "webdriver"))]
let _ = webdriver_port;
if let Some(port) = cdp_port {
let (bridge_tx, bridge_rx) = bridge_channel(Duration::from_secs(30));
let (console_tx, console_rx) = bao_ipc::unbounded::<cdp_server::ConsoleMessage>();
runtime.set_console_log_channel(console_tx);
let (event_tx, servo_event_rx) = bao_ipc::unbounded::<ServoEvent>();
runtime.set_event_channel(event_tx);
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.clone());
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,
Some(registry.clone()),
);
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();
}
}