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(); }
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 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 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 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 let mock_event = WsResponse {
279 id: None, 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 let event_to_send = mock_event.clone();
290 tokio::spawn(async move {
291 tokio::time::sleep(Duration::from_millis(10)).await;
293 let _ = event_tx.send(event_to_send);
294 });
295
296 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 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 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 let result = page_events.next().await;
346
347 match result {
350 Some(Err(CdpError::InternalError(msg))) => {
351 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 tokio::time::sleep(Duration::from_millis(10)).await;
386 {
387 let mut map = pending_requests.lock().unwrap();
388 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}