1use crate::error::{CdpError, CdpResult};
2use crate::event_filter::EventFilter;
3use crate::protocol::{WsCommand, WsResponse};
4use crate::rest_client::get_websocket_url;
5use futures_util::{SinkExt, StreamExt};
6use serde::Serialize;
7use std::collections::HashMap;
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use std::sync::{Arc, Mutex};
10use std::time::Duration;
11use tokio::sync::broadcast;
12use tokio::sync::mpsc;
13use tokio::time::timeout;
14use tokio_tungstenite::connect_async;
15use tokio_tungstenite::tungstenite::Message;
16use tracing::{debug, error, trace};
17
18#[derive(Clone)]
19pub struct CdpClient {
20 command_tx: mpsc::UnboundedSender<String>,
21 pending_requests: Arc<Mutex<HashMap<u64, oneshot::Sender<WsResponse>>>>,
22 event_tx: broadcast::Sender<WsResponse>,
23 next_id: Arc<AtomicU64>,
24 default_timeout: Duration,
25 is_alive: Arc<AtomicBool>,
26}
27
28impl CdpClient {
29 pub async fn new(host: &str, default_timeout: Duration) -> CdpResult<Self> {
30 let ws_url = get_websocket_url(host).await?;
31
32 let (ws_stream, _) = connect_async(ws_url).await?;
33 let (mut ws_sink, mut ws_stream) = ws_stream.split();
34
35 let pending_requests = Arc::new(Mutex::new(
36 HashMap::<u64, oneshot::Sender<WsResponse>>::new(),
37 ));
38 let pending_requests_clone = Arc::clone(&pending_requests);
39
40 let (command_tx, mut command_rx) = mpsc::unbounded_channel::<String>();
41
42 let (event_tx, _) = broadcast::channel(128);
43 let event_tx_clone = event_tx.clone();
44
45 let is_alive = Arc::new(AtomicBool::new(true));
46 let is_alive_reader = is_alive.clone();
47
48 tokio::spawn(async move {
49 let result: CdpResult<()> = async {
50 while let Some(json_str) = command_rx.recv().await {
51 debug!("About to send command {}", json_str);
52 ws_sink.send(Message::Text(json_str.into())).await?;
53 }
54 Ok(())
55 }
56 .await;
57
58 if let Err(e) = result {
59 error!("Fatal error in CDP Writer task: {}", e);
60 }
61 });
62
63 tokio::spawn(async move {
64 let reader_result: CdpResult<()> = async {
65 while let Some(result_stream) = ws_stream.next().await {
66 let msg = result_stream.map_err(CdpError::from)?;
67
68 if let Message::Text(received_text) = msg {
69 let response: WsResponse = serde_json::from_str(&received_text)?;
70
71 if let Some(id) = response.id {
72 let mut map = pending_requests_clone.lock().map_err(|_| {
73 CdpError::InternalError(
74 "Could not acquire mutex lock on pending requests cloned map"
75 .to_string(),
76 )
77 })?;
78
79 if let Some(responder) = map.remove(&id) {
80 trace!("Received expected id {} from CDP", id);
81 let _ = responder.send(response);
82 } else {
83 trace!("Discarded message with id {} (no listener found)", id);
84 }
85 } else if let Some(ref method) = response.method {
86 trace!("CDP Event: {}", method);
87 let _ = event_tx_clone.send(response.clone());
88 } else {
89 debug!(
90 "Received unexpected message from CDP (no id, no method): {}",
91 received_text
92 );
93 }
94 }
95 }
96 Ok(())
97 }
98 .await;
99
100 is_alive_reader.store(false, Ordering::SeqCst);
101
102 match reader_result {
103 Err(e) => debug!("CDP Connection lost due to error: {}", e),
104 Ok(_) => debug!("CDP Connection closed by server/gracefully"),
105 }
106
107 if let Ok(mut map) = pending_requests_clone.lock() {
108 debug!(
109 "Closing {} pending requests due to disconnection",
110 map.len()
111 );
112 map.clear(); }
114 });
115
116 let next_id = Arc::new(AtomicU64::new(1));
117 Ok(Self {
118 command_tx,
119 pending_requests,
120 event_tx,
121 next_id,
122 default_timeout,
123 is_alive,
124 })
125 }
126
127 pub fn subscribe(&self) -> broadcast::Receiver<WsResponse> {
128 self.event_tx.subscribe()
129 }
130
131 pub fn on_domain(&self, domain: &'static str) -> EventFilter {
132 EventFilter::new(self.subscribe(), domain)
133 }
134
135 pub async fn send_raw_command<P: Serialize>(
136 &self,
137 method: &str,
138 params: P,
139 ) -> CdpResult<WsResponse> {
140 if !self.is_alive.load(Ordering::SeqCst) {
141 return Err(CdpError::Disconnected);
142 }
143
144 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
145 let (tx, rx) = oneshot::channel();
146
147 {
148 let mut map = self.pending_requests.lock().map_err(|_| {
149 CdpError::InternalError(
150 "Mutex poisoned: another thread panicked while holding the lock".into(),
151 )
152 })?;
153 map.insert(id, tx);
154 }
155
156 let cmd = WsCommand {
157 id,
158 method: method.to_string(),
159 params: Some(params),
160 };
161
162 let json_payload = serde_json::to_string(&cmd)?;
163 self.command_tx.send(json_payload).map_err(|e| {
164 self.is_alive.store(false, Ordering::SeqCst);
165 CdpError::InternalError(format!("Failed to send command to writer task: {}", e))
166 })?;
167
168 match timeout(self.default_timeout, rx).await {
169 Ok(Ok(response)) => {
170 if let Some(error_obj) = response.error {
171 return Err(CdpError::ProtocolError {
172 code: error_obj["code"].as_i64().unwrap_or(-1),
173 message: error_obj["message"]
174 .as_str()
175 .unwrap_or("Unknown CDP error")
176 .to_string(),
177 });
178 }
179 Ok(response)
180 }
181 Ok(Err(_)) => Err(CdpError::Disconnected),
182 Err(_) => {
183 if let Ok(mut map) = self.pending_requests.lock() {
184 map.remove(&id);
185 }
186
187 Err(CdpError::Timeout {
188 method: method.to_string(),
189 timeout: self.default_timeout,
190 })
191 }
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use std::time::Duration;
200
201 #[tokio::test]
202 async fn test_command_timeout_and_cleanup() {
203 let (command_tx, _command_rx) = mpsc::unbounded_channel();
204
205 let client = CdpClient {
206 command_tx,
207 pending_requests: Arc::new(Mutex::new(HashMap::new())),
208 event_tx: broadcast::channel(1).0,
209 next_id: Arc::new(AtomicU64::new(1)),
210 default_timeout: Duration::from_millis(100),
211 is_alive: Arc::new(AtomicBool::new(true)),
212 };
213
214 let result = client
215 .send_raw_command("Page.navigate", serde_json::json!({"url": "about:blank"}))
216 .await;
217
218 match result {
219 Err(CdpError::Timeout { method, .. }) => {
220 assert_eq!(method, "Page.navigate");
221 println!("✅ Timeout detected successfully");
222 }
223 other => panic!("❌ Expected Timeout, but got: {:?}", other),
224 }
225
226 let pending = client.pending_requests.lock().unwrap();
227 assert_eq!(pending.len(), 0, "❌ ID was not removed from HashMap");
228 println!("✅ HashMap is clean, no memory leaks");
229 }
230
231 #[tokio::test]
232 async fn test_protocol_error_mapping() {
233 let (command_tx, _command_rx) = mpsc::unbounded_channel();
234 let client = CdpClient {
235 command_tx,
236 pending_requests: Arc::new(Mutex::new(HashMap::new())),
237 event_tx: broadcast::channel(1).0,
238 next_id: Arc::new(AtomicU64::new(1)),
239 default_timeout: Duration::from_secs(1),
240 is_alive: Arc::new(AtomicBool::new(true)),
241 };
242
243 let client_clone = client.clone();
244
245 let handle = tokio::spawn(async move {
246 client_clone
247 .send_raw_command("Page.navigate", serde_json::json!({"url": "invalid-url"}))
248 .await
249 });
250
251 tokio::time::sleep(Duration::from_millis(10)).await;
253
254 let responder = {
255 let mut map = client.pending_requests.lock().unwrap();
256 map.remove(&1)
257 .expect("The client should have registered request with ID 1")
258 };
259
260 let error_response = WsResponse {
262 id: Some(1),
263 result: None,
264 method: None,
265 params: None,
266 error: Some(serde_json::json!({
267 "code": -32000,
268 "message": "Cannot navigate to invalid URL"
269 })),
270 };
271
272 responder.send(error_response).unwrap();
273
274 let result = handle.await.unwrap();
276 match result {
277 Err(CdpError::ProtocolError { code, message }) => {
278 assert_eq!(code, -32000);
279 assert_eq!(message, "Cannot navigate to invalid URL");
280 println!("✅ Protocol Error mapped correctly");
281 }
282 other => panic!("❌ Expected ProtocolError, but got: {:?}", other),
283 }
284 }
285
286 #[tokio::test]
287 async fn test_event_broadcasting_and_filtering() {
288 let (command_tx, _command_rx) = mpsc::unbounded_channel();
289 let (event_tx, _) = broadcast::channel(32);
290
291 let client = CdpClient {
292 command_tx,
293 pending_requests: Arc::new(Mutex::new(HashMap::new())),
294 event_tx: event_tx.clone(),
295 next_id: Arc::new(AtomicU64::new(1)),
296 default_timeout: Duration::from_secs(1),
297 is_alive: Arc::new(AtomicBool::new(true)),
298 };
299
300 let mut page_events = client.on_domain("Page");
301
302 let mock_event = WsResponse {
304 id: None, result: None,
306 method: Some("Page.loadEventFired".to_string()),
307 params: Some(serde_json::json!({
308 "timestamp": 12345.678
309 })),
310 error: None,
311 };
312
313 let event_to_send = mock_event.clone();
315 tokio::spawn(async move {
316 tokio::time::sleep(Duration::from_millis(10)).await;
318 let _ = event_tx.send(event_to_send);
319 });
320
321 let received_event = timeout(Duration::from_secs(1), page_events.next())
323 .await
324 .expect("Timeout waiting for Page event")
325 .expect("Stream closed unexpectedly")
326 .expect("Failed to receive event from broadcast channel");
327
328 assert_eq!(
329 received_event.method.as_deref(),
330 Some("Page.loadEventFired")
331 );
332
333 let timestamp = received_event
334 .params
335 .as_ref()
336 .and_then(|p| p.get("timestamp"))
337 .and_then(|t| t.as_f64());
338
339 assert_eq!(timestamp, Some(12345.678));
340 println!("✅ Event correctly filtered and received by domain subscriber");
341 }
342
343 #[tokio::test]
344 async fn test_broadcast_lagged_error() {
345 let (command_tx, _command_rx) = mpsc::unbounded_channel();
347 let (event_tx, _) = broadcast::channel(4);
348
349 let client = CdpClient {
350 command_tx,
351 pending_requests: Arc::new(Mutex::new(HashMap::new())),
352 event_tx: event_tx.clone(),
353 next_id: Arc::new(AtomicU64::new(1)),
354 default_timeout: Duration::from_secs(1),
355 is_alive: Arc::new(AtomicBool::new(true)),
356 };
357
358 let mut page_events = client.on_domain("Page");
359
360 for i in 0..10 {
363 let mock_event = WsResponse {
364 id: None,
365 result: None,
366 method: Some("Page.event".to_string()),
367 params: Some(serde_json::json!({ "index": i })),
368 error: None,
369 };
370 let _ = event_tx.send(mock_event);
371 }
372
373 let result = page_events.next().await;
376
377 match result {
380 Some(Err(CdpError::InternalError(msg))) => {
381 assert!(msg.contains("channel overflow") || msg.contains("lagged"));
383 println!("✅ Correctly detected lagged subscriber (buffer overflow)");
384 }
385 Some(Ok(event)) => {
386 panic!(
387 "❌ Expected a Lagged error, but received a successful event: {:?}",
388 event
389 );
390 }
391 None => panic!("❌ Stream closed unexpectedly"),
392 _ => {}
393 }
394
395 println!("✅ Buffer overflow test passed: Oldest messages were dropped as expected");
396 }
397
398 #[tokio::test]
399 async fn test_connection_drop_during_request() {
400 let (command_tx, _command_rx) = mpsc::unbounded_channel();
401 let pending_requests = Arc::new(Mutex::new(HashMap::new()));
402
403 let client = CdpClient {
404 command_tx,
405 pending_requests: pending_requests.clone(),
406 event_tx: broadcast::channel(1).0,
407 next_id: Arc::new(AtomicU64::new(1)),
408 default_timeout: Duration::from_secs(5),
409 is_alive: Arc::new(AtomicBool::new(true)),
410 };
411
412 let client_clone = client.clone();
413 let request_handle = tokio::spawn(async move {
414 client_clone
415 .send_raw_command("Debugger.enable", serde_json::json!({}))
416 .await
417 });
418
419 tokio::time::sleep(Duration::from_millis(10)).await;
421 {
422 let mut map = pending_requests.lock().unwrap();
423 map.clear();
426 }
427 client.is_alive.store(false, Ordering::SeqCst);
428
429 let result = request_handle.await.unwrap();
430 match result {
431 Err(CdpError::Disconnected) => {
432 println!("✅ Correctly detected disconnection during pending request");
433 }
434 other => panic!("❌ Expected CdpError::Disconnected, but got: {:?}", other),
435 }
436 }
437}