bao_browser/lib.rs
1// @trace REQ-BRW-001 [entity:BrowserContext] [entity:PageHandle]
2// @trace REQ-BRW-004 [entity:Worker] [entity:DedicatedWorkerGlobalScope]
3// @trace REQ-BRW-4 [entity:Worker] [entity:SharedWorker] [entity:ServiceWorker]
4// @trace REQ-CLI-002
5#![allow(dead_code, unused_imports)]
6// REQ-BRW-001: Browser engine integration with servo
7// REQ-BRW-004: Worker constructor bridging to Page Realm (DF-WK-11)
8// REQ-BRW-4: Worker/SharedWorker/ServiceWorker constructors on JS global object
9// REQ-CLI-002: bao browser 子命令 → servo 初始化 + CDP 端口输出
10// REQ-LIB-004: BaoRuntime top-level coordinator
11mod cdp_handler;
12mod config;
13mod delegate;
14mod error;
15mod page;
16mod page_pool;
17mod permission;
18mod runtime_bridge;
19mod screenshot;
20mod ws_registry;
21
22pub use config::{BaoConfig, BrowserConfig, PageConfig};
23// Bridge-command handler — the servo-side executor that drains
24// BridgeCommand during the event loop (run_with_bridge's per-command entry).
25// Public for e2e tests that drive the same loop shape as run_browser.
26pub use cdp_handler::handle_bridge_command;
27pub use delegate::{
28 crash_safe_teardown_worker, is_javascript_mime_type, AutoCloseWorker, BaoServoDelegate,
29 BaoWebViewDelegate, BaoWebViewState, DedicatedWorkerGlobalScopeState, ServiceWorkerFetchEvent,
30 ServiceWorkerFetchInterceptMode, ServiceWorkerGlobalScopeState, ServiceWorkerHandle,
31 ServiceWorkerRegistrationId, ServiceWorkerRegistrationState, ServiceWorkerRegistrationTracking,
32 ServiceWorkerScopeConfig, SharedWorkerChannelBridge, SharedWorkerConnectEvent,
33 SharedWorkerGlobalScopeState, SharedWorkerHandle, SharedWorkerId, SharedWorkerPortChannel,
34 SharedWorkerPortEndpoints, SharedWorkerPortRef, SharedWorkerScopeConfig,
35 StructuredClonePayload, WorkerChannelBridge, WorkerChannelEndpoints, WorkerErrorEvent,
36 WorkerGlobalScopeState, WorkerHandle, WorkerId, WorkerLifecycleState, WorkerLocation,
37 WorkerMessageDirection, WorkerMessageEvent, WorkerNavigator, WorkerNetworkInformation,
38 WorkerScopeConfig, WorkerScriptLoadError, WorkerScriptLoadResult, WorkerScriptLoadState,
39 WorkerScriptLoader, WorkerScriptSource, WorkerScriptType, WorkerStructuredMessage,
40 WorkerTeardownPath, WorkerTeardownResult,
41};
42pub use error::BrowserError;
43pub use page::{PageHandle, PageState};
44pub use page_pool::PagePool;
45pub use permission::{Permission, PermissionDenied, PermissionGuard};
46pub use runtime_bridge::{
47 register_worker_scope_callback_native, BridgeChannel, BridgeCommand, BridgeReceiver,
48 BridgeResponse, EvaluateResult, RuntimeBridge, WorkerScopeInitFn,
49};
50pub use screenshot::{encode_image, ScreenshotFormat};
51pub use ws_registry::BaoWsRegistry;
52
53use std::rc::Rc;
54use std::sync::Arc;
55use std::time::Duration;
56
57use servo::{Opts, Preferences, Servo, ServoBuilder};
58
59use bao_cdp::domains::ServoTargetProvider;
60use bao_cdp::servo_bridge::bridge_channel;
61use bao_cdp_client::bridge::{translate, ServoEvent};
62use cdp_server::{CdpServer, EventBroadcaster, EventSender, ServerConfig};
63
64// BAO PATCH (BCE-20260627-009): Process-global servo opts initialization.
65// servo's `opts::initialize_options` uses an `OnceLock<Opts>` that panics on re-init,
66// and `opts::get()` lazily fills it with `Default`. If ANY servo code calls `get()`
67// before our explicit `initialize_options`, the OnceLock locks to Default and bao's
68// config (force_isolate_event_loops=true) can never win.
69//
70// `BAO_SERVO_OPTS_INIT` is a `LazyLock` that runs `initialize_options` with bao's
71// config on first access. `BaoRuntime::new` forces it (`.clone()` triggers init)
72// BEFORE constructing `Servo`, winning the OnceLock race process-wide. Multi-instance
73// safety: subsequent `BaoRuntime::new` calls hit the idempotent path in the patched
74// `initialize_options` (same bao config → no-op).
75static BAO_SERVO_OPTS_INIT: std::sync::LazyLock<()> = std::sync::LazyLock::new(|| {
76 servo::opts::initialize_options(Opts {
77 force_isolate_event_loops: true,
78 disable_script_debugger: true,
79 ..Opts::default()
80 });
81});
82
83pub struct BaoRuntime {
84 servo: Rc<Servo>,
85 delegate: Rc<BaoServoDelegate>,
86 page_pool: Rc<PagePool>,
87 cdp_port: Option<u16>,
88}
89
90impl BaoRuntime {
91 pub fn new(config: BaoConfig) -> Result<Self, BrowserError> {
92 config.validate().map_err(BrowserError::Init)?;
93
94 // Force-init servo's process-global OnceLock<Opts> BEFORE any servo code
95 // calls get() (which would lazily lock in Default). This wins the race
96 // against servo's get_or_init(Default::default).
97 //
98 // Config-derived opts go in FIRST (initialize_options is
99 // first-writer-wins): the static LazyLock below only exists to win the
100 // same race for code paths that never construct a BaoRuntime, so once
101 // this call runs it is a no-op. `ignore_certificate_errors` is the one
102 // field tests set non-default (WPT posture against local self-signed
103 // TLS fixtures, e.g. the U2 h2 e2e matrix).
104 servo::opts::initialize_options(Opts {
105 force_isolate_event_loops: true,
106 disable_script_debugger: true,
107 ignore_certificate_errors: config.ignore_certificate_errors,
108 ..Opts::default()
109 });
110 std::sync::LazyLock::force(&BAO_SERVO_OPTS_INIT);
111
112 // BUG-ENG-366: `force_isolate_event_loops` only governs servo's event-loop
113 // multiplexing (per-pipeline ScriptThread vs shared). It does NOT control
114 // SpiderMonkey Compartment isolation — every page always gets its own
115 // Window global in a distinct Compartment (servo DOM invariant), and the
116 // Node Realm is created via NewCompartmentAndZone unconditionally
117 // (runtime_bridge::create_node_realm_native). Stealth noise is keyed
118 // per-Realm via bao_stealth::engine_props::set_profile_for_global, so
119 // even with `force_isolate_event_loops: false` each page's Canvas /
120 // Navigator / WebGL / Audio fingerprints remain isolated. The flag is
121 // kept `true` here purely to bound servo's resource use (one ScriptThread
122 // per page) — disabling it does not regress isolation.
123 // @trace REQ-SEC-002 [req:REQ-SEC-002] [req:BUG-ENG-366]
124 //
125 // BAO PATCH (BCE-20260627-009): Idempotent servo config init.
126 // servo's `opts::initialize_options` uses a process-global `OnceLock<Opts>`;
127 // re-initializing panics. Each `BaoRuntime::new` → `Servo::new` →
128 // `initialize_options`. Multiple BaoRuntime instances (production
129 // multi-tenant + concurrent integration tests) therefore collide.
130 // Strategy:
131 // (1) Detect whether servo config is already initialized by reading
132 // `servo::opts::get()` (returns `&'static Opts`, never panics —
133 // falls back to `Default` via `get_or_init`).
134 // (2) `force_isolate_event_loops` is `false` in `Opts::default` but
135 // `true` in our desired config, so it is a reliable sentinel for
136 // "already initialized by a prior BaoRuntime".
137 // (3) On the already-initialized path we skip `.opts(...)` — but
138 // `Servo::new` still calls `initialize_options` internally, so the
139 // vendor-side patch (idempotent `initialize_options`) is the real
140 // guarantee. This bao-layer check just avoids passing conflicting
141 // opts when we know a prior instance already configured servo.
142 let desired_opts = Opts {
143 force_isolate_event_loops: true,
144 ignore_certificate_errors: config.ignore_certificate_errors,
145 // BAO PATCH (BCE-20260621-002): Skip servo's
146 // `JS::Debugger::addDebuggee` path entirely. Bao embeds
147 // servo but uses `bao_cdp` (its own CDP) and never connects
148 // to servo's devtools server, so the servo Debugger is pure
149 // overhead and a SIGSEGV source: `fire_add_debuggee` marks
150 // every page's Realm as a debuggee
151 // (`Realm::setIsDebuggee`), which toggles
152 // BaselineInterpreter debugger instrumentation. Under bao's
153 // multi-page + navigate + later-`evaluate` workload, a
154 // subsequent JIT OSR dereferences
155 // `cx->activation_->prev()->asInterpreter()` as NULL and
156 // SIGSEGVs deterministically. Setting this flag bypasses
157 // `fire_add_debuggee` (gated upstream in
158 // `script_thread.rs`), so `setIsDebuggee` is never called
159 // and the JIT toggle never happens. Servo's default `false`
160 // keeps devtools working for normal servo embedders.
161 disable_script_debugger: true,
162 ..Opts::default()
163 };
164 // `opts::get()` is `get_or_init(Default::default)`: returns the
165 // process-wide config if already set, otherwise `Default` (where
166 // `force_isolate_event_loops == false`). Our config sets it `true`,
167 // so observing `true` here means a prior BaoRuntime already won.
168 let servo_already_initialized = servo::opts::is_initialized();
169
170 // Pref override surface (bao is the embedder; vendor defaults stay
171 // untouched). `Servo::new` ends with
172 // `prefs::set(preferences.unwrap_or_default())` — passing NO builder
173 // preferences resets every pref to `Preferences::default()`. So the
174 // only durable injection point is `ServoBuilder::preferences`, on
175 // BOTH branches (the already-initialized branch would otherwise wipe
176 // the flip below on the next BaoRuntime). Both branches start from
177 // `Preferences::default()` — exactly what `Servo::new` would install
178 // without a builder override — so the only delta is the flip.
179 //
180 // `dom_indexeddb_enabled` defaults to false upstream (experimental),
181 // but bao is a full browser runtime and servo ships a real IDB
182 // implementation; `GlobalScope::obtain_storage_key` reads this pref
183 // at runtime per IDB open, so every page (and worker) scope gets it.
184 let mut preferences = Preferences::default();
185 preferences.dom_indexeddb_enabled = true;
186
187 let servo: Rc<Servo> = Rc::new(if servo_already_initialized {
188 // Already initialized. `Servo::new` (servo.rs:877) ALWAYS calls
189 // `initialize_options(opts.unwrap_or_default())` — if we pass no
190 // `.opts(...)`, it would invoke `initialize_options(Default)`
191 // with (force_isolate_event_loops=false, disable_script_debugger=false),
192 // which DIFFERS from the already-set (true, true) and would trip
193 // the "conflicting bao config" panic in the patched
194 // `initialize_options`. To stay idempotent, we clone the
195 // already-stored opts and re-pass them: `Servo::new`'s internal
196 // `initialize_options(existing.clone())` then sees identical
197 // bao fields and becomes a no-op. This is the only way to keep
198 // `Servo::new`'s unconditional `initialize_options` call safe
199 // across multiple BaoRuntime instances.
200 //
201 // NOTE: we use `is_initialized()` (pure read, no side effect),
202 // NOT `opts::get()`. `opts::get()` uses `get_or_init(Default)`,
203 // which would itself populate the `OnceLock` with defaults on the
204 // very first call — racing against `Servo::new`'s real
205 // `initialize_options((true, true))` and causing a spurious
206 // "conflicting config" panic.
207 ServoBuilder::default()
208 .opts(servo::opts::get().clone())
209 .preferences(preferences)
210 .build()
211 } else {
212 ServoBuilder::default()
213 .opts(desired_opts)
214 .preferences(preferences)
215 .build()
216 });
217
218 let delegate = Rc::new(BaoServoDelegate::new());
219 servo.set_delegate(Rc::clone(&delegate) as Rc<dyn servo::ServoDelegate>);
220
221 let page_pool = Rc::new(PagePool::new(
222 Rc::clone(&servo),
223 Rc::clone(&delegate),
224 &config,
225 ));
226
227 Ok(BaoRuntime {
228 servo,
229 delegate,
230 page_pool,
231 cdp_port: config.cdp_port,
232 })
233 }
234
235 pub fn page_pool(&self) -> &Rc<PagePool> {
236 &self.page_pool
237 }
238
239 pub fn create_page(&self, config: &PageConfig) -> Result<PageHandle, BrowserError> {
240 let page = self.page_pool.create_page(config)?;
241
242 // Drive servo's event loop until the WebView pipeline is ready.
243 // Without this, inject_all_with_profile() → drain_callbacks() → evaluate_js_web()
244 // will SIGSEGV because servo's script thread hasn't finished setting up
245 // the pipeline for this WebView.
246 page.wait_for_pipeline_ready(Duration::from_secs(5))?;
247
248 runtime_bridge::inject_all_with_profile(&page, &config.stealth_profile)?;
249 Ok(page)
250 }
251
252 /// Create a Dedicated Worker bridged to a page's servo Realm.
253 ///
254 /// This is the primary entry point for the Worker constructor bridging
255 /// Create a Dedicated Worker via servo's native Worker::Constructor.
256 ///
257 /// Per DEC-WK-001 (BCE-20260627-008), bao no longer spawns a
258 /// `bao_engine::WebWorker` bypass thread. Instead, this method dispatches
259 /// `new Worker(url)` into the page via servo's DOM binding, and servo
260 /// constructs the Worker thread + DedicatedWorkerGlobalScope internally.
261 /// bao's role is reduced to:
262 /// 1. Steering stealth profile + DedicatedWorkerGlobalScope Web APIs by
263 /// registering the scope callback via
264 /// `register_worker_scope_callback_native` (invoked at page-init time,
265 /// see `inject_all_with_profile`). The callback runs on the Worker
266 /// thread via the servo vendor patch `drain_worker_scope_callbacks`.
267 /// 2. Tracking the WorkerHandle for CDP observability + page-unload
268 /// termination (criterion #10, AutoCloseWorker).
269 /// 3. Providing a WorkerChannelBridge for page↔worker postMessage
270 /// (criterion #6, DF-WK-4/5) so the bao side can still observe
271 /// structured-clone traffic even though the thread is servo-owned.
272 ///
273 /// The `script` argument is treated as a Worker script URL. For inline
274 /// scripts, callers should materialize a `data:`/`blob:` URL and pass it
275 /// here (or call `create_worker_with_url`).
276 ///
277 /// @trace DEC-WK-001 servo-native Worker path (bypass removed)
278 /// @trace REQ-BRW-004 [entity:Worker] [criterion:1..10] [criterion:12..18]
279 /// @trace REQ-BRW-4 [criterion:C1..C4]
280 pub fn create_worker(
281 &self,
282 page: &PageHandle,
283 script: &str,
284 ) -> Result<WorkerHandle, BrowserError> {
285 self.create_worker_with_url(page, script)
286 }
287
288 /// Create a Dedicated Worker with a script URL resolved by servo's native
289 /// script loading pipeline.
290 ///
291 /// Per DEC-WK-001 (BCE-20260627-008) the bypass `bao_engine::WebWorker`
292 /// path is removed. The Worker is constructed by servo's DOM
293 /// `Worker::Constructor` when `new Worker(url)` is evaluated in the page.
294 /// This method:
295 /// 1. Builds the WorkerId + WorkerHandle (closing/terminated flags +
296 /// REALM_PROFILES global_addr_slot, criterion #18).
297 /// 2. Wires up the WorkerChannelBridge (criterion #6, DF-WK-4/5).
298 /// 3. Registers DedicatedWorkerGlobalScope state for CDP observability
299 /// (criteria #8, #12-17 stealth consistency).
300 /// 4. Tracks the Worker with AutoCloseWorker (criterion #10).
301 /// 5. Dispatches `new Worker(url)` into the page via servo's DOM binding.
302 ///
303 /// @trace DEC-WK-001 servo-native Worker path (bypass removed)
304 /// @trace REQ-BRW-004 [entity:Worker] [DF-WK-2]
305 /// @trace REQ-BRW-4 [criterion:C1..C4]
306 pub fn create_worker_with_url(
307 &self,
308 page: &PageHandle,
309 url: &str,
310 ) -> Result<WorkerHandle, BrowserError> {
311 let webview_state = page.webview_state();
312
313 // Get the page's WorkerScopeConfig for stealth consistency.
314 // The scope callback (registered at page-init via
315 // register_worker_scope_callback_native) inherits this profile onto
316 // the Worker's DedicatedWorkerGlobalScope.
317 // @trace REQ-BRW-004 [criterion:12..17] CRIT-STL-WK
318 let scope_config = webview_state.borrow().worker_scope_config.clone();
319
320 // Generate WorkerId
321 let worker_id = crate::delegate::WorkerId(url.to_string());
322
323 // Create WorkerHandle — tracks closing/terminated state via
324 // Arc<AtomicBool> and the worker_global_addr for REALM_PROFILES
325 // cleanup (criterion #18). The servo-native scope callback (registered
326 // globally) writes the Worker's global address here on creation.
327 // @trace REQ-BRW-004 [criterion:18] REALM_PROFILES 条目注销
328 let handle = WorkerHandle::new(url.to_string());
329
330 // Create channel bridge (DF-WK-4/5). Even though servo owns the Worker
331 // thread, bao still tracks the bidirectional structured-clone traffic
332 // for CDP observability and message logging.
333 // @trace REQ-BRW-004 [criterion:6] DF-WK-4 / DF-WK-5
334 let _endpoints = webview_state
335 .borrow_mut()
336 .create_worker_channel(worker_id.clone());
337
338 // Register DedicatedWorkerGlobalScope state for CDP observability
339 // and stealth consistency verification.
340 // @trace REQ-BRW-004 [entity:DedicatedWorkerGlobalScope]
341 let scope_state =
342 crate::delegate::DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &scope_config);
343 webview_state
344 .borrow_mut()
345 .register_dedicated_worker_scope(worker_id.clone(), scope_state);
346
347 // Track the WorkerHandle with AutoCloseWorker — ensures termination on
348 // page unload (SPEC criterion #10: GlobalScope::track_worker).
349 // @trace REQ-BRW-004 [criterion:10] GlobalScope::track_worker + AutoCloseWorker
350 webview_state.borrow_mut().track_worker(handle.clone());
351
352 // Dispatch `new Worker(url)` into the page via servo's DOM binding.
353 // servo's Worker::Constructor runs the full DF-WK-2 pipeline
354 // (fetch → MIME check → decode → compile) and spawns the Worker thread
355 // internally; bao's scope callback fires on the Worker thread to install
356 // DedicatedWorkerGlobalScope APIs + stealth properties (criteria #8, #12-17).
357 // @trace DEC-WK-001 servo-native Worker path
358 // @trace REQ-BRW-004 [criterion:1] new Worker(url) creates worker thread
359 // @trace REQ-BRW-004 [DF-WK-2] Worker script loading pipeline
360 let new_worker_js = format!(
361 "(function() {{ var w = new Worker({}); return ''; }})();",
362 serde_json::Value::String(url.to_string())
363 );
364 page.evaluate_js_web(&new_worker_js).map_err(|e| {
365 BrowserError::Init(format!(
366 "Failed to dispatch new Worker({:?}) via servo DOM: {}",
367 url, e
368 ))
369 })?;
370
371 log::debug!(
372 "[bao] dispatched new Worker({:?}) via servo DOM (tracked via AutoCloseWorker, DEC-WK-001 native path)",
373 url
374 );
375
376 Ok(handle)
377 }
378
379 pub fn spin_event_loop(&self) {
380 self.servo.spin_event_loop();
381 }
382
383 /// Set the console log forwarding channel on the servo delegate.
384 /// Console messages from servo will be sent to this channel.
385 pub fn set_console_log_channel(&self, tx: std::sync::mpsc::Sender<cdp_server::ConsoleMessage>) {
386 self.delegate.set_console_log_tx(tx);
387 }
388
389 /// Set the structured event forwarding channel on the servo delegate.
390 /// When set, servo callbacks push ServoEvent (Path B) as the primary event path.
391 /// @trace REQ-CDP-006 [entity:ServoDelegateHooks]
392 pub fn set_event_channel(&self, tx: std::sync::mpsc::Sender<ServoEvent>) {
393 self.delegate.set_event_tx(tx);
394 }
395
396 pub fn run(&self) -> Result<(), BrowserError> {
397 let max_wait = Duration::from_secs(300);
398 let start = std::time::Instant::now();
399
400 while start.elapsed() < max_wait {
401 self.servo.spin_event_loop();
402 self.page_pool.check_idle_pages();
403 // Yield instead of sleep — servo spin_event_loop is non-blocking.
404 std::thread::yield_now();
405 }
406
407 let _stats = self.page_pool.stats();
408
409 Ok(())
410 }
411
412 /// Run with a CDP bridge that processes commands during the event loop.
413 /// Also drains ServoEvent from the EventSubscriber path (Path B) and
414 /// broadcasts translated CdpEvents via the shared EventBroadcaster.
415 /// @trace REQ-CDP-006 [entity:ServoDelegateHooks]
416 pub fn run_with_bridge(
417 &self,
418 bridge_rx: bao_cdp::servo_bridge::BridgeReceiver,
419 servo_event_rx: std::sync::mpsc::Receiver<ServoEvent>,
420 broadcaster: Arc<EventBroadcaster>,
421 ) -> Result<(), BrowserError> {
422 let max_wait = Duration::from_secs(3600);
423 let start = std::time::Instant::now();
424
425 while start.elapsed() < max_wait {
426 self.servo.spin_event_loop();
427 self.page_pool.check_idle_pages();
428
429 // Process pending CDP bridge commands
430 bridge_rx.drain(|cmd| cdp_handler::handle_bridge_command(cmd, &self.page_pool));
431
432 // Drain ServoEvent from EventSubscriber (Path B) and broadcast
433 // as CDP events via the shared EventBroadcaster.
434 // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
435 while let Ok(servo_event) = servo_event_rx.try_recv() {
436 let cdp_events = translate(servo_event);
437 for cdp_event in cdp_events {
438 broadcaster.send_event(&cdp_event.method, cdp_event.params);
439 }
440 }
441
442 // Yield instead of sleep — check bridge commands more frequently.
443 std::thread::yield_now();
444 }
445
446 Ok(())
447 }
448}
449
450impl Drop for BaoRuntime {
451 fn drop(&mut self) {
452 self.page_pool.close_all();
453 }
454}
455
456pub fn run_browser(config: BrowserConfig) -> Result<(), BrowserError> {
457 let _stealth = config.stealth_profile.is_some();
458 let url = config.url.clone();
459 let bao_config: BaoConfig = config.into();
460 let cdp_port = bao_config.cdp_port;
461
462 let runtime = BaoRuntime::new(bao_config)?;
463
464 // Create initial page
465 let page_config = PageConfig {
466 url: url.clone(),
467 stealth_profile: None,
468 ..Default::default()
469 };
470 let page = runtime.create_page(&page_config)?;
471 if let Some(ref page_url) = url {
472 log::debug!("[bao] navigating to {}", page_url);
473 }
474
475 if let Some(port) = cdp_port {
476 // Create bridge channel for CDP <-> servo communication
477 let (bridge_tx, bridge_rx) = bridge_channel(Duration::from_secs(30));
478
479 // Create console log forwarding channel: servo delegate → CDP Log domain
480 let (console_tx, console_rx) = std::sync::mpsc::channel::<cdp_server::ConsoleMessage>();
481 runtime.set_console_log_channel(console_tx);
482
483 // Create EventSubscriber pair for structured ServoEvent path (Path B).
484 // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
485 let (event_subscriber, servo_event_rx) = bao_cdp_client::bridge::EventSubscriber::new();
486 runtime.set_event_channel(event_subscriber.sender());
487
488 // Build CdpServer and extract the shared broadcaster BEFORE moving the
489 // server into its thread. The broadcaster is Arc<EventBroadcaster> which
490 // shares the same SessionMap — events sent via this broadcaster reach all
491 // connected WebSocket sessions.
492 // @trace REQ-CDP-006 [entity:ServoDelegateHooks]
493 //
494 // REQ-CDP WS command face: the registry is the real command dispatcher
495 // (BaoWsRegistry → bao_cdp::handle_command → servo bridge), not the
496 // EmptyHandler placeholder — WS sessions get real Page.navigate /
497 // Runtime.evaluate / Target.* round-trips (Playwright direct connect).
498 let registry = Arc::new(BaoWsRegistry::new(bridge_tx.clone()));
499 let config = ServerConfig::builder().host("127.0.0.1").port(port).build();
500 // The default target is the initial page's real id (cdp_handler parses
501 // decimal page ids — a timestamp hex would never resolve to a page).
502 let target_id = page.id().to_string();
503 let mut server = CdpServer::with_registry(config, registry);
504 let provider = Arc::new(ServoTargetProvider::new(
505 bridge_tx,
506 target_id,
507 "127.0.0.1".into(),
508 port,
509 ));
510 server.set_target_provider(provider);
511 server.set_console_receiver(console_rx);
512 // Clone the broadcaster before moving server into the thread.
513 // Arc<EventBroadcaster> shares the same SessionMap with the server.
514 let broadcaster = server.broadcaster();
515
516 let _handle = std::thread::spawn(move || {
517 let _ = server.run();
518 });
519
520 let result = runtime.run_with_bridge(bridge_rx, servo_event_rx, broadcaster);
521 _handle.thread().unpark();
522 return result;
523 }
524
525 runtime.run()
526}