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 let n = pages.len();
204 if n == 1 {
205 if let Some(page) = pages.into_iter().next() {
206 return Ok(page);
207 }
208 }
209 Err(format!(
210 "No chromiumoxide Page for session_id={session_id} (pages={n})"
211 ))
212 }
213}
214
215fn format_cdp_err(method: &str, e: CdpError) -> String {
216 format!("CDP error ({method}): {e}")
217}
218
219async fn spawn_event_forwarders(
220 browser: Arc<Mutex<Browser>>,
221 event_tx: broadcast::Sender<CdpEvent>,
222) -> Result<Vec<JoinHandle<()>>, String> {
223 let mut handles = Vec::new();
224 let b = browser.lock().await;
225
226 macro_rules! fwd {
227 ($ty:ty, $method:expr) => {{
228 let mut stream = b
229 .event_listener::<$ty>()
230 .await
231 .map_err(|e| format!("event_listener {}: {e}", $method))?;
232 let tx = event_tx.clone();
233 handles.push(tokio::spawn(async move {
234 while let Some(ev) = stream.next().await {
235 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
236 let _ = tx.send(CdpEvent {
237 method: $method.to_string(),
238 params,
239 session_id: None,
241 });
242 }
243 }));
244 }};
245 }
246
247 fwd!(EventLoadEventFired, "Page.loadEventFired");
248 fwd!(EventDomContentEventFired, "Page.domContentEventFired");
249 fwd!(EventRequestWillBeSent, "Network.requestWillBeSent");
250 fwd!(EventLoadingFinished, "Network.loadingFinished");
251 fwd!(EventLoadingFailed, "Network.loadingFailed");
252 fwd!(EventRequestPaused, "Fetch.requestPaused");
253 fwd!(EventJavascriptDialogOpening, "Page.javascriptDialogOpening");
254 fwd!(EventConsoleApiCalled, "Runtime.consoleAPICalled");
256 fwd!(
258 EventAddHeapSnapshotChunk,
259 "HeapProfiler.addHeapSnapshotChunk"
260 );
261 fwd!(
262 EventReportHeapSnapshotProgress,
263 "HeapProfiler.reportHeapSnapshotProgress"
264 );
265 fwd!(EventDataCollected, "Tracing.dataCollected");
266 fwd!(EventTracingComplete, "Tracing.tracingComplete");
267 fwd!(EventScreencastFrame, "Page.screencastFrame");
268
269 drop(b);
270 Ok(handles)
271}
272
273impl CdpClient {
274 pub async fn attach_page_console_forwarders(&self) -> Result<(), String> {
277 self.attach_page_event_forwarders_console().await
278 }
279
280 pub async fn attach_page_network_forwarders(&self) -> Result<(), String> {
282 let browser = self.browser.lock().await;
283 let pages = browser
284 .pages()
285 .await
286 .map_err(|e| format!("Browser::pages for network listeners: {e}"))?;
287 for page in pages {
288 let mut stream = page
289 .event_listener::<EventRequestWillBeSent>()
290 .await
291 .map_err(|e| format!("page EventRequestWillBeSent listener: {e}"))?;
292 let tx = self.event_tx.clone();
293 tokio::spawn(async move {
294 while let Some(ev) = stream.next().await {
295 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
296 let _ = tx.send(CdpEvent {
297 method: "Network.requestWillBeSent".to_string(),
298 params,
299 session_id: None,
300 });
301 }
302 });
303 }
304 Ok(())
305 }
306
307 async fn attach_page_event_forwarders_console(&self) -> Result<(), String> {
308 let browser = self.browser.lock().await;
309 let pages = browser
310 .pages()
311 .await
312 .map_err(|e| format!("Browser::pages for console listeners: {e}"))?;
313 for page in pages {
314 let mut stream = page
315 .event_listener::<EventConsoleApiCalled>()
316 .await
317 .map_err(|e| format!("page EventConsoleApiCalled listener: {e}"))?;
318 let tx = self.event_tx.clone();
319 tokio::spawn(async move {
320 while let Some(ev) = stream.next().await {
321 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
322 let _ = tx.send(CdpEvent {
323 method: "Runtime.consoleAPICalled".to_string(),
324 params,
325 session_id: None,
326 });
327 }
328 });
329 }
330 Ok(())
331 }
332
333 pub async fn attach_page_session_forwarders(&self) -> Result<(), String> {
336 let browser = self.browser.lock().await;
337 let pages = browser
338 .pages()
339 .await
340 .map_err(|e| format!("Browser::pages for session listeners: {e}"))?;
341 for page in pages {
342 {
344 let mut stream = page
345 .event_listener::<EventAddHeapSnapshotChunk>()
346 .await
347 .map_err(|e| format!("page EventAddHeapSnapshotChunk: {e}"))?;
348 let tx = self.event_tx.clone();
349 tokio::spawn(async move {
350 while let Some(ev) = stream.next().await {
351 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
352 let _ = tx.send(CdpEvent {
353 method: "HeapProfiler.addHeapSnapshotChunk".to_string(),
354 params,
355 session_id: None,
356 });
357 }
358 });
359 }
360 {
362 let mut stream = page
363 .event_listener::<EventReportHeapSnapshotProgress>()
364 .await
365 .map_err(|e| format!("page EventReportHeapSnapshotProgress: {e}"))?;
366 let tx = self.event_tx.clone();
367 tokio::spawn(async move {
368 while let Some(ev) = stream.next().await {
369 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
370 let _ = tx.send(CdpEvent {
371 method: "HeapProfiler.reportHeapSnapshotProgress".to_string(),
372 params,
373 session_id: None,
374 });
375 }
376 });
377 }
378 {
380 let mut stream = page
381 .event_listener::<EventScreencastFrame>()
382 .await
383 .map_err(|e| format!("page EventScreencastFrame: {e}"))?;
384 let tx = self.event_tx.clone();
385 tokio::spawn(async move {
386 while let Some(ev) = stream.next().await {
387 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
388 let _ = tx.send(CdpEvent {
389 method: "Page.screencastFrame".to_string(),
390 params,
391 session_id: None,
392 });
393 }
394 });
395 }
396 {
398 let mut stream = page
399 .event_listener::<EventJavascriptDialogOpening>()
400 .await
401 .map_err(|e| format!("page EventJavascriptDialogOpening: {e}"))?;
402 let tx = self.event_tx.clone();
403 tokio::spawn(async move {
404 while let Some(ev) = stream.next().await {
405 let params = serde_json::to_value(ev.as_ref()).unwrap_or(Value::Null);
406 let _ = tx.send(CdpEvent {
407 method: "Page.javascriptDialogOpening".to_string(),
408 params,
409 session_id: None,
410 });
411 }
412 });
413 }
414 }
415 Ok(())
416 }
417}