1use std::sync::mpsc;
2use std::sync::mpsc::{RecvTimeoutError, TryRecvError};
3use std::sync::Arc;
4use std::sync::Mutex;
5use std::time::Duration;
6
7use anyhow::{anyhow, Result};
8use log::{debug, error, info, trace};
9
10use process::Process;
11pub use process::{LaunchOptions, LaunchOptionsBuilder, DEFAULT_ARGS};
12pub use tab::Tab;
13pub use transport::ConnectionClosed;
14use transport::Transport;
15use url::Url;
16use which::which;
17
18use crate::protocol::cdp::{
19 self, types::Event, types::Method, Browser as B, Target, Target::GetTargets,
20};
21
22use crate::browser::context::Context;
23use crate::util;
24use Target::{CreateTarget, SetDiscoverTargets};
25use B::GetVersion;
26pub use B::GetVersionReturnObject;
27
28#[cfg(feature = "fetch")]
29pub use fetcher::FetcherOptions;
30
31#[cfg(feature = "fetch")]
32pub use fetcher::Revision;
33
34pub mod context;
35#[cfg(feature = "fetch")]
36mod fetcher;
37mod process;
38pub mod tab;
39pub mod transport;
40
41#[derive(Clone)]
79pub struct Browser {
80 inner: Arc<BrowserInner>,
81}
82
83pub struct BrowserInner {
84 process: Option<Process>,
85 transport: Arc<Transport>,
86 tabs: Arc<Mutex<Vec<Arc<Tab>>>>,
87 loop_shutdown_tx: mpsc::SyncSender<()>,
88 close_on_drop: bool,
89}
90
91impl Browser {
92 pub fn new(launch_options: LaunchOptions) -> Result<Self> {
97 let idle_browser_timeout = launch_options.idle_browser_timeout;
98 let process = Process::new(launch_options)?;
99 let process_id = process.get_id();
100
101 let transport = Arc::new(Transport::new(
102 process.debug_ws_url.clone(),
103 Some(process_id),
104 idle_browser_timeout,
105 )?);
106
107 Self::create_browser(Some(process), transport, idle_browser_timeout, true)
108 }
109
110 pub fn default() -> Result<Self> {
113 let launch_options = LaunchOptions::default_builder()
114 .path(Some(default_executable().map_err(|e| anyhow!(e))?))
115 .build()?;
116 Self::new(launch_options)
117 }
118
119 pub fn connect(debug_ws_url: String) -> Result<Self> {
122 Self::connect_with_timeout(debug_ws_url, Duration::from_secs(30))
123 }
124
125 pub fn connect_with_timeout(
128 debug_ws_url: String,
129 idle_browser_timeout: Duration,
130 ) -> Result<Self> {
131 let url = Url::parse(&debug_ws_url)?;
132
133 let transport = Arc::new(Transport::new(url, None, idle_browser_timeout)?);
134 trace!("created transport");
135
136 Self::create_browser(None, transport, idle_browser_timeout, false)
137 }
138
139 fn create_browser(
140 process: Option<Process>,
141 transport: Arc<Transport>,
142 idle_browser_timeout: Duration,
143 close_on_drop: bool,
144 ) -> Result<Self> {
145 let tabs = Arc::new(Mutex::new(Vec::with_capacity(1)));
146
147 let (shutdown_tx, shutdown_rx) = mpsc::sync_channel(100);
148
149 let browser = Browser {
150 inner: Arc::new(BrowserInner {
151 process,
152 tabs,
153 transport,
154 loop_shutdown_tx: shutdown_tx,
155 close_on_drop,
156 }),
157 };
158
159 let incoming_events_rx = browser.inner.transport.listen_to_browser_events();
160
161 browser.handle_browser_level_events(
162 incoming_events_rx,
163 browser.get_process_id(),
164 shutdown_rx,
165 idle_browser_timeout,
166 );
167 trace!("created browser event listener");
168
169 trace!("Calling set discover");
171 browser.call_method(SetDiscoverTargets {
172 discover: true,
173 filter: None,
174 })?;
175
176 Ok(browser)
177 }
178
179 pub fn get_process_id(&self) -> Option<u32> {
180 self.inner.process.as_ref().map(process::Process::get_id)
181 }
182
183 pub fn get_ws_url(&self) -> String {
184 match &self.inner.process {
185 None => "browser is not running".to_string(),
186 Some(process) => process.debug_ws_url.clone().to_string(),
187 }
188 }
189
190 pub fn get_tabs(&self) -> &Arc<Mutex<Vec<Arc<Tab>>>> {
193 &self.inner.tabs
194 }
195
196 #[deprecated(since = "1.0.4", note = "Use new_tab() instead.")]
204 pub fn wait_for_initial_tab(&self) -> Result<Arc<Tab>> {
205 match util::Wait::with_timeout(Duration::from_secs(10))
206 .until(|| self.inner.tabs.lock().unwrap().first().cloned())
207 {
208 Ok(tab) => Ok(tab),
209 Err(_) => self.new_tab(),
210 }
211 }
212
213 pub fn new_tab(&self) -> Result<Arc<Tab>> {
232 let default_blank_tab = CreateTarget {
233 url: "about:blank".to_string(),
234 width: None,
235 height: None,
236 browser_context_id: None,
237 enable_begin_frame_control: None,
238 new_window: None,
239 background: None,
240 for_tab: None,
241 };
242 self.new_tab_with_options(default_blank_tab)
243 }
244
245 pub fn new_tab_with_options(&self, create_target_params: CreateTarget) -> Result<Arc<Tab>> {
264 let target_id = self.call_method(create_target_params)?.target_id;
265
266 util::Wait::with_timeout(Duration::from_secs(20))
267 .until(|| {
268 let tabs = self.inner.tabs.lock().unwrap();
269 tabs.iter().find_map(|tab| {
270 if *tab.get_target_id() == target_id {
271 Some(tab.clone())
272 } else {
273 None
274 }
275 })
276 })
277 .map_err(Into::into)
278 }
279
280 pub fn new_context(&self) -> Result<context::Context> {
282 debug!("Creating new browser context");
283 let context_id = self
284 .call_method(Target::CreateBrowserContext {
285 dispose_on_detach: None,
286 proxy_server: None,
287 proxy_bypass_list: None,
288 origins_with_universal_network_access: None,
289 })?
290 .browser_context_id;
291 debug!("Created new browser context: {:?}", context_id);
292 Ok(Context::new(self, context_id))
293 }
294
295 pub fn register_missing_tabs(&self) {
297 let targets = self.call_method(GetTargets { filter: None });
298
299 let mut tabs_lock = self.inner.tabs.lock().unwrap();
300 let mut previous_target_id: String = String::default();
301 for target in targets.unwrap().target_infos {
302 let target_id = target.target_id.clone();
303
304 if tabs_lock
305 .iter()
306 .any(|t| t.get_target_id().clone() == target_id || !target.attached)
307 {
308 previous_target_id = target.target_id;
309 continue;
310 }
311
312 let tab = Tab::new(target, self.inner.transport.clone());
313 if let Ok(tab) = tab {
314 if let Some(index) = tabs_lock
315 .iter()
316 .position(|x| x.get_target_id().clone() == previous_target_id)
317 {
318 tabs_lock.insert(index, Arc::new(tab));
319 } else {
320 tabs_lock.push(Arc::new(tab));
321 }
322 }
323
324 previous_target_id = target_id;
325 }
326 }
327
328 pub fn get_version(&self) -> Result<GetVersionReturnObject> {
343 self.call_method(GetVersion(None))
344 }
345
346 fn handle_browser_level_events(
347 &self,
348 events_rx: mpsc::Receiver<Event>,
349 process_id: Option<u32>,
350 shutdown_rx: mpsc::Receiver<()>,
351 idle_browser_timeout: Duration,
352 ) {
353 let tabs = Arc::clone(&self.inner.tabs);
354 let transport = Arc::clone(&self.inner.transport);
355
356 std::thread::spawn(move || {
357 trace!("Starting browser's event handling loop");
358 loop {
359 match shutdown_rx.try_recv() {
360 Ok(()) | Err(TryRecvError::Disconnected) => {
361 info!("Browser event loop received shutdown message");
362 break;
363 }
364 Err(TryRecvError::Empty) => {}
365 }
366
367 match events_rx.recv_timeout(idle_browser_timeout) {
368 Err(recv_timeout_error) => {
369 match recv_timeout_error {
370 RecvTimeoutError::Timeout => {
371 error!(
372 "Got a timeout while listening for browser events (Chrome #{:?})",
373 process_id
374 );
375 }
376 RecvTimeoutError::Disconnected => {
377 debug!(
378 "Browser event sender disconnected while loop was waiting (Chrome #{:?})",
379 process_id
380 );
381 }
382 }
383 break;
384 }
385 Ok(event) => {
386 match event {
387 Event::TargetCreated(ev) => {
388 let target_info = ev.params.target_info;
389 trace!("Creating target: {:?}", target_info);
390 if target_info.Type == "page" {
394 match Tab::new(target_info, Arc::clone(&transport)) {
395 Ok(new_tab) => {
396 tabs.lock().unwrap().push(Arc::new(new_tab));
397 }
398 Err(_tab_creation_err) => {
399 info!("Failed to create a handle to new tab");
400 break;
401 }
402 }
403 }
404 }
405 Event::TargetInfoChanged(ev) => {
406 let target_info = ev.params.target_info;
407 trace!("Target info changed: {:?}", target_info);
408 if target_info.Type == "page"
409 && !target_info.url.starts_with("devtools://")
410 {
411 let locked_tabs = tabs.lock().unwrap();
412 let updated_tab = locked_tabs
413 .iter()
414 .find(|tab| *tab.get_target_id() == target_info.target_id)
415 .expect("got TargetInfoChanged event about a tab not in our list");
416 updated_tab.update_target_info(target_info);
417 }
418 }
419 Event::AttachedToTarget(ev) => {
420 let target_info = ev.params.target_info;
421 trace!("Attached To Target : {:?}", target_info);
422 }
425 Event::TargetDestroyed(ev) => {
426 trace!("Target destroyed: {:?}", ev.params.target_id);
427 let mut locked_tabs = tabs.lock().unwrap();
428 let pos = locked_tabs
429 .iter()
430 .position(|tab| *tab.get_target_id() == ev.params.target_id);
431
432 if let Some(idx) = pos {
433 locked_tabs.remove(idx);
434 }
435 }
436 _ => {
437 let raw_event = format!("{event:?}");
438 trace!(
439 "Unhandled event: {}",
440 raw_event.chars().take(50).collect::<String>()
441 );
442 }
443 }
444 }
445 }
446 }
447 info!("Finished browser's event handling loop");
448 });
449 }
450
451 fn call_method<C>(&self, method: C) -> Result<C::ReturnObject>
455 where
456 C: Method + serde::Serialize,
457 {
458 self.inner.transport.call_method_on_browser(method)
459 }
460
461 #[allow(dead_code)]
462 #[cfg(test)]
463 pub(crate) fn process(&self) -> Option<&Process> {
464 #[allow(clippy::used_underscore_binding)]
465 self.inner.process.as_ref()
466 }
467}
468
469impl Drop for BrowserInner {
472 fn drop(&mut self) {
473 info!("Dropping browser");
474 if self.close_on_drop {
475 self.transport
476 .call_method_on_browser(cdp::Browser::Close(None))
477 .ok();
478 }
479 self.loop_shutdown_tx.send(()).ok();
480 self.transport.shutdown();
481 }
482}
483
484pub fn default_executable() -> Result<std::path::PathBuf, String> {
493 if let Ok(path) = std::env::var("CHROME") {
494 if std::path::Path::new(&path).exists() {
495 return Ok(path.into());
496 }
497 }
498
499 for app in &[
500 "google-chrome-stable",
501 "google-chrome-beta",
502 "google-chrome-dev",
503 "google-chrome-unstable",
504 "chromium",
505 "chromium-browser",
506 "microsoft-edge-stable",
507 "microsoft-edge-beta",
508 "microsoft-edge-dev",
509 "chrome",
510 "chrome-browser",
511 "msedge",
512 "microsoft-edge",
513 ] {
514 if let Ok(path) = which(app) {
515 return Ok(path);
516 }
517 }
518
519 #[cfg(target_os = "macos")]
520 {
521 for path in &[
522 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
523 "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
524 "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
525 "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
526 "/Applications/Chromium.app/Contents/MacOS/Chromium",
527 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
528 "/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta",
529 "/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev",
530 "/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary",
531 ][..]
532 {
533 if std::path::Path::new(path).exists() {
534 return Ok(path.into());
535 }
536 }
537 }
538
539 #[cfg(windows)]
540 {
541 use crate::browser::process::get_chrome_path_from_registry;
542
543 if let Some(path) = get_chrome_path_from_registry() {
544 if path.exists() {
545 return Ok(path);
546 }
547 }
548
549 for path in &[r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"][..] {
550 if std::path::Path::new(path).exists() {
551 return Ok(path.into());
552 }
553 }
554 }
555
556 Err("Could not auto detect a chrome executable".to_string())
557}
558
559#[cfg(test)]
560mod test {
561 use super::Browser;
562
563 fn is_sync<T>()
564 where
565 T: Sync,
566 {
567 }
568
569 #[test]
570 fn test_if_browser_is_sync() {
571 is_sync::<Browser>();
572 }
573}