Skip to main content

cdp_lite/
client.rs

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(); // Dropping the 'oneshot' senders sends an Err to the receivers
113            }
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        // Simulate server time response
252        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        // Simulate response with error in Browser
261        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        // Verify the client mapped the JSON to the CdpError enum
275        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        // Mock a CDP Event
303        let mock_event = WsResponse {
304            id: None, // Events never have an ID
305            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        // Simulate the reader task sending the event to the broadcast channel
314        let event_to_send = mock_event.clone();
315        tokio::spawn(async move {
316            // Wait a bit to ensure the subscriber is ready
317            tokio::time::sleep(Duration::from_millis(10)).await;
318            let _ = event_tx.send(event_to_send);
319        });
320
321        // Await the event in our filter
322        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        // Setup with a very small broadcast buffer (only 4 messages allowed)
346        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        // Flood the channel with 10 events without reading them
361        // This will exceed the buffer capacity of 4
362        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        // Try to read from the subscriber
374        // Since we sent 10 messages but the buffer is 4, we are "lagged"
375        let result = page_events.next().await;
376
377        // The first call to next() after a lag should return an Error
378        // specifically a CdpError that wraps broadcast::error::RecvError::Lagged
379        match result {
380            Some(Err(CdpError::InternalError(msg))) => {
381                // Your CdpError::from implementation for RecvError likely formats it as a string
382                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        // Simulate a sudden disconnection
420        tokio::time::sleep(Duration::from_millis(10)).await;
421        {
422            let mut map = pending_requests.lock().unwrap();
423            // Dropping the 'oneshot::Sender' is what happens when we clear the map
424            // or the task dies. This sends a 'RecvError' to the 'Receiver'.
425            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}