1use crate::error::{CdpError, Result};
2use futures_util::{SinkExt, StreamExt};
3use serde_json::Value;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::Duration;
7use tokio::sync::{broadcast, mpsc, oneshot};
8use tokio::task::JoinHandle;
9use tokio_tungstenite::{MaybeTlsStream, WebSocketStream, connect_async};
10
11#[cfg(target_os = "macos")]
12use crate::browser::dismiss_allow_debugging_dialog;
13use tokio_tungstenite::tungstenite::Message;
14pub(crate) static NEXT_CDP_ID: AtomicU64 = AtomicU64::new(1);
15
16#[derive(Debug, Clone)]
18pub struct CdpEvent {
19 pub method: String,
20 pub params: Value,
21 pub session_id: Option<String>,
22}
23
24pub(crate) struct PendingCall {
26 method: String,
27 tx: oneshot::Sender<Result<Value>>,
28}
29
30pub(crate) type PendingMap = HashMap<u64, PendingCall>;
31
32pub(crate) enum InternalMessage {
34 Call {
35 id: u64,
36 method: String,
37 params: Value,
38 session_id: Option<String>,
39 tx: oneshot::Sender<Result<Value>>,
40 },
41}
42
43pub struct Connection {
48 write: mpsc::UnboundedSender<InternalMessage>,
49 events: broadcast::Sender<CdpEvent>,
50 handle: JoinHandle<()>,
51}
52
53impl std::fmt::Debug for Connection {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("Connection").finish_non_exhaustive()
56 }
57}
58
59impl Connection {
60 pub async fn connect(ws_url: &str) -> Result<Self> {
62 let ws_url_owned = ws_url.to_owned();
64 let connect_fut = tokio::spawn(async move { connect_async(&ws_url_owned).await });
65
66 #[cfg(target_os = "macos")]
70 {
71 tokio::time::sleep(Duration::from_millis(600)).await;
72 dismiss_allow_debugging_dialog();
73 }
74
75 let (ws_stream, _) = connect_fut
77 .await
78 .map_err(|e| CdpError::ConnectionFailed {
79 detail: format!("task join: {e}"),
80 })?
81 .map_err(|e| CdpError::ConnectionFailed {
82 detail: format!("WebSocket connect to {ws_url} failed: {e}"),
83 })?;
84
85 let (write_tx, write_rx) = mpsc::unbounded_channel::<InternalMessage>();
86 let (events_tx, _) = broadcast::channel::<CdpEvent>(256);
87 let pending: PendingMap = HashMap::new();
88
89 let (ws_writer, ws_reader) = ws_stream.split();
90 let events_clone = events_tx.clone();
91
92 let handle = tokio::spawn(async move {
93 Self::run(write_rx, ws_writer, ws_reader, pending, events_clone).await;
94 });
95
96 Ok(Connection {
97 write: write_tx,
98 events: events_tx,
99 handle,
100 })
101 }
102
103 async fn run(
105 mut write_rx: mpsc::UnboundedReceiver<InternalMessage>,
106 mut ws_writer: futures_util::stream::SplitSink<
107 WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>,
108 Message,
109 >,
110 mut ws_reader: futures_util::stream::SplitStream<
111 WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>,
112 >,
113 mut pending: PendingMap,
114 events: broadcast::Sender<CdpEvent>,
115 ) {
116 loop {
117 tokio::select! {
118 msg = write_rx.recv() => {
120 match msg {
121 Some(InternalMessage::Call { id, method, params, session_id, tx }) => {
122 let cmd = if let Some(ref sid) = session_id {
123 serde_json::json!({
124 "id": id,
125 "method": method,
126 "params": params,
127 "sessionId": sid,
128 })
129 } else {
130 serde_json::json!({
131 "id": id,
132 "method": method,
133 "params": params,
134 })
135 };
136 let text = match serde_json::to_string(&cmd) {
137 Ok(t) => t,
138 Err(e) => {
139 tracing::warn!("Failed to serialize CDP command: {e}");
140 let _ = tx.send(Err(CdpError::Json(e)));
141 continue;
142 }
143 };
144 pending.insert(id, PendingCall { method, tx });
146 if let Err(e) = ws_writer.send(Message::Text(text)).await {
147 tracing::warn!("WS send error: {e}");
148 if let Some(pc) = pending.remove(&id) {
149 let _ = pc.tx.send(Err(CdpError::ConnectionFailed {
150 detail: format!("WebSocket send failed: {e}"),
151 }));
152 }
153 break;
154 }
155 }
156 None => break,
157 }
158 }
159 msg = ws_reader.next() => {
161 match msg {
162 Some(Ok(Message::Text(text))) => {
163 if let Ok(value) = serde_json::from_str::<Value>(&text) {
164 Self::dispatch_message(value, &mut pending, &events).await;
165 }
166 }
167 Some(Ok(Message::Close(frame))) => {
168 tracing::debug!("CDP WebSocket closed: {frame:?}");
169 break;
170 }
171 Some(Ok(Message::Binary(_))) => {
172 }
174 Some(Err(e)) => {
175 tracing::warn!("CDP WebSocket read error: {e}");
176 break;
177 }
178 None => {
179 tracing::debug!("CDP WebSocket stream ended");
180 break;
181 }
182 _ => {}
183 }
184 }
185 }
186 }
187
188 for (_, pc) in pending.drain() {
190 let _ = pc.tx.send(Err(CdpError::ConnectionFailed {
191 detail: "WebSocket connection closed".into(),
192 }));
193 }
194 }
195
196 async fn dispatch_message(
198 value: Value,
199 pending: &mut PendingMap,
200 events: &broadcast::Sender<CdpEvent>,
201 ) {
202 if let Some(id) = value.get("id").and_then(|v| v.as_u64()) {
203 if let Some(pc) = pending.remove(&id) {
205 let result = if let Some(err) = value.get("error") {
206 let detail = err
207 .get("message")
208 .and_then(|v| v.as_str())
209 .unwrap_or("unknown error")
210 .to_string();
211 Err(CdpError::CdpCallFailed {
212 method: pc.method,
213 detail,
214 })
215 } else {
216 Ok(value.get("result").cloned().unwrap_or(Value::Null))
217 };
218 let _ = pc.tx.send(result);
219 }
220 } else if let Some(method) = value.get("method").and_then(|v| v.as_str()) {
221 let session_id = value
223 .get("sessionId")
224 .and_then(|v| v.as_str())
225 .map(String::from);
226 let evt = CdpEvent {
227 method: method.to_string(),
228 params: value.get("params").cloned().unwrap_or(Value::Null),
229 session_id,
230 };
231 let _ = events.send(evt);
232 }
233 }
234
235 pub async fn call(
240 &self,
241 method: &str,
242 params: Value,
243 session_id: Option<&str>,
244 ) -> Result<Value> {
245 let id = NEXT_CDP_ID.fetch_add(1, Ordering::Relaxed);
246 let (tx, rx) = oneshot::channel();
247
248 let msg = InternalMessage::Call {
249 id,
250 method: method.to_string(),
251 params,
252 session_id: session_id.map(String::from),
253 tx,
254 };
255
256 self.write
257 .send(msg)
258 .map_err(|_| CdpError::ConnectionFailed {
259 detail: "background I/O task has terminated".into(),
260 })?;
261
262 tokio::time::timeout(Duration::from_secs(30), rx)
263 .await
264 .map_err(|_| CdpError::CdpCallFailed {
265 method: method.to_string(),
266 detail: "timeout waiting for response".into(),
267 })? .map_err(|_| CdpError::CdpCallFailed {
269 method: method.to_string(),
270 detail: "oneshot channel closed".into(),
271 })? }
273
274 pub fn event_rx(&self) -> broadcast::Receiver<CdpEvent> {
276 self.events.subscribe()
277 }
278
279 pub(crate) fn write_tx(&self) -> mpsc::UnboundedSender<InternalMessage> {
282 self.write.clone()
283 }
284
285 pub async fn close(self) {
288 let Self {
289 write,
290 events: _,
291 handle,
292 } = self;
293 drop(write); let _ = handle.await;
295 }
296}
297
298pub(crate) fn call_async(
301 write: &mpsc::UnboundedSender<InternalMessage>,
302 method: &str,
303 params: Value,
304 session_id: Option<String>,
305) {
306 let id = NEXT_CDP_ID.fetch_add(1, Ordering::Relaxed);
307 let (tx, _) = oneshot::channel();
308 let msg = InternalMessage::Call {
309 id,
310 method: method.to_string(),
311 params,
312 session_id,
313 tx,
314 };
315 let _ = write.send(msg);
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use serde_json::json;
322
323 #[test]
328 fn test_dispatch_message_routes_response_to_oneshot() {
329 let mut pending: PendingMap = HashMap::new();
330 let (tx, mut rx) = oneshot::channel();
331 pending.insert(
332 1,
333 PendingCall {
334 method: "Test.method".into(),
335 tx,
336 },
337 );
338 let (event_tx, _) = broadcast::channel::<CdpEvent>(256);
339
340 let rt = tokio::runtime::Runtime::new().unwrap();
341 rt.block_on(Connection::dispatch_message(
342 json!({"id": 1, "result": {"data": "hello"}}),
343 &mut pending,
344 &event_tx,
345 ));
346
347 assert!(pending.is_empty(), "pending call should be removed");
348 let received = rx.try_recv().expect("oneshot should have been sent");
349 assert!(received.is_ok(), "response should be Ok");
350 assert_eq!(
351 received.unwrap().get("data").and_then(|v| v.as_str()),
352 Some("hello")
353 );
354 }
355
356 #[test]
357 fn test_dispatch_message_routes_error_to_oneshot() {
358 let mut pending: PendingMap = HashMap::new();
359 let (tx, mut rx) = oneshot::channel();
360 pending.insert(
361 42,
362 PendingCall {
363 method: "Test.method".into(),
364 tx,
365 },
366 );
367 let (event_tx, _) = broadcast::channel::<CdpEvent>(256);
368
369 let rt = tokio::runtime::Runtime::new().unwrap();
370 rt.block_on(Connection::dispatch_message(
371 json!({"id": 42, "error": {"code": -32000, "message": "Cannot find context"}}),
372 &mut pending,
373 &event_tx,
374 ));
375
376 assert!(pending.is_empty(), "pending call should be removed");
377 let received = rx.try_recv().expect("oneshot should have been sent");
378 assert!(received.is_err(), "error response should be Err");
379 }
380
381 #[test]
382 fn test_dispatch_message_unknown_id_ignored() {
383 let mut pending: PendingMap = HashMap::new();
384 let (event_tx, _) = broadcast::channel::<CdpEvent>(256);
385
386 let rt = tokio::runtime::Runtime::new().unwrap();
388 rt.block_on(Connection::dispatch_message(
389 json!({"id": 999, "result": {}}),
390 &mut pending,
391 &event_tx,
392 ));
393
394 assert!(pending.is_empty(), "pending should remain empty");
395 }
396
397 #[test]
398 fn test_dispatch_message_broadcasts_event_with_session_id() {
399 let mut pending: PendingMap = HashMap::new();
400 let (event_tx, mut event_rx) = broadcast::channel::<CdpEvent>(256);
401
402 let rt = tokio::runtime::Runtime::new().unwrap();
403 rt.block_on(Connection::dispatch_message(
404 json!({"method": "Runtime.consoleAPICalled", "params": {"level": "info"}, "sessionId": "sess-1"}),
405 &mut pending,
406 &event_tx,
407 ));
408
409 assert!(pending.is_empty());
410 let evt = event_rx.try_recv().expect("event should be broadcast");
411 assert_eq!(evt.method, "Runtime.consoleAPICalled");
412 assert_eq!(
413 evt.params.get("level").and_then(|v| v.as_str()),
414 Some("info")
415 );
416 assert_eq!(evt.session_id.as_deref(), Some("sess-1"));
417 }
418
419 #[test]
420 fn test_dispatch_message_unrecognized_message_is_noop() {
421 let mut pending: PendingMap = HashMap::new();
422 let (event_tx, mut event_rx) = broadcast::channel::<CdpEvent>(256);
423
424 let rt = tokio::runtime::Runtime::new().unwrap();
425 rt.block_on(Connection::dispatch_message(
426 json!({"some": "garbage"}),
427 &mut pending,
428 &event_tx,
429 ));
430
431 assert!(pending.is_empty());
432 match event_rx.try_recv() {
433 Err(broadcast::error::TryRecvError::Empty) => {} other => panic!("expected Empty, got {other:?}"),
435 }
436 }
437}