browser_automation_cli/native/cdp/
client.rs1#![allow(missing_docs)]
3use std::borrow::Cow;
9use std::sync::Arc;
10
11use chromiumoxide::browser::Browser;
12use chromiumoxide::cdp::browser_protocol::fetch::EventRequestPaused;
13use chromiumoxide::cdp::browser_protocol::network::{
14 EventLoadingFailed, EventLoadingFinished, EventRequestWillBeSent,
15};
16use chromiumoxide::cdp::browser_protocol::page::{
17 EventDomContentEventFired, EventJavascriptDialogOpening, EventLoadEventFired,
18 EventScreencastFrame,
19};
20use chromiumoxide::cdp::browser_protocol::tracing::{EventDataCollected, EventTracingComplete};
21use chromiumoxide::cdp::js_protocol::heap_profiler::{
22 EventAddHeapSnapshotChunk, EventReportHeapSnapshotProgress,
23};
24use chromiumoxide::cdp::js_protocol::runtime::EventConsoleApiCalled;
25use chromiumoxide::error::CdpError;
26use chromiumoxide::page::Page;
27use chromiumoxide::types::{Command, Method, MethodId};
28use chromiumoxide::Handler;
29use futures::StreamExt;
30use serde::Serialize;
31use serde_json::Value;
32use tokio::sync::{broadcast, Mutex};
33use tokio::task::JoinHandle;
34
35use super::types::CdpEvent;
36
37#[derive(Debug, Clone)]
39struct RawCdpCommand {
40 method: String,
41 params: Value,
42}
43
44impl Serialize for RawCdpCommand {
45 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46 where
47 S: serde::Serializer,
48 {
49 match &self.params {
50 Value::Null => {
51 use serde::ser::SerializeMap;
52 let map = serializer.serialize_map(Some(0))?;
53 map.end()
54 }
55 other => other.serialize(serializer),
56 }
57 }
58}
59
60impl Method for RawCdpCommand {
61 fn identifier(&self) -> MethodId {
62 Cow::Owned(self.method.clone())
63 }
64}
65
66impl Command for RawCdpCommand {
67 type Response = Value;
68}
69
70pub struct CdpClient {
71 browser: Arc<Mutex<Browser>>,
72 event_tx: broadcast::Sender<CdpEvent>,
73 _handler: JoinHandle<()>,
74 _event_forwarders: Vec<JoinHandle<()>>,
75}
76
77impl CdpClient {
78 pub async fn from_browser(browser: Browser, mut handler: Handler) -> Result<Self, String> {
80 let handler_task = tokio::spawn(async move {
81 while let Some(h) = handler.next().await {
82 if h.is_err() {
83 break;
84 }
85 }
86 });
87
88 let (event_tx, _) = broadcast::channel(4096);
89
90 let browser = Arc::new(Mutex::new(browser));
91 let event_forwarders = spawn_event_forwarders(browser.clone(), event_tx.clone()).await?;
92
93 Ok(Self {
94 browser,
95 event_tx,
96 _handler: handler_task,
97 _event_forwarders: event_forwarders,
98 })
99 }
100
101 pub async fn connect(url: &str) -> Result<Self, String> {
103 Self::connect_with_headers(url, None).await
104 }
105
106 pub async fn connect_with_headers(
108 url: &str,
109 _headers: Option<Vec<(String, String)>>,
110 ) -> Result<Self, String> {
111 let (browser, handler) = Browser::connect(url)
112 .await
113 .map_err(|e| format!("CDP Browser::connect failed: {e}"))?;
114 Self::from_browser(browser, handler).await
115 }
116
117 pub fn browser(&self) -> Arc<Mutex<Browser>> {
119 self.browser.clone()
120 }
121
122 pub async fn send_command(
123 &self,
124 method: &str,
125 params: Option<Value>,
126 session_id: Option<&str>,
127 ) -> Result<Value, String> {
128 let cmd = RawCdpCommand {
129 method: method.to_string(),
130 params: params.unwrap_or(Value::Null),
131 };
132
133 let result = if let Some(sid) = session_id.filter(|s| !s.is_empty()) {
134 let page = self.page_for_session(sid).await?;
135 page.execute(cmd)
136 .await
137 .map_err(|e| format_cdp_err(method, e))?
138 } else {
139 let browser = self.browser.lock().await;
140 browser
141 .execute(cmd)
142 .await
143 .map_err(|e| format_cdp_err(method, e))?
144 };
145
146 Ok(result.result)
147 }
148
149 pub fn subscribe(&self) -> broadcast::Receiver<CdpEvent> {
150 self.event_tx.subscribe()
151 }
152
153 pub async fn send_command_typed<P: serde::Serialize, R: serde::de::DeserializeOwned>(
154 &self,
155 method: &str,
156 params: &P,
157 session_id: Option<&str>,
158 ) -> Result<R, String> {
159 let params_value = serde_json::to_value(params)
160 .map_err(|e| format!("Failed to serialize params: {}", e))?;
161 let result = self
162 .send_command(method, Some(params_value), session_id)
163 .await?;
164 serde_json::from_value(result)
165 .map_err(|e| format!("Failed to deserialize CDP response for {}: {}", method, e))
166 }
167
168 pub async fn send_command_no_params(
169 &self,
170 method: &str,
171 session_id: Option<&str>,
172 ) -> Result<Value, String> {
173 self.send_command(method, None, session_id).await
174 }
175
176 pub async fn send_command_no_wait(
178 &self,
179 method: &str,
180 params: Option<Value>,
181 session_id: Option<&str>,
182 ) -> Result<(), String> {
183 let _ = self.send_command(method, params, session_id).await;
184 Ok(())
185 }
186
187 async fn page_for_session(&self, session_id: &str) -> Result<Page, String> {
188 let browser = self.browser.lock().await;
189 let pages = browser
190 .pages()
191 .await
192 .map_err(|e| format!("Browser::pages failed: {e}"))?;
193 for page in pages {
194 if page.session_id().as_ref() == session_id {
195 return Ok(page);
196 }
197 }
198 let pages = browser
200 .pages()
201 .await
202 .map_err(|e| format!("Browser::pages failed: {e}"))?;
203 if pages.len() == 1 {
204 return Ok(pages.into_iter().next().unwrap());
205 }
206 Err(format!(
207 "No chromiumoxide Page for session_id={session_id} (pages={})",
208 pages.len()
209 ))
210 }
211}
212
213fn format_cdp_err(method: &str, e: CdpError) -> String {
214 format!("CDP error ({method}): {e}")
215}
216
217async fn spawn_event_forwarders(
218 browser: Arc<Mutex<Browser>>,
219 event_tx: broadcast::Sender<CdpEvent>,
220) -> Result<Vec<JoinHandle<()>>, String> {
221 let mut handles = Vec::new();
222 let b = browser.lock().await;
223
224 macro_rules! fwd {
225 ($ty:ty, $method:expr) => {{
226 let mut stream = b
227 .event_listener::<$ty>()
228 .await
229 .map_err(|e| format!("event_listener {}: {e}", $method))?;
230 let tx = event_tx.clone();
231 handles.push(tokio::spawn(async move {
232 while let Some(ev) = stream.next().await {
233 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
234 let _ = tx.send(CdpEvent {
235 method: $method.to_string(),
236 params,
237 session_id: None,
239 });
240 }
241 }));
242 }};
243 }
244
245 fwd!(EventLoadEventFired, "Page.loadEventFired");
246 fwd!(EventDomContentEventFired, "Page.domContentEventFired");
247 fwd!(EventRequestWillBeSent, "Network.requestWillBeSent");
248 fwd!(EventLoadingFinished, "Network.loadingFinished");
249 fwd!(EventLoadingFailed, "Network.loadingFailed");
250 fwd!(EventRequestPaused, "Fetch.requestPaused");
251 fwd!(EventJavascriptDialogOpening, "Page.javascriptDialogOpening");
252 fwd!(EventConsoleApiCalled, "Runtime.consoleAPICalled");
254 fwd!(
256 EventAddHeapSnapshotChunk,
257 "HeapProfiler.addHeapSnapshotChunk"
258 );
259 fwd!(
260 EventReportHeapSnapshotProgress,
261 "HeapProfiler.reportHeapSnapshotProgress"
262 );
263 fwd!(EventDataCollected, "Tracing.dataCollected");
264 fwd!(EventTracingComplete, "Tracing.tracingComplete");
265 fwd!(EventScreencastFrame, "Page.screencastFrame");
266
267 drop(b);
268 Ok(handles)
269}
270
271impl CdpClient {
272 pub async fn attach_page_console_forwarders(&self) -> Result<(), String> {
275 self.attach_page_event_forwarders_console().await
276 }
277
278 pub async fn attach_page_network_forwarders(&self) -> Result<(), String> {
280 let browser = self.browser.lock().await;
281 let pages = browser
282 .pages()
283 .await
284 .map_err(|e| format!("Browser::pages for network listeners: {e}"))?;
285 for page in pages {
286 let mut stream = page
287 .event_listener::<EventRequestWillBeSent>()
288 .await
289 .map_err(|e| format!("page EventRequestWillBeSent listener: {e}"))?;
290 let tx = self.event_tx.clone();
291 tokio::spawn(async move {
292 while let Some(ev) = stream.next().await {
293 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
294 let _ = tx.send(CdpEvent {
295 method: "Network.requestWillBeSent".to_string(),
296 params,
297 session_id: None,
298 });
299 }
300 });
301 }
302 Ok(())
303 }
304
305 async fn attach_page_event_forwarders_console(&self) -> Result<(), String> {
306 let browser = self.browser.lock().await;
307 let pages = browser
308 .pages()
309 .await
310 .map_err(|e| format!("Browser::pages for console listeners: {e}"))?;
311 for page in pages {
312 let mut stream = page
313 .event_listener::<EventConsoleApiCalled>()
314 .await
315 .map_err(|e| format!("page EventConsoleApiCalled listener: {e}"))?;
316 let tx = self.event_tx.clone();
317 tokio::spawn(async move {
318 while let Some(ev) = stream.next().await {
319 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
320 let _ = tx.send(CdpEvent {
321 method: "Runtime.consoleAPICalled".to_string(),
322 params,
323 session_id: None,
324 });
325 }
326 });
327 }
328 Ok(())
329 }
330
331 pub async fn attach_page_session_forwarders(&self) -> Result<(), String> {
334 let browser = self.browser.lock().await;
335 let pages = browser
336 .pages()
337 .await
338 .map_err(|e| format!("Browser::pages for session listeners: {e}"))?;
339 for page in pages {
340 {
342 let mut stream = page
343 .event_listener::<EventAddHeapSnapshotChunk>()
344 .await
345 .map_err(|e| format!("page EventAddHeapSnapshotChunk: {e}"))?;
346 let tx = self.event_tx.clone();
347 tokio::spawn(async move {
348 while let Some(ev) = stream.next().await {
349 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
350 let _ = tx.send(CdpEvent {
351 method: "HeapProfiler.addHeapSnapshotChunk".to_string(),
352 params,
353 session_id: None,
354 });
355 }
356 });
357 }
358 {
360 let mut stream = page
361 .event_listener::<EventReportHeapSnapshotProgress>()
362 .await
363 .map_err(|e| format!("page EventReportHeapSnapshotProgress: {e}"))?;
364 let tx = self.event_tx.clone();
365 tokio::spawn(async move {
366 while let Some(ev) = stream.next().await {
367 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
368 let _ = tx.send(CdpEvent {
369 method: "HeapProfiler.reportHeapSnapshotProgress".to_string(),
370 params,
371 session_id: None,
372 });
373 }
374 });
375 }
376 {
378 let mut stream = page
379 .event_listener::<EventScreencastFrame>()
380 .await
381 .map_err(|e| format!("page EventScreencastFrame: {e}"))?;
382 let tx = self.event_tx.clone();
383 tokio::spawn(async move {
384 while let Some(ev) = stream.next().await {
385 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
386 let _ = tx.send(CdpEvent {
387 method: "Page.screencastFrame".to_string(),
388 params,
389 session_id: None,
390 });
391 }
392 });
393 }
394 {
396 let mut stream = page
397 .event_listener::<EventJavascriptDialogOpening>()
398 .await
399 .map_err(|e| format!("page EventJavascriptDialogOpening: {e}"))?;
400 let tx = self.event_tx.clone();
401 tokio::spawn(async move {
402 while let Some(ev) = stream.next().await {
403 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
404 let _ = tx.send(CdpEvent {
405 method: "Page.javascriptDialogOpening".to_string(),
406 params,
407 session_id: None,
408 });
409 }
410 });
411 }
412 }
413 Ok(())
414 }
415}