use super::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use dpi::PhysicalSize;
use servo::{
AllowOrDenyRequest, ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntRect,
DeviceIntSize, EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, LoadStatus,
NavigationRequest, PermissionRequest, ScreenGeometry, ServoDelegate, ServoError, TraversalId,
WebView, WebViewDelegate,
};
use bao_cdp::servo_bridge::main_frame_id_for_target;
use bao_cdp::{BaoEvent, ConsoleMessage};
use bao_cdp_client::bridge::{ConsoleLevel, ServoEvent};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerTeardownPath {
Terminate,
SelfClose,
PageUnload,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerLifecycleState {
Running,
Closing(WorkerTeardownPath),
Terminated(WorkerTeardownPath),
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerTeardownResult {
pub path: WorkerTeardownPath,
pub thread_joined: bool,
pub realm_profile_unregistered: bool,
pub closing_flag_set: bool,
pub never_registered: bool,
}
impl WorkerTeardownResult {
pub fn is_crash_safe(&self) -> bool {
self.closing_flag_set
&& self.thread_joined
&& (self.realm_profile_unregistered || self.never_registered)
}
}
const WORKER_TEARDOWN_TIMEOUT_MS: u64 = 5000;
pub fn crash_safe_teardown_worker(
handle: &WorkerHandle,
path: WorkerTeardownPath,
) -> WorkerTeardownResult {
let was_already_closing = handle.is_closing();
handle.terminate();
let realm_unregistered = if handle.worker_global_addr() != 0 {
handle.unregister_stealth_profile();
true
} else {
false
};
let thread_joined = true;
handle.mark_terminated();
if !was_already_closing {
log::debug!(
"[bao] crash-safe teardown: worker '{}' via {:?}, joined={}, realm_unreg={}",
handle.script_url,
path,
thread_joined,
realm_unregistered,
);
}
WorkerTeardownResult {
path,
thread_joined,
realm_profile_unregistered: realm_unregistered,
closing_flag_set: true,
never_registered: handle.worker_global_addr() == 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_worker_teardown_path_equality() {
assert_eq!(WorkerTeardownPath::Terminate, WorkerTeardownPath::Terminate);
assert_eq!(WorkerTeardownPath::SelfClose, WorkerTeardownPath::SelfClose);
assert_eq!(
WorkerTeardownPath::PageUnload,
WorkerTeardownPath::PageUnload
);
assert_ne!(WorkerTeardownPath::Terminate, WorkerTeardownPath::SelfClose);
}
#[test]
fn test_worker_lifecycle_state_running() {
let handle = WorkerHandle::new("worker.js".to_string());
let guard = AutoCloseWorker::new(handle);
assert_eq!(guard.lifecycle_state(), WorkerLifecycleState::Running);
}
#[test]
fn test_worker_lifecycle_state_closing() {
let handle = WorkerHandle::new("worker.js".to_string());
let mut guard = AutoCloseWorker::new(handle);
guard.terminate_via(WorkerTeardownPath::Terminate);
assert_eq!(
guard.lifecycle_state(),
WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
);
}
#[test]
fn test_worker_lifecycle_state_terminated() {
let handle = WorkerHandle::new("worker.js".to_string());
let mut guard = AutoCloseWorker::new(handle);
guard.terminate_via(WorkerTeardownPath::SelfClose);
guard.handle().mark_terminated();
assert_eq!(
guard.lifecycle_state(),
WorkerLifecycleState::Terminated(WorkerTeardownPath::SelfClose)
);
}
#[test]
fn test_worker_lifecycle_states_snapshot() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
let snapshot = state.worker_lifecycle_states();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot[0].0, WorkerId("worker1.js".to_string()));
assert_eq!(snapshot[0].1, WorkerLifecycleState::Running);
assert_eq!(snapshot[1].0, WorkerId("worker2.js".to_string()));
assert_eq!(snapshot[1].1, WorkerLifecycleState::Running);
}
#[test]
fn test_worker_handle_global_addr_default_zero() {
let handle = WorkerHandle::new("worker.js".to_string());
assert_eq!(handle.worker_global_addr(), 0);
}
#[test]
fn test_worker_handle_global_addr_set_and_get() {
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(0xDEADBEEF);
assert_eq!(handle.worker_global_addr(), 0xDEADBEEF);
}
#[test]
fn test_worker_handle_global_addr_arc_shared() {
let handle = WorkerHandle::new("worker.js".to_string());
let arc = handle.worker_global_addr_arc();
arc.store(0xCAFEBABE_usize as u64, Ordering::Release);
assert_eq!(handle.worker_global_addr(), 0xCAFEBABE);
}
#[test]
fn test_worker_handle_unregister_stealth_profile_no_addr() {
let handle = WorkerHandle::new("worker.js".to_string());
handle.unregister_stealth_profile();
}
#[test]
fn test_worker_handle_unregister_stealth_profile_with_addr() {
let fake_addr = 0x12345678_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
handle.unregister_stealth_profile();
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
bao_stealth::engine_props::clear_all_realm_profiles();
}
#[test]
fn test_teardown_result_crash_safe() {
let result = WorkerTeardownResult {
path: WorkerTeardownPath::Terminate,
thread_joined: true,
realm_profile_unregistered: true,
closing_flag_set: true,
never_registered: false,
};
assert!(result.is_crash_safe());
}
#[test]
fn test_teardown_result_not_crash_safe_no_join() {
let result = WorkerTeardownResult {
path: WorkerTeardownPath::PageUnload,
thread_joined: false,
realm_profile_unregistered: true,
closing_flag_set: true,
never_registered: false,
};
assert!(!result.is_crash_safe());
}
#[test]
fn test_teardown_result_not_crash_safe_no_closing() {
let result = WorkerTeardownResult {
path: WorkerTeardownPath::SelfClose,
thread_joined: true,
realm_profile_unregistered: true,
closing_flag_set: false,
never_registered: false,
};
assert!(!result.is_crash_safe());
}
#[test]
fn test_crash_safe_teardown_no_web_worker() {
let handle = WorkerHandle::new("worker.js".to_string());
let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::Terminate);
assert!(result.closing_flag_set);
assert!(result.thread_joined); assert!(!result.realm_profile_unregistered); assert!(handle.is_closing());
assert!(handle.is_terminated());
}
#[test]
fn test_crash_safe_teardown_with_stealth_profile() {
let fake_addr = 0xABCD0000_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::SelfClose);
assert!(result.closing_flag_set);
assert!(result.thread_joined);
assert!(result.realm_profile_unregistered);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
assert!(handle.is_closing());
assert!(handle.is_terminated());
}
#[test]
fn test_auto_close_worker_drop_unregisters_stealth_profile() {
let fake_addr = 0xBEEF0000_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
let guard = AutoCloseWorker::new(handle);
drop(guard);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
}
#[test]
fn test_terminate_all_workers_unregisters_stealth_profiles() {
let fake_addr1 = 0xAAAA0001_usize;
let fake_addr2 = 0xAAAA0002_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr1,
&bao_stealth::StealthProfile::firefox_default(),
);
bao_stealth::engine_props::set_profile_for_global(
fake_addr2,
&bao_stealth::StealthProfile::firefox_default(),
);
let mut state = BaoWebViewState::default();
let h1 = WorkerHandle::new("worker1.js".to_string());
h1.set_worker_global_addr(fake_addr1);
let h2 = WorkerHandle::new("worker2.js".to_string());
h2.set_worker_global_addr(fake_addr2);
state.track_worker(h1);
state.track_worker(h2);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_some());
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_some());
state.terminate_all_workers();
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_none());
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_none());
assert!(state.active_workers.iter().all(|g| g.handle().is_closing()));
assert!(state
.active_workers
.iter()
.all(|g| g.handle().is_terminated()));
}
#[test]
fn test_terminate_worker_via_path_terminate() {
let fake_addr = 0xCCCC0001_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let mut state = BaoWebViewState::default();
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let worker_id = WorkerId("worker.js".to_string());
state.track_worker(handle.clone());
let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.path, WorkerTeardownPath::Terminate);
assert!(result.closing_flag_set);
assert!(result.thread_joined);
assert!(result.realm_profile_unregistered);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
assert!(handle.is_closing());
assert!(handle.is_terminated());
}
#[test]
fn test_terminate_worker_via_path_self_close() {
let fake_addr = 0xDDDD0001_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let mut state = BaoWebViewState::default();
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let worker_id = WorkerId("worker.js".to_string());
state.track_worker(handle.clone());
let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::SelfClose);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.path, WorkerTeardownPath::SelfClose);
assert!(result.closing_flag_set);
assert!(result.realm_profile_unregistered);
}
#[test]
fn test_terminate_worker_via_path_not_found() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("nonexistent.js".to_string());
let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
assert!(result.is_none());
}
#[test]
fn test_three_paths_all_crash_safe() {
for path in [
WorkerTeardownPath::Terminate,
WorkerTeardownPath::SelfClose,
WorkerTeardownPath::PageUnload,
] {
let fake_addr = 0x12340000_usize
+ match &path {
WorkerTeardownPath::Terminate => 1,
WorkerTeardownPath::SelfClose => 2,
WorkerTeardownPath::PageUnload => 3,
};
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let result = crash_safe_teardown_worker(&handle, path.clone());
assert!(
result.closing_flag_set,
"closing flag not set for {:?}",
path
);
assert!(result.thread_joined, "thread not joined for {:?}", path);
assert!(
result.realm_profile_unregistered,
"profile not unregistered for {:?}",
path
);
assert!(result.is_crash_safe(), "not crash-safe for {:?}", path);
assert!(handle.is_closing(), "handle not closing for {:?}", path);
assert!(
handle.is_terminated(),
"handle not terminated for {:?}",
path
);
assert!(
bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none(),
"profile not removed for {:?}",
path
);
}
}
#[test]
fn test_track_worker_guard() {
let handle = WorkerHandle::new("worker.js".to_string());
let guard = AutoCloseWorker::new(handle);
let mut state = BaoWebViewState::default();
state.track_worker_guard(guard);
assert_eq!(state.active_worker_count(), 1);
}
}