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;
14use tracing;
15
16static NEXT_CDP_ID: AtomicU64 = AtomicU64::new(1);
17
18#[derive(Debug, Clone)]
20pub struct CdpEvent {
21 pub method: String,
22 pub params: Value,
23 pub session_id: Option<String>,
24}
25
26pub(crate) struct PendingCall {
28 method: String,
29 tx: oneshot::Sender<Result<Value>>,
30}
31
32pub(crate) type PendingMap = HashMap<u64, PendingCall>;
33
34pub(crate) enum InternalMessage {
36 Call {
37 id: u64,
38 method: String,
39 params: Value,
40 session_id: Option<String>,
41 tx: oneshot::Sender<Result<Value>>,
42 },
43}
44
45pub struct Connection {
50 write: mpsc::UnboundedSender<InternalMessage>,
51 events: broadcast::Sender<CdpEvent>,
52 #[allow(dead_code)]
53 handle: JoinHandle<()>,
54}
55
56impl std::fmt::Debug for Connection {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("Connection").finish_non_exhaustive()
59 }
60}
61
62impl Connection {
63 pub async fn connect(ws_url: &str) -> Result<Self> {
65 let ws_url_owned = ws_url.to_owned();
67 let connect_fut = tokio::spawn(async move { connect_async(&ws_url_owned).await });
68
69 #[cfg(target_os = "macos")]
73 {
74 tokio::time::sleep(Duration::from_millis(600)).await;
75 dismiss_allow_debugging_dialog();
76 }
77
78 let (ws_stream, _) = connect_fut
80 .await
81 .map_err(|e| CdpError::ConnectionFailed {
82 detail: format!("task join: {e}"),
83 })?
84 .map_err(|e| CdpError::ConnectionFailed {
85 detail: format!("WebSocket connect to {ws_url} failed: {e}"),
86 })?;
87
88 let (write_tx, write_rx) = mpsc::unbounded_channel::<InternalMessage>();
89 let (events_tx, _) = broadcast::channel::<CdpEvent>(256);
90 let pending: PendingMap = HashMap::new();
91
92 let (ws_writer, ws_reader) = ws_stream.split();
93 let events_clone = events_tx.clone();
94
95 let handle = tokio::spawn(async move {
96 Self::run(write_rx, ws_writer, ws_reader, pending, events_clone).await;
97 });
98
99 Ok(Connection {
100 write: write_tx,
101 events: events_tx,
102 handle,
103 })
104 }
105
106 async fn run(
108 mut write_rx: mpsc::UnboundedReceiver<InternalMessage>,
109 mut ws_writer: futures_util::stream::SplitSink<
110 WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>,
111 Message,
112 >,
113 mut ws_reader: futures_util::stream::SplitStream<
114 WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>,
115 >,
116 mut pending: PendingMap,
117 events: broadcast::Sender<CdpEvent>,
118 ) {
119 loop {
120 tokio::select! {
121 msg = write_rx.recv() => {
123 match msg {
124 Some(InternalMessage::Call { id, method, params, session_id, tx }) => {
125 let cmd = if let Some(ref sid) = session_id {
126 serde_json::json!({
127 "id": id,
128 "method": method,
129 "params": params,
130 "sessionId": sid,
131 })
132 } else {
133 serde_json::json!({
134 "id": id,
135 "method": method,
136 "params": params,
137 })
138 };
139 let text = match serde_json::to_string(&cmd) {
140 Ok(t) => t,
141 Err(e) => {
142 tracing::warn!("Failed to serialize CDP command: {e}");
143 let _ = tx.send(Err(CdpError::Json(e)));
144 continue;
145 }
146 };
147 pending.insert(id, PendingCall { method, tx });
149 if let Err(e) = ws_writer.send(Message::Text(text)).await {
150 tracing::warn!("WS send error: {e}");
151 if let Some(pc) = pending.remove(&id) {
152 let _ = pc.tx.send(Err(CdpError::ConnectionFailed {
153 detail: format!("WebSocket send failed: {e}"),
154 }));
155 }
156 break;
157 }
158 }
159 None => break,
160 }
161 }
162 msg = ws_reader.next() => {
164 match msg {
165 Some(Ok(Message::Text(text))) => {
166 if let Ok(value) = serde_json::from_str::<Value>(&text) {
167 Self::dispatch_message(value, &mut pending, &events).await;
168 }
169 }
170 Some(Ok(Message::Close(frame))) => {
171 tracing::debug!("CDP WebSocket closed: {frame:?}");
172 break;
173 }
174 Some(Ok(Message::Binary(_))) => {
175 }
177 Some(Err(e)) => {
178 tracing::warn!("CDP WebSocket read error: {e}");
179 break;
180 }
181 None => {
182 tracing::debug!("CDP WebSocket stream ended");
183 break;
184 }
185 _ => {}
186 }
187 }
188 }
189 }
190
191 for (_, pc) in pending.drain() {
193 let _ = pc.tx.send(Err(CdpError::ConnectionFailed {
194 detail: "WebSocket connection closed".into(),
195 }));
196 }
197 }
198
199 async fn dispatch_message(
201 value: Value,
202 pending: &mut PendingMap,
203 events: &broadcast::Sender<CdpEvent>,
204 ) {
205 if let Some(id) = value.get("id").and_then(|v| v.as_u64()) {
206 if let Some(pc) = pending.remove(&id) {
208 let result = if let Some(err) = value.get("error") {
209 let detail = err
210 .get("message")
211 .and_then(|v| v.as_str())
212 .unwrap_or("unknown error")
213 .to_string();
214 Err(CdpError::CdpCallFailed {
215 method: pc.method,
216 detail,
217 })
218 } else {
219 Ok(value.get("result").cloned().unwrap_or(Value::Null))
220 };
221 let _ = pc.tx.send(result);
222 }
223 } else if let Some(method) = value.get("method").and_then(|v| v.as_str()) {
224 let session_id = value
226 .get("sessionId")
227 .and_then(|v| v.as_str())
228 .map(String::from);
229 let evt = CdpEvent {
230 method: method.to_string(),
231 params: value.get("params").cloned().unwrap_or(Value::Null),
232 session_id,
233 };
234 let _ = events.send(evt);
235 }
236 }
237
238 pub async fn call(
243 &self,
244 method: &str,
245 params: Value,
246 session_id: Option<&str>,
247 ) -> Result<Value> {
248 let id = NEXT_CDP_ID.fetch_add(1, Ordering::Relaxed);
249 let (tx, rx) = oneshot::channel();
250
251 let msg = InternalMessage::Call {
252 id,
253 method: method.to_string(),
254 params,
255 session_id: session_id.map(String::from),
256 tx,
257 };
258
259 self.write
260 .send(msg)
261 .map_err(|_| CdpError::ConnectionFailed {
262 detail: "background I/O task has terminated".into(),
263 })?;
264
265 tokio::time::timeout(Duration::from_secs(30), rx)
266 .await
267 .map_err(|_| CdpError::CdpCallFailed {
268 method: method.to_string(),
269 detail: "timeout waiting for response".into(),
270 })? .map_err(|_| CdpError::CdpCallFailed {
272 method: method.to_string(),
273 detail: "oneshot channel closed".into(),
274 })? }
276
277 pub fn event_rx(&self) -> broadcast::Receiver<CdpEvent> {
279 self.events.subscribe()
280 }
281
282 pub async fn close(self) {
285 let Self {
286 write,
287 events: _,
288 handle,
289 } = self;
290 drop(write); let _ = handle.await;
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use serde_json::json;
298
299 #[test]
300 fn test_cdp_response_parsing() {
301 let response = json!({"id": 1, "result": {"value": "hello"}});
302 assert_eq!(response["id"].as_u64(), Some(1));
303 assert!(response.get("error").is_none());
304 assert_eq!(response["result"]["value"].as_str(), Some("hello"));
305 }
306
307 #[test]
308 fn test_cdp_error_response() {
309 let response =
310 json!({"id": 2, "error": {"code": -32000, "message": "Cannot find context"}});
311 assert!(response.get("error").is_some());
312 assert_eq!(
313 response["error"]["message"].as_str(),
314 Some("Cannot find context")
315 );
316 }
317
318 #[test]
319 fn test_cdp_event_has_no_id() {
320 let event = json!({"method": "Page.frameStartedLoading", "params": {"frameId": "123"}});
321 assert!(event.get("id").is_none());
322 assert!(event.get("method").is_some());
323 assert!(event.get("params").is_some());
324 }
325
326 #[test]
327 fn test_cdp_event_with_session_id() {
328 let event = json!({
329 "method": "Runtime.consoleAPICalled",
330 "params": {},
331 "sessionId": "session-123"
332 });
333 assert!(event.get("id").is_none());
334 assert_eq!(event["sessionId"].as_str(), Some("session-123"));
335 }
336}