cdp_lite/
client.rs

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