use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use serde_json::Value;
use crate::backend::{CdpBackend, ExternalBackend, InternalBackend};
use crate::protocol::CdpError;
fn rand_id() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
let d = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
d.as_nanos() as u64 ^ (d.as_nanos() as u64).wrapping_shr(17)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendKind {
Internal,
External,
}
type EventHandler = Box<dyn Fn(Value)>;
struct SessionInner {
target_id: String,
backend: BackendKind,
enabled_domains: RefCell<std::collections::HashSet<String>>,
event_handlers: RefCell<HashMap<String, EventHandler>>,
}
pub struct CdpRouter {
internal: InternalBackend,
external: RefCell<Option<ExternalBackend>>,
sessions: RefCell<HashMap<String, Rc<SessionInner>>>,
}
impl Default for CdpRouter {
fn default() -> Self {
CdpRouter {
internal: InternalBackend::new(),
external: RefCell::new(None),
sessions: RefCell::new(HashMap::new()),
}
}
}
impl CdpRouter {
pub fn new() -> Self {
Self::default()
}
pub fn create_internal_session(&self, target_id: &str) -> CdpSession {
let session_id = format!("{:016x}", rand_id());
let inner = Rc::new(SessionInner {
target_id: target_id.to_string(),
backend: BackendKind::Internal,
enabled_domains: RefCell::new(std::collections::HashSet::new()),
event_handlers: RefCell::new(HashMap::new()),
});
self.sessions
.borrow_mut()
.insert(session_id.clone(), Rc::clone(&inner));
CdpSession { session_id, inner }
}
pub fn connect_external(&self, endpoint: &str) -> Result<ExternalBrowser, CdpError> {
let backend = ExternalBackend::new(endpoint)?;
*self.external.borrow_mut() = Some(backend);
let session_id = format!("{:016x}", rand_id());
let inner = Rc::new(SessionInner {
target_id: endpoint.to_string(),
backend: BackendKind::External,
enabled_domains: RefCell::new(std::collections::HashSet::new()),
event_handlers: RefCell::new(HashMap::new()),
});
self.sessions
.borrow_mut()
.insert(session_id.clone(), Rc::clone(&inner));
Ok(ExternalBrowser {
endpoint: endpoint.to_string(),
session_id,
})
}
pub fn send_command(
&self,
session_id: &str,
method: &str,
params: Option<Value>,
) -> Result<Value, CdpError> {
let sessions = self.sessions.borrow();
let session = sessions.get(session_id).ok_or_else(|| CdpError {
code: -32602,
message: format!("session not found: {session_id}"),
})?;
match session.backend {
BackendKind::Internal => {
self.internal
.send_command(method, ¶ms, &session.target_id)
}
BackendKind::External => {
let external = self.external.borrow();
let backend = external.as_ref().ok_or_else(|| CdpError {
code: -32603,
message: "external backend not connected".into(),
})?;
backend.send_command(method, ¶ms, &session.target_id)
}
}
}
pub fn detach_session(&self, session_id: &str) -> Result<(), CdpError> {
self.sessions
.borrow_mut()
.remove(session_id)
.ok_or_else(|| CdpError {
code: -32602,
message: format!("session not found: {session_id}"),
})?;
Ok(())
}
}
pub struct ExternalBrowser {
pub endpoint: String,
pub session_id: String,
}
pub struct CdpSession {
session_id: String,
inner: Rc<SessionInner>,
}
impl CdpSession {
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn target_id(&self) -> &str {
&self.inner.target_id
}
pub fn backend_kind(&self) -> BackendKind {
self.inner.backend
}
pub fn send(
&self,
router: &CdpRouter,
method: &str,
params: Option<Value>,
) -> Result<Value, CdpError> {
let domain = method.split('.').next().unwrap_or("");
self.inner
.enabled_domains
.borrow_mut()
.insert(domain.to_string());
router.send_command(&self.session_id, method, params)
}
pub fn on<F: Fn(Value) + 'static>(&self, event: &str, handler: F) {
self.inner
.event_handlers
.borrow_mut()
.insert(event.to_string(), Box::new(handler));
}
pub fn detach(&self, router: &CdpRouter) -> Result<(), CdpError> {
router.detach_session(&self.session_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_creates_empty_router() {
let router = CdpRouter::new();
assert!(router.sessions.borrow().is_empty());
}
#[test]
fn default_same_as_new() {
let router_new = CdpRouter::new();
let router_default = CdpRouter::default();
assert!(router_new.sessions.borrow().is_empty());
assert!(router_default.sessions.borrow().is_empty());
}
#[test]
fn create_internal_session_returns_session_with_id() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
assert!(!session.session_id().is_empty());
assert_eq!(session.session_id().len(), 16);
}
#[test]
fn create_internal_session_has_internal_backend() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
assert_eq!(session.backend_kind(), BackendKind::Internal);
}
#[test]
fn create_internal_session_has_target_id() {
let router = CdpRouter::new();
let session = router.create_internal_session("my-target-123");
assert_eq!(session.target_id(), "my-target-123");
}
#[test]
fn send_command_on_valid_session_returns_ok() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let result = router.send_command(
session.session_id(),
"Target.getTargetInfo",
Some(serde_json::json!({})),
);
assert!(result.is_ok());
}
#[test]
fn send_command_on_invalid_session_returns_error() {
let router = CdpRouter::new();
let result = router.send_command("nonexistent-id", "Target.getTargetInfo", None);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code, -32602);
assert!(err.message.contains("session not found"));
}
#[test]
fn detach_session_removes_session() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let session_id = session.session_id().to_string();
assert_eq!(router.sessions.borrow().len(), 1);
let result = router.detach_session(&session_id);
assert!(result.is_ok());
assert!(router.sessions.borrow().is_empty());
}
#[test]
fn detach_session_twice_returns_error_on_second() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let session_id = session.session_id().to_string();
let first = router.detach_session(&session_id);
assert!(first.is_ok());
let second = router.detach_session(&session_id);
assert!(second.is_err());
let err = second.unwrap_err();
assert_eq!(err.code, -32602);
assert!(err.message.contains("session not found"));
}
#[test]
fn backend_kind_variants_equality() {
assert_eq!(BackendKind::Internal, BackendKind::Internal);
assert_eq!(BackendKind::External, BackendKind::External);
assert_ne!(BackendKind::Internal, BackendKind::External);
}
#[test]
fn backend_kind_debug_format() {
assert_eq!(format!("{:?}", BackendKind::Internal), "Internal");
assert_eq!(format!("{:?}", BackendKind::External), "External");
}
#[test]
fn cdp_session_session_id_returns_correct_id() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let session_id = session.session_id();
assert!(!session_id.is_empty());
assert_eq!(session_id.len(), 16);
}
#[test]
fn cdp_session_target_id_returns_correct_target() {
let router = CdpRouter::new();
let session = router.create_internal_session("test-target-xyz");
assert_eq!(session.target_id(), "test-target-xyz");
}
#[test]
fn cdp_session_backend_kind_returns_internal() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
assert_eq!(session.backend_kind(), BackendKind::Internal);
}
#[test]
fn cdp_session_send_registers_domain() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let _ = session.send(&router, "Page.enable", Some(serde_json::json!({})));
let sessions = router.sessions.borrow();
let inner = sessions.get(session.session_id()).unwrap();
assert!(inner.enabled_domains.borrow().contains("Page"));
}
#[test]
fn two_sessions_work_independently() {
let router = CdpRouter::new();
let session1 = router.create_internal_session("target-1");
let session2 = router.create_internal_session("target-2");
assert_ne!(session1.session_id(), session2.session_id());
assert_eq!(session1.target_id(), "target-1");
assert_eq!(session2.target_id(), "target-2");
let _ = session1.send(&router, "Page.enable", None);
let _ = session2.send(&router, "Network.enable", None);
let sessions = router.sessions.borrow();
let inner1 = sessions.get(session1.session_id()).unwrap();
let inner2 = sessions.get(session2.session_id()).unwrap();
assert!(inner1.enabled_domains.borrow().contains("Page"));
assert!(!inner1.enabled_domains.borrow().contains("Network"));
assert!(inner2.enabled_domains.borrow().contains("Network"));
assert!(!inner2.enabled_domains.borrow().contains("Page"));
}
#[test]
fn cdp_session_on_registers_event_handler() {
use std::cell::Cell;
use std::rc::Rc;
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let called = Rc::new(Cell::new(false));
let called_clone = Rc::clone(&called);
session.on("Page.loadEventFired", move |_event| {
called_clone.set(true);
});
let sessions = router.sessions.borrow();
let inner = sessions.get(session.session_id()).unwrap();
assert!(inner
.event_handlers
.borrow()
.contains_key("Page.loadEventFired"));
let binding = inner.event_handlers.borrow();
let handler = binding.get("Page.loadEventFired").unwrap();
handler(serde_json::json!({}));
assert!(called.get());
}
#[test]
fn cdp_session_detach_removes_session() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let _session_id = session.session_id().to_string();
assert_eq!(router.sessions.borrow().len(), 1);
let result = session.detach(&router);
assert!(result.is_ok());
assert!(router.sessions.borrow().is_empty());
}
#[test]
fn backend_kind_copy_clone() {
let kind = BackendKind::Internal;
let copied = kind;
let cloned = kind.clone();
assert_eq!(kind, copied);
assert_eq!(kind, cloned);
fn takes_copy<T: Copy>(_: T) {}
takes_copy(BackendKind::Internal);
fn takes_clone<T: Clone>(_: T) {}
takes_clone(BackendKind::Internal);
}
#[test]
fn external_browser_fields_accessible() {
let browser = ExternalBrowser {
endpoint: "ws://localhost:9222".to_string(),
session_id: "abcd1234efgh5678".to_string(),
};
assert_eq!(browser.endpoint, "ws://localhost:9222");
assert_eq!(browser.session_id, "abcd1234efgh5678");
}
#[test]
fn create_multiple_sessions_unique_ids() {
let router = CdpRouter::new();
let mut ids = std::collections::HashSet::new();
for i in 0..10 {
let session = router.create_internal_session(&format!("target-{i}"));
assert!(
ids.insert(session.session_id().to_string()),
"session ids must be unique"
);
}
assert_eq!(router.sessions.borrow().len(), 10);
}
#[test]
fn send_command_unknown_domain_still_returns() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let result = session.send(&router, "UnknownDomain.someMethod", None);
let _ = result;
}
#[test]
fn send_registers_multiple_domains() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let _ = session.send(&router, "Page.enable", None);
let _ = session.send(&router, "Runtime.enable", None);
let _ = session.send(&router, "Network.enable", None);
let sessions = router.sessions.borrow();
let inner = sessions.get(session.session_id()).unwrap();
let domains = inner.enabled_domains.borrow();
assert!(domains.contains("Page"));
assert!(domains.contains("Runtime"));
assert!(domains.contains("Network"));
assert_eq!(domains.len(), 3);
}
#[test]
fn session_id_is_hex_format() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let id = session.session_id();
assert!(
id.chars().all(|c| c.is_ascii_hexdigit()),
"session id must be hex"
);
}
#[test]
fn detach_one_of_two_sessions() {
let router = CdpRouter::new();
let session1 = router.create_internal_session("target-1");
let session2 = router.create_internal_session("target-2");
assert_eq!(router.sessions.borrow().len(), 2);
let id1 = session1.session_id().to_string();
router.detach_session(&id1).unwrap();
assert_eq!(router.sessions.borrow().len(), 1);
let result = session2.send(&router, "Page.enable", None);
let _ = result;
}
#[test]
fn send_command_with_params() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let result = session.send(
&router,
"Page.navigate",
Some(serde_json::json!({"url": "https://example.com"})),
);
let _ = result;
}
#[test]
fn send_command_with_null_params() {
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let result = session.send(&router, "Page.enable", Some(serde_json::Value::Null));
let _ = result;
}
#[test]
fn event_handler_receives_value() {
use std::sync::{Arc, Mutex};
let router = CdpRouter::new();
let session = router.create_internal_session("target-1");
let received: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let received_clone = Arc::clone(&received);
session.on("Runtime.consoleAPICalled", move |event| {
*received_clone.lock().unwrap() = Some(event.to_string());
});
let sessions = router.sessions.borrow();
let inner = sessions.get(session.session_id()).unwrap();
let binding = inner.event_handlers.borrow();
let handler = binding.get("Runtime.consoleAPICalled").unwrap();
let test_event = serde_json::json!({"type": "log", "args": [42]});
handler(test_event.clone());
let stored = received.lock().unwrap().clone();
assert_eq!(stored, Some(test_event.to_string()));
}
#[test]
fn backend_kind_all_variants() {
let variants = [BackendKind::Internal, BackendKind::External];
for v in &variants {
let _debug = format!("{:?}", v);
let _copy = *v;
let _clone = v.clone();
}
}
#[test]
fn external_browser_empty_endpoint() {
let browser = ExternalBrowser {
endpoint: String::new(),
session_id: String::new(),
};
assert!(browser.endpoint.is_empty());
assert!(browser.session_id.is_empty());
}
}