Skip to main content

chromiumoxide/
browser.rs

1use hashbrown::HashMap;
2use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
3use std::future::Future;
4use std::time::Duration;
5use std::{
6    io,
7    path::{Path, PathBuf},
8};
9
10use tokio::sync::mpsc::{channel, unbounded_channel, Sender};
11use tokio::sync::oneshot::channel as oneshot_channel;
12
13use crate::async_process::{self, Child, ExitStatus, Stdio};
14use crate::cmd::{to_command_response, CommandMessage};
15use crate::conn::Connection;
16use crate::detection::{self, DetectionOptions};
17use crate::error::{BrowserStderr, CdpError, Result};
18use crate::handler::browser::BrowserContext;
19use crate::handler::viewport::Viewport;
20use crate::handler::{Handler, HandlerConfig, HandlerMessage, REQUEST_TIMEOUT};
21use crate::listeners::{EventListenerRequest, EventStream};
22use crate::page::Page;
23use crate::utils;
24use chromiumoxide_cdp::cdp::browser_protocol::browser::{
25    BrowserContextId, CloseReturns, GetVersionParams, GetVersionReturns,
26};
27use chromiumoxide_cdp::cdp::browser_protocol::browser::{
28    PermissionDescriptor, PermissionSetting, SetPermissionParams,
29};
30use chromiumoxide_cdp::cdp::browser_protocol::network::{Cookie, CookieParam};
31use chromiumoxide_cdp::cdp::browser_protocol::storage::{
32    ClearCookiesParams, GetCookiesParams, SetCookiesParams,
33};
34use chromiumoxide_cdp::cdp::browser_protocol::target::{
35    CreateBrowserContextParams, CreateTargetParams, DisposeBrowserContextParams,
36    GetBrowserContextsParams, GetBrowserContextsReturns, TargetId, TargetInfo,
37};
38
39use chromiumoxide_cdp::cdp::{CdpEventMessage, IntoEventKind};
40use chromiumoxide_types::*;
41use spider_network_blocker::intercept_manager::NetworkInterceptManager;
42
43/// Default `Browser::launch` timeout in MS
44pub const LAUNCH_TIMEOUT: u64 = 20_000;
45
46lazy_static::lazy_static! {
47    /// The request client to get the web socket url.
48    static ref REQUEST_CLIENT: reqwest::Client = reqwest::Client::builder()
49        .timeout(Duration::from_secs(60))
50        .default_headers({
51            let mut m = HeaderMap::new();
52
53            m.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
54
55            m
56        })
57        .tcp_keepalive(Some(Duration::from_secs(5)))
58        .pool_idle_timeout(Some(Duration::from_secs(60)))
59        .pool_max_idle_per_host(10)
60        .build()
61        .expect("client to build");
62}
63
64/// A [`Browser`] is created when chromiumoxide connects to a Chromium instance.
65#[derive(Debug)]
66pub struct Browser {
67    /// The `Sender` to send messages to the connection handler that drives the
68    /// websocket
69    pub(crate) sender: Sender<HandlerMessage>,
70    /// How the spawned chromium instance was configured, if any
71    config: Option<BrowserConfig>,
72    /// The spawned chromium instance
73    child: Option<Child>,
74    /// The debug web socket url of the chromium instance
75    debug_ws_url: String,
76    /// The context of the browser
77    pub browser_context: BrowserContext,
78}
79
80/// Browser connection information.
81#[derive(serde::Deserialize, Debug, Default)]
82pub struct BrowserConnection {
83    #[serde(rename = "Browser")]
84    /// The browser name
85    pub browser: String,
86    #[serde(rename = "Protocol-Version")]
87    /// Browser version
88    pub protocol_version: String,
89    #[serde(rename = "User-Agent")]
90    /// User Agent used by default.
91    pub user_agent: String,
92    #[serde(rename = "V8-Version")]
93    /// The v8 engine version
94    pub v8_version: String,
95    #[serde(rename = "WebKit-Version")]
96    /// Webkit version
97    pub webkit_version: String,
98    #[serde(rename = "webSocketDebuggerUrl")]
99    /// Remote debugging address
100    pub web_socket_debugger_url: String,
101}
102
103impl Browser {
104    /// Connect to an already running chromium instance via the given URL.
105    ///
106    /// If the URL is a http(s) URL, it will first attempt to retrieve the Websocket URL from the `json/version` endpoint.
107    pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
108        Self::connect_with_config(url, HandlerConfig::default()).await
109    }
110
111    // Connect to an already running chromium instance with a given `HandlerConfig`.
112    ///
113    /// If the URL is a http URL, it will first attempt to retrieve the Websocket URL from the `json/version` endpoint.
114    pub async fn connect_with_config(
115        url: impl Into<String>,
116        config: HandlerConfig,
117    ) -> Result<(Self, Handler)> {
118        let mut debug_ws_url = url.into();
119        let retries = config.connection_retries;
120
121        if debug_ws_url.starts_with("http") {
122            let version_url = if debug_ws_url.ends_with("/json/version")
123                || debug_ws_url.ends_with("/json/version/")
124            {
125                debug_ws_url.to_owned()
126            } else {
127                format!(
128                    "{}{}json/version",
129                    &debug_ws_url,
130                    if debug_ws_url.ends_with('/') { "" } else { "/" }
131                )
132            };
133
134            let mut discovered = false;
135
136            for attempt in 0..=retries {
137                let retry = || async {
138                    if attempt < retries {
139                        let backoff_ms = 50u64 * 3u64.saturating_pow(attempt);
140                        tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
141                    }
142                };
143
144                match REQUEST_CLIENT.get(&version_url).send().await {
145                    Ok(req) => match req.bytes().await {
146                        Ok(b) => {
147                            match crate::serde_json::from_slice::<Box<BrowserConnection>>(&b) {
148                                Ok(connection)
149                                    if !connection.web_socket_debugger_url.is_empty() =>
150                                {
151                                    debug_ws_url = connection.web_socket_debugger_url;
152                                    discovered = true;
153                                    break;
154                                }
155                                _ => {
156                                    // JSON parse failed or webSocketDebuggerUrl was empty — retry
157                                    retry().await;
158                                }
159                            }
160                        }
161                        Err(_) => {
162                            retry().await;
163                        }
164                    },
165                    Err(_) => {
166                        retry().await;
167                    }
168                }
169            }
170
171            if !discovered {
172                return Err(CdpError::NoResponse);
173            }
174        }
175
176        let conn =
177            Connection::<CdpEventMessage>::connect_with_retries(&debug_ws_url, retries).await?;
178
179        let (tx, rx) = channel(config.channel_capacity);
180
181        let handler_config = BrowserConfig {
182            ignore_https_errors: config.ignore_https_errors,
183            viewport: config.viewport.clone(),
184            request_timeout: config.request_timeout,
185            request_intercept: config.request_intercept,
186            cache_enabled: config.cache_enabled,
187            ignore_visuals: config.ignore_visuals,
188            ignore_stylesheets: config.ignore_stylesheets,
189            ignore_javascript: config.ignore_javascript,
190            ignore_analytics: config.ignore_analytics,
191            ignore_prefetch: config.ignore_prefetch,
192            ignore_ads: config.ignore_ads,
193            extra_headers: config.extra_headers.clone(),
194            only_html: config.only_html,
195            service_worker_enabled: config.service_worker_enabled,
196            intercept_manager: config.intercept_manager,
197            max_bytes_allowed: config.max_bytes_allowed,
198            whitelist_patterns: config.whitelist_patterns.clone(),
199            blacklist_patterns: config.blacklist_patterns.clone(),
200            ..Default::default()
201        };
202
203        let fut = Handler::new(conn, rx, config);
204        let browser_context = fut.default_browser_context().clone();
205
206        let browser = Self {
207            sender: tx,
208            config: Some(handler_config),
209            child: None,
210            debug_ws_url,
211            browser_context,
212        };
213
214        Ok((browser, fut))
215    }
216
217    /// Launches a new instance of `chromium` in the background and attaches to
218    /// its debug web socket.
219    ///
220    /// This fails when no chromium executable could be detected.
221    ///
222    /// This fails if no web socket url could be detected from the child
223    /// processes stderr for more than the configured `launch_timeout`
224    /// (20 seconds by default).
225    pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
226        // Canonalize paths to reduce issues with sandboxing
227        config.executable = utils::canonicalize_except_snap(config.executable).await?;
228
229        // Launch a new chromium instance
230        let mut child = config.launch()?;
231
232        /// Faillible initialization to run once the child process is created.
233        ///
234        /// All faillible calls must be executed inside this function. This ensures that all
235        /// errors are caught and that the child process is properly cleaned-up.
236        async fn with_child(
237            config: &BrowserConfig,
238            child: &mut Child,
239        ) -> Result<(String, Connection<CdpEventMessage>)> {
240            let dur = config.launch_timeout;
241            let timeout_fut = Box::pin(tokio::time::sleep(dur));
242
243            // extract the ws:
244            let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
245            let conn = Connection::<CdpEventMessage>::connect_with_retries(
246                &debug_ws_url,
247                config.connection_retries,
248            )
249            .await?;
250            Ok((debug_ws_url, conn))
251        }
252
253        let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
254            Ok(conn) => conn,
255            Err(e) => {
256                // An initialization error occurred, clean up the process
257                if let Ok(Some(_)) = child.try_wait() {
258                    // already exited, do nothing, may happen if the browser crashed
259                } else {
260                    // the process is still alive, kill it and wait for exit (avoid zombie processes)
261                    let _ = child.kill().await;
262                    let _ = child.wait().await;
263                }
264                return Err(e);
265            }
266        };
267
268        // Only infaillible calls are allowed after this point to avoid clean-up issues with the
269        // child process.
270
271        let (tx, rx) = channel(config.channel_capacity);
272
273        let handler_config = HandlerConfig {
274            ignore_https_errors: config.ignore_https_errors,
275            viewport: config.viewport.clone(),
276            context_ids: Vec::new(),
277            request_timeout: config.request_timeout,
278            request_intercept: config.request_intercept,
279            cache_enabled: config.cache_enabled,
280            ignore_visuals: config.ignore_visuals,
281            ignore_stylesheets: config.ignore_stylesheets,
282            ignore_javascript: config.ignore_javascript,
283            ignore_analytics: config.ignore_analytics,
284            ignore_prefetch: config.ignore_prefetch,
285            ignore_ads: config.ignore_ads,
286            extra_headers: config.extra_headers.clone(),
287            only_html: config.only_html,
288            service_worker_enabled: config.service_worker_enabled,
289            created_first_target: false,
290            intercept_manager: config.intercept_manager,
291            max_bytes_allowed: config.max_bytes_allowed,
292            whitelist_patterns: config.whitelist_patterns.clone(),
293            blacklist_patterns: config.blacklist_patterns.clone(),
294            #[cfg(feature = "adblock")]
295            adblock_filter_rules: config.adblock_filter_rules.clone(),
296            channel_capacity: config.channel_capacity,
297            connection_retries: config.connection_retries,
298        };
299
300        let fut = Handler::new(conn, rx, handler_config);
301        let browser_context = fut.default_browser_context().clone();
302
303        let browser = Self {
304            sender: tx,
305            config: Some(config),
306            child: Some(child),
307            debug_ws_url,
308            browser_context,
309        };
310
311        Ok((browser, fut))
312    }
313
314    /// Request to fetch all existing browser targets.
315    ///
316    /// By default, only targets launched after the browser connection are tracked
317    /// when connecting to a existing browser instance with the devtools websocket url
318    /// This function fetches existing targets on the browser and adds them as pages internally
319    ///
320    /// The pages are not guaranteed to be ready as soon as the function returns
321    /// You should wait a few millis if you need to use a page
322    /// Returns [TargetInfo]
323    pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
324        let (tx, rx) = oneshot_channel();
325
326        self.sender.send(HandlerMessage::FetchTargets(tx)).await?;
327
328        rx.await?
329    }
330
331    /// Request for the browser to close completely.
332    ///
333    /// If the browser was spawned by [`Browser::launch`], it is recommended to wait for the
334    /// spawned instance exit, to avoid "zombie" processes ([`Browser::wait`],
335    /// [`Browser::wait_sync`], [`Browser::try_wait`]).
336    /// [`Browser::drop`] waits automatically if needed.
337    pub async fn close(&self) -> Result<CloseReturns> {
338        let (tx, rx) = oneshot_channel();
339
340        self.sender.send(HandlerMessage::CloseBrowser(tx)).await?;
341
342        rx.await?
343    }
344
345    /// Asynchronously wait for the spawned chromium instance to exit completely.
346    ///
347    /// The instance is spawned by [`Browser::launch`]. `wait` is usually called after
348    /// [`Browser::close`]. You can call this explicitly to collect the process and avoid
349    /// "zombie" processes.
350    ///
351    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
352    /// connected to an existing browser through [`Browser::connect`])
353    pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
354        if let Some(child) = self.child.as_mut() {
355            Ok(Some(child.wait().await?))
356        } else {
357            Ok(None)
358        }
359    }
360
361    /// If the spawned chromium instance has completely exited, wait for it.
362    ///
363    /// The instance is spawned by [`Browser::launch`]. `try_wait` is usually called after
364    /// [`Browser::close`]. You can call this explicitly to collect the process and avoid
365    /// "zombie" processes.
366    ///
367    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
368    /// connected to an existing browser through [`Browser::connect`])
369    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
370        if let Some(child) = self.child.as_mut() {
371            child.try_wait()
372        } else {
373            Ok(None)
374        }
375    }
376
377    /// Get the spawned chromium instance
378    ///
379    /// The instance is spawned by [`Browser::launch`]. The result is a [`async_process::Child`]
380    /// value. It acts as a compat wrapper for an `async-std` or `tokio` child process.
381    ///
382    /// You may use [`async_process::Child::as_mut_inner`] to retrieve the concrete implementation
383    /// for the selected runtime.
384    ///
385    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
386    /// connected to an existing browser through [`Browser::connect`])
387    pub fn get_mut_child(&mut self) -> Option<&mut Child> {
388        self.child.as_mut()
389    }
390
391    /// Has a browser instance launched on system.
392    pub fn has_child(&self) -> bool {
393        self.child.is_some()
394    }
395
396    /// Forcibly kill the spawned chromium instance
397    ///
398    /// The instance is spawned by [`Browser::launch`]. `kill` will automatically wait for the child
399    /// process to exit to avoid "zombie" processes.
400    ///
401    /// This method is provided to help if the browser does not close by itself. You should prefer
402    /// to use [`Browser::close`].
403    ///
404    /// This call has no effect if this [`Browser`] did not spawn any chromium instance (e.g.
405    /// connected to an existing browser through [`Browser::connect`])
406    pub async fn kill(&mut self) -> Option<io::Result<()>> {
407        match self.child.as_mut() {
408            Some(child) => Some(child.kill().await),
409            None => None,
410        }
411    }
412
413    /// If not launched as incognito this creates a new incognito browser
414    /// context. After that this browser exists within the incognito session.
415    /// New pages created while being in incognito mode will also run in the
416    /// incognito context. Incognito contexts won't share cookies/cache with
417    /// other browser contexts.
418    pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
419        if !self.is_incognito_configured() {
420            let browser_context_id = self
421                .create_browser_context(CreateBrowserContextParams::default())
422                .await?;
423            self.browser_context = BrowserContext::from(browser_context_id);
424            self.sender
425                .send(HandlerMessage::InsertContext(self.browser_context.clone()))
426                .await?;
427        }
428
429        Ok(self)
430    }
431
432    /// If a incognito session was created with
433    /// `Browser::start_incognito_context` this disposes this context.
434    ///
435    /// # Note This will also dispose all pages that were running within the
436    /// incognito context.
437    pub async fn quit_incognito_context_base(
438        &self,
439        browser_context_id: BrowserContextId,
440    ) -> Result<&Self> {
441        self.dispose_browser_context(browser_context_id.clone())
442            .await?;
443        self.sender
444            .send(HandlerMessage::DisposeContext(BrowserContext::from(
445                browser_context_id,
446            )))
447            .await?;
448        Ok(self)
449    }
450
451    /// If a incognito session was created with
452    /// `Browser::start_incognito_context` this disposes this context.
453    ///
454    /// # Note This will also dispose all pages that were running within the
455    /// incognito context.
456    pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
457        if let Some(id) = self.browser_context.take() {
458            let _ = self.quit_incognito_context_base(id).await;
459        }
460        Ok(self)
461    }
462
463    /// Whether incognito mode was configured from the start
464    fn is_incognito_configured(&self) -> bool {
465        self.config
466            .as_ref()
467            .map(|c| c.incognito)
468            .unwrap_or_default()
469    }
470
471    /// Returns the address of the websocket this browser is attached to
472    pub fn websocket_address(&self) -> &String {
473        &self.debug_ws_url
474    }
475
476    /// Whether the BrowserContext is incognito.
477    pub fn is_incognito(&self) -> bool {
478        self.is_incognito_configured() || self.browser_context.is_incognito()
479    }
480
481    /// The config of the spawned chromium instance if any.
482    pub fn config(&self) -> Option<&BrowserConfig> {
483        self.config.as_ref()
484    }
485
486    /// Create a new browser page
487    pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
488        let (tx, rx) = oneshot_channel();
489        let mut params = params.into();
490
491        if let Some(id) = self.browser_context.id() {
492            if params.browser_context_id.is_none() {
493                params.browser_context_id = Some(id.clone());
494            }
495        }
496
497        let _ = self
498            .sender
499            .send(HandlerMessage::CreatePage(params, tx))
500            .await;
501
502        rx.await?
503    }
504
505    /// Version information about the browser
506    pub async fn version(&self) -> Result<GetVersionReturns> {
507        Ok(self.execute(GetVersionParams::default()).await?.result)
508    }
509
510    /// Returns the user agent of the browser
511    pub async fn user_agent(&self) -> Result<String> {
512        Ok(self.version().await?.user_agent)
513    }
514
515    /// Call a browser method.
516    pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
517        let (tx, rx) = oneshot_channel();
518        let method = cmd.identifier();
519        let msg = CommandMessage::new(cmd, tx)?;
520
521        self.sender.send(HandlerMessage::Command(msg)).await?;
522        let resp = rx.await??;
523        to_command_response::<T>(resp, method)
524    }
525
526    /// Set permission settings for given embedding and embedded origins.
527    /// [PermissionDescriptor](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#type-PermissionDescriptor)
528    /// [PermissionSetting](https://chromedevtools.github.io/devtools-protocol/tot/Browser/#type-PermissionSetting)
529    pub async fn set_permission(
530        &self,
531        permission: PermissionDescriptor,
532        setting: PermissionSetting,
533        origin: Option<impl Into<String>>,
534        embedded_origin: Option<impl Into<String>>,
535        browser_context_id: Option<BrowserContextId>,
536    ) -> Result<&Self> {
537        self.execute(SetPermissionParams {
538            permission,
539            setting,
540            origin: origin.map(Into::into),
541            embedded_origin: embedded_origin.map(Into::into),
542            browser_context_id: browser_context_id.or_else(|| self.browser_context.id.clone()),
543        })
544        .await?;
545        Ok(self)
546    }
547
548    /// Convenience: set a permission for a single origin using the current browser context.
549    pub async fn set_permission_for_origin(
550        &self,
551        origin: impl Into<String>,
552        embedded_origin: Option<impl Into<String>>,
553        permission: PermissionDescriptor,
554        setting: PermissionSetting,
555    ) -> Result<&Self> {
556        self.set_permission(permission, setting, Some(origin), embedded_origin, None)
557            .await
558    }
559
560    /// "Reset" a permission override by setting it back to Prompt.
561    pub async fn reset_permission_for_origin(
562        &self,
563        origin: impl Into<String>,
564        embedded_origin: Option<impl Into<String>>,
565        permission: PermissionDescriptor,
566    ) -> Result<&Self> {
567        self.set_permission_for_origin(
568            origin,
569            embedded_origin,
570            permission,
571            PermissionSetting::Prompt,
572        )
573        .await
574    }
575
576    /// "Grant" all permissions.
577    pub async fn grant_all_permission_for_origin(
578        &self,
579        origin: impl Into<String>,
580        embedded_origin: Option<impl Into<String>>,
581        permission: PermissionDescriptor,
582    ) -> Result<&Self> {
583        self.set_permission_for_origin(
584            origin,
585            embedded_origin,
586            permission,
587            PermissionSetting::Granted,
588        )
589        .await
590    }
591
592    /// "Deny" all permissions.
593    pub async fn deny_all_permission_for_origin(
594        &self,
595        origin: impl Into<String>,
596        embedded_origin: Option<impl Into<String>>,
597        permission: PermissionDescriptor,
598    ) -> Result<&Self> {
599        self.set_permission_for_origin(
600            origin,
601            embedded_origin,
602            permission,
603            PermissionSetting::Denied,
604        )
605        .await
606    }
607
608    /// Return all of the pages of the browser
609    pub async fn pages(&self) -> Result<Vec<Page>> {
610        let (tx, rx) = oneshot_channel();
611        self.sender.send(HandlerMessage::GetPages(tx)).await?;
612        Ok(rx.await?)
613    }
614
615    /// Return page of given target_id
616    pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
617        let (tx, rx) = oneshot_channel();
618        self.sender
619            .send(HandlerMessage::GetPage(target_id, tx))
620            .await?;
621        rx.await?.ok_or(CdpError::NotFound)
622    }
623
624    /// Set listener for browser event
625    pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
626        let (tx, rx) = unbounded_channel();
627        self.sender
628            .send(HandlerMessage::AddEventListener(
629                EventListenerRequest::new::<T>(tx),
630            ))
631            .await?;
632
633        Ok(EventStream::new(rx))
634    }
635
636    /// Creates a new empty browser context.
637    pub async fn create_browser_context(
638        &mut self,
639        params: CreateBrowserContextParams,
640    ) -> Result<BrowserContextId> {
641        let response = self.execute(params).await?;
642
643        Ok(response.result.browser_context_id)
644    }
645
646    /// Returns all browser contexts created with Target.createBrowserContext method.
647    pub async fn get_browser_contexts(
648        &mut self,
649        params: GetBrowserContextsParams,
650    ) -> Result<GetBrowserContextsReturns> {
651        let response = self.execute(params).await?;
652        Ok(response.result)
653    }
654
655    /// Send a new empty browser context.
656    pub async fn send_new_context(
657        &mut self,
658        browser_context_id: BrowserContextId,
659    ) -> Result<&Self> {
660        self.browser_context = BrowserContext::from(browser_context_id);
661        self.sender
662            .send(HandlerMessage::InsertContext(self.browser_context.clone()))
663            .await?;
664        Ok(self)
665    }
666
667    /// Deletes a browser context.
668    pub async fn dispose_browser_context(
669        &self,
670        browser_context_id: impl Into<BrowserContextId>,
671    ) -> Result<&Self> {
672        self.execute(DisposeBrowserContextParams::new(browser_context_id))
673            .await?;
674
675        Ok(self)
676    }
677
678    /// Clears cookies.
679    pub async fn clear_cookies(&self) -> Result<&Self> {
680        self.execute(ClearCookiesParams::default()).await?;
681        Ok(self)
682    }
683
684    /// Returns all browser cookies.
685    pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
686        let cmd = GetCookiesParams {
687            browser_context_id: self.browser_context.id.clone(),
688        };
689
690        Ok(self.execute(cmd).await?.result.cookies)
691    }
692
693    /// Sets given cookies.
694    pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
695        for cookie in &mut cookies {
696            if let Some(url) = cookie.url.as_ref() {
697                crate::page::validate_cookie_url(url)?;
698            }
699        }
700
701        let mut cookies_param = SetCookiesParams::new(cookies);
702
703        cookies_param.browser_context_id = self.browser_context.id.clone();
704
705        self.execute(cookies_param).await?;
706        Ok(self)
707    }
708}
709
710impl Drop for Browser {
711    fn drop(&mut self) {
712        if let Some(child) = self.child.as_mut() {
713            if let Ok(Some(_)) = child.try_wait() {
714                // Already exited, do nothing. Usually occurs after using the method close or kill.
715            } else {
716                // We set the `kill_on_drop` property for the child process, so no need to explicitely
717                // kill it here. It can't really be done anyway since the method is async.
718                //
719                // On Unix, the process will be reaped in the background by the runtime automatically
720                // so it won't leave any resources locked. It is, however, a better practice for the user to
721                // do it himself since the runtime doesn't provide garantees as to when the reap occurs, so we
722                // warn him here.
723                tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
724            }
725        }
726    }
727}
728
729/// Resolve devtools WebSocket URL from the provided browser process
730///
731/// If an error occurs, it returns the browser's stderr output.
732///
733/// The URL resolution fails if:
734/// - [`CdpError::LaunchTimeout`]: `timeout_fut` completes, this corresponds to a timeout
735/// - [`CdpError::LaunchExit`]: the browser process exits (or is killed)
736/// - [`CdpError::LaunchIo`]: an input/output error occurs when await the process exit or reading
737///   the browser's stderr: end of stream, invalid UTF-8, other
738async fn ws_url_from_output(
739    child_process: &mut Child,
740    timeout_fut: impl Future<Output = ()> + Unpin,
741) -> Result<String> {
742    use tokio::io::AsyncBufReadExt;
743    let stderr = match child_process.stderr.take() {
744        Some(stderr) => stderr,
745        None => {
746            return Err(CdpError::LaunchIo(
747                io::Error::new(io::ErrorKind::NotFound, "browser process has no stderr"),
748                BrowserStderr::new(Vec::new()),
749            ));
750        }
751    };
752    let mut stderr_bytes = Vec::<u8>::new();
753    let mut buf = tokio::io::BufReader::new(stderr);
754    let mut timeout_fut = timeout_fut;
755    loop {
756        tokio::select! {
757            _ = &mut timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
758            exit_status = child_process.wait() => {
759                return Err(match exit_status {
760                    Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
761                    Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
762                })
763            },
764            read_res = buf.read_until(b'\n', &mut stderr_bytes) => {
765                match read_res {
766                    Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
767                    Ok(byte_count) => {
768                        if byte_count == 0 {
769                            let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
770                            return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
771                        }
772                        let start_offset = stderr_bytes.len() - byte_count;
773                        let new_bytes = &stderr_bytes[start_offset..];
774                        match std::str::from_utf8(new_bytes) {
775                            Err(_) => {
776                                let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
777                                return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
778                            }
779                            Ok(line) => {
780                                if let Some((_, ws)) = line.rsplit_once("listening on ") {
781                                    if ws.starts_with("ws") && ws.contains("devtools/browser") {
782                                        return Ok(ws.trim().to_string());
783                                    }
784                                }
785                            }
786                        }
787                    }
788                }
789            }
790        }
791    }
792}
793
794#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
795pub enum HeadlessMode {
796    /// The "headful" mode.
797    False,
798    /// The old headless mode.
799    #[default]
800    True,
801    /// The new headless mode. See also: https://developer.chrome.com/docs/chromium/new-headless
802    New,
803}
804
805#[derive(Debug, Clone, Default)]
806pub struct BrowserConfig {
807    /// Determines whether to run headless version of the browser. Defaults to
808    /// true.
809    headless: HeadlessMode,
810    /// Determines whether to run the browser with a sandbox.
811    sandbox: bool,
812    /// Launch the browser with a specific window width and height.
813    window_size: Option<(u32, u32)>,
814    /// Launch the browser with a specific debugging port.
815    port: u16,
816    /// Path for Chrome or Chromium.
817    ///
818    /// If unspecified, the create will try to automatically detect a suitable
819    /// binary.
820    executable: std::path::PathBuf,
821
822    /// A list of Chrome extensions to load.
823    ///
824    /// An extension should be a path to a folder containing the extension code.
825    /// CRX files cannot be used directly and must be first extracted.
826    ///
827    /// Note that Chrome does not support loading extensions in headless-mode.
828    /// See https://bugs.chromium.org/p/chromium/issues/detail?id=706008#c5
829    extensions: Vec<String>,
830
831    /// Environment variables to set for the Chromium process.
832    /// Passes value through to std::process::Command::envs.
833    pub process_envs: Option<HashMap<String, String>>,
834
835    /// Data dir for user data
836    pub user_data_dir: Option<PathBuf>,
837
838    /// Whether to launch the `Browser` in incognito mode.
839    incognito: bool,
840
841    /// Timeout duration for `Browser::launch`.
842    launch_timeout: Duration,
843
844    /// Ignore https errors, default is true.
845    ignore_https_errors: bool,
846    pub viewport: Option<Viewport>,
847    /// The duration after a request with no response should time out.
848    request_timeout: Duration,
849
850    /// Additional command line arguments to pass to the browser instance.
851    args: Vec<String>,
852
853    /// Whether to disable DEFAULT_ARGS or not, default is false.
854    disable_default_args: bool,
855
856    /// Whether to enable request interception.
857    pub request_intercept: bool,
858
859    /// Whether to enable cache.
860    pub cache_enabled: bool,
861    /// Whether to enable or disable Service Workers.
862    /// Disabling may reduce background network activity and caching effects.
863    pub service_worker_enabled: bool,
864    /// Whether to ignore image/visual requests during interception.
865    /// Can reduce bandwidth and speed up crawling when visuals are unnecessary.
866    pub ignore_visuals: bool,
867    /// Whether to ignore stylesheet (CSS) requests during interception.
868    /// Useful for content-only crawls.
869    pub ignore_stylesheets: bool,
870    /// Whether to ignore JavaScript requests during interception.
871    /// This still allows critical framework bundles to pass when applicable.
872    pub ignore_javascript: bool,
873    /// Whether to ignore analytics/telemetry requests during interception.
874    pub ignore_analytics: bool,
875    /// Ignore prefetch request.
876    pub ignore_prefetch: bool,
877    /// Whether to ignore ad network requests during interception.
878    pub ignore_ads: bool,
879    /// Extra headers.
880    pub extra_headers: Option<std::collections::HashMap<String, String>>,
881    /// Only html
882    pub only_html: bool,
883    /// The interception intercept manager.
884    pub intercept_manager: NetworkInterceptManager,
885    /// The max bytes to receive.
886    pub max_bytes_allowed: Option<u64>,
887    /// Whitelist patterns to allow through the network.
888    pub whitelist_patterns: Option<Vec<String>>,
889    /// Blacklist patterns to block through the network.
890    pub blacklist_patterns: Option<Vec<String>>,
891    /// Extra ABP/uBO filter rules to load into the adblock engine (requires `adblock` feature).
892    /// These are merged with the built-in `ADBLOCK_PATTERNS` for richer blocking
893    /// (e.g. EasyList / EasyPrivacy content).
894    #[cfg(feature = "adblock")]
895    pub adblock_filter_rules: Option<Vec<String>>,
896    /// Capacity of the channel between browser handle and handler.
897    /// Defaults to 1000.
898    pub channel_capacity: usize,
899    /// Number of WebSocket connection retry attempts with exponential backoff.
900    /// Defaults to 4.
901    pub connection_retries: u32,
902}
903
904#[derive(Debug, Clone)]
905pub struct BrowserConfigBuilder {
906    /// Headless mode configuration for the browser.
907    headless: HeadlessMode,
908    /// Whether to run the browser with a sandbox.
909    sandbox: bool,
910    /// Optional initial browser window size `(width, height)`.
911    window_size: Option<(u32, u32)>,
912    /// DevTools debugging port to bind to.
913    port: u16,
914    /// Optional explicit path to the Chrome/Chromium executable.
915    /// If `None`, auto-detection may be attempted based on `executation_detection`.
916    executable: Option<PathBuf>,
917    /// Controls auto-detection behavior for finding a Chrome/Chromium binary.
918    executation_detection: DetectionOptions,
919    /// List of unpacked extensions (directories) to load at startup.
920    extensions: Vec<String>,
921    /// Environment variables to set on the spawned Chromium process.
922    process_envs: Option<HashMap<String, String>>,
923    /// User data directory to persist browser state, or `None` for ephemeral.
924    user_data_dir: Option<PathBuf>,
925    /// Whether to start the browser in incognito (off-the-record) mode.
926    incognito: bool,
927    /// Maximum time to wait for the browser to launch and become ready.
928    launch_timeout: Duration,
929    /// Whether to ignore HTTPS/TLS errors during navigation and requests.
930    ignore_https_errors: bool,
931    /// Default page viewport configuration applied on startup.
932    viewport: Option<Viewport>,
933    /// Timeout for individual network requests without response progress.
934    request_timeout: Duration,
935    /// Additional command-line flags passed directly to the browser process.
936    args: Vec<String>,
937    /// Disable the default argument set and use only the provided `args`.
938    disable_default_args: bool,
939    /// Enable Network.requestInterception for request filtering/handling.
940    request_intercept: bool,
941    /// Enable the browser cache for navigations and subresources.
942    cache_enabled: bool,
943    /// Enable/disable Service Workers.
944    service_worker_enabled: bool,
945    /// Drop image/visual requests when interception is enabled.
946    ignore_visuals: bool,
947    /// Drop ad network requests when interception is enabled.
948    ignore_ads: bool,
949    /// Drop JavaScript requests when interception is enabled.
950    ignore_javascript: bool,
951    /// Drop stylesheet (CSS) requests when interception is enabled.
952    ignore_stylesheets: bool,
953    /// Ignore prefetch domains.
954    ignore_prefetch: bool,
955    /// Drop analytics/telemetry requests when interception is enabled.
956    ignore_analytics: bool,
957    /// If `true`, limit fetching to HTML documents.
958    only_html: bool,
959    /// Extra HTTP headers to include with every request.
960    extra_headers: Option<std::collections::HashMap<String, String>>,
961    /// Network interception manager used to configure filtering behavior.
962    intercept_manager: NetworkInterceptManager,
963    /// Optional upper bound on bytes that may be received (per session/run).
964    max_bytes_allowed: Option<u64>,
965    /// Whitelist patterns to allow through the network.
966    whitelist_patterns: Option<Vec<String>>,
967    /// Blacklist patterns to block through the network.
968    blacklist_patterns: Option<Vec<String>>,
969    /// Extra ABP/uBO filter rules for the adblock engine.
970    #[cfg(feature = "adblock")]
971    adblock_filter_rules: Option<Vec<String>>,
972    /// Capacity of the channel between browser handle and handler.
973    channel_capacity: usize,
974    /// Number of WebSocket connection retry attempts.
975    connection_retries: u32,
976}
977
978impl BrowserConfig {
979    /// Browser builder default config.
980    pub fn builder() -> BrowserConfigBuilder {
981        BrowserConfigBuilder::default()
982    }
983
984    /// Launch with the executable path.
985    pub fn with_executable(path: impl AsRef<Path>) -> Self {
986        // SAFETY: build() only fails when no executable is provided,
987        // but we always provide one via chrome_executable().
988        Self::builder().chrome_executable(path).build().unwrap()
989    }
990}
991
992impl Default for BrowserConfigBuilder {
993    fn default() -> Self {
994        Self {
995            headless: HeadlessMode::True,
996            sandbox: true,
997            window_size: None,
998            port: 0,
999            executable: None,
1000            executation_detection: DetectionOptions::default(),
1001            extensions: Vec::new(),
1002            process_envs: None,
1003            user_data_dir: None,
1004            incognito: false,
1005            launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
1006            ignore_https_errors: true,
1007            viewport: Some(Default::default()),
1008            request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1009            args: Vec::new(),
1010            disable_default_args: false,
1011            request_intercept: false,
1012            cache_enabled: true,
1013            ignore_visuals: false,
1014            ignore_ads: false,
1015            ignore_javascript: false,
1016            ignore_analytics: false,
1017            ignore_stylesheets: false,
1018            ignore_prefetch: true,
1019            only_html: false,
1020            extra_headers: Default::default(),
1021            service_worker_enabled: true,
1022            intercept_manager: NetworkInterceptManager::Unknown,
1023            max_bytes_allowed: None,
1024            whitelist_patterns: None,
1025            blacklist_patterns: None,
1026            #[cfg(feature = "adblock")]
1027            adblock_filter_rules: None,
1028            channel_capacity: 1000,
1029            connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1030        }
1031    }
1032}
1033
1034impl BrowserConfigBuilder {
1035    /// Configure window size.
1036    pub fn window_size(mut self, width: u32, height: u32) -> Self {
1037        self.window_size = Some((width, height));
1038        self
1039    }
1040    /// Configure sandboxing.
1041    pub fn no_sandbox(mut self) -> Self {
1042        self.sandbox = false;
1043        self
1044    }
1045    /// Configure the launch to start non headless.
1046    pub fn with_head(mut self) -> Self {
1047        self.headless = HeadlessMode::False;
1048        self
1049    }
1050    /// Configure the launch with the new headless mode.
1051    pub fn new_headless_mode(mut self) -> Self {
1052        self.headless = HeadlessMode::New;
1053        self
1054    }
1055    /// Configure the launch with headless.
1056    pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
1057        self.headless = mode;
1058        self
1059    }
1060    /// Configure the launch in incognito.
1061    pub fn incognito(mut self) -> Self {
1062        self.incognito = true;
1063        self
1064    }
1065
1066    pub fn respect_https_errors(mut self) -> Self {
1067        self.ignore_https_errors = false;
1068        self
1069    }
1070
1071    pub fn port(mut self, port: u16) -> Self {
1072        self.port = port;
1073        self
1074    }
1075
1076    pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
1077        self.max_bytes_allowed = max_bytes_allowed;
1078        self
1079    }
1080
1081    pub fn launch_timeout(mut self, timeout: Duration) -> Self {
1082        self.launch_timeout = timeout;
1083        self
1084    }
1085
1086    pub fn request_timeout(mut self, timeout: Duration) -> Self {
1087        self.request_timeout = timeout;
1088        self
1089    }
1090
1091    /// Configures the viewport of the browser, which defaults to `800x600`.
1092    /// `None` disables viewport emulation (i.e., it uses the browsers default
1093    /// configuration, which fills the available space. This is similar to what
1094    /// Playwright does when you provide `null` as the value of its `viewport`
1095    /// option).
1096    pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
1097        self.viewport = viewport.into();
1098        self
1099    }
1100
1101    pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
1102        self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
1103        self
1104    }
1105
1106    pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
1107        self.executable = Some(path.as_ref().to_path_buf());
1108        self
1109    }
1110
1111    pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
1112        self.executation_detection = options;
1113        self
1114    }
1115
1116    pub fn extension(mut self, extension: impl Into<String>) -> Self {
1117        self.extensions.push(extension.into());
1118        self
1119    }
1120
1121    pub fn extensions<I, S>(mut self, extensions: I) -> Self
1122    where
1123        I: IntoIterator<Item = S>,
1124        S: Into<String>,
1125    {
1126        for ext in extensions {
1127            self.extensions.push(ext.into());
1128        }
1129        self
1130    }
1131
1132    pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
1133        self.process_envs
1134            .get_or_insert(HashMap::new())
1135            .insert(key.into(), val.into());
1136        self
1137    }
1138
1139    pub fn envs<I, K, V>(mut self, envs: I) -> Self
1140    where
1141        I: IntoIterator<Item = (K, V)>,
1142        K: Into<String>,
1143        V: Into<String>,
1144    {
1145        self.process_envs
1146            .get_or_insert(HashMap::new())
1147            .extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
1148        self
1149    }
1150
1151    pub fn arg(mut self, arg: impl Into<String>) -> Self {
1152        self.args.push(arg.into());
1153        self
1154    }
1155
1156    pub fn args<I, S>(mut self, args: I) -> Self
1157    where
1158        I: IntoIterator<Item = S>,
1159        S: Into<String>,
1160    {
1161        for arg in args {
1162            self.args.push(arg.into());
1163        }
1164        self
1165    }
1166
1167    pub fn disable_default_args(mut self) -> Self {
1168        self.disable_default_args = true;
1169        self
1170    }
1171
1172    pub fn enable_request_intercept(mut self) -> Self {
1173        self.request_intercept = true;
1174        self
1175    }
1176
1177    pub fn disable_request_intercept(mut self) -> Self {
1178        self.request_intercept = false;
1179        self
1180    }
1181
1182    pub fn enable_cache(mut self) -> Self {
1183        self.cache_enabled = true;
1184        self
1185    }
1186
1187    pub fn disable_cache(mut self) -> Self {
1188        self.cache_enabled = false;
1189        self
1190    }
1191
1192    /// Set service worker enabled.
1193    pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
1194        self.service_worker_enabled = bypass;
1195        self
1196    }
1197
1198    /// Set extra request headers.
1199    pub fn set_extra_headers(
1200        mut self,
1201        headers: Option<std::collections::HashMap<String, String>>,
1202    ) -> Self {
1203        self.extra_headers = headers;
1204        self
1205    }
1206
1207    /// Set whitelist patterns to allow through network interception allowing.
1208    pub fn set_whitelist_patterns(mut self, whitelist_patterns: Option<Vec<String>>) -> Self {
1209        self.whitelist_patterns = whitelist_patterns;
1210        self
1211    }
1212
1213    /// Set blacklist patterns to block through network interception.
1214    pub fn set_blacklist_patterns(mut self, blacklist_patterns: Option<Vec<String>>) -> Self {
1215        self.blacklist_patterns = blacklist_patterns;
1216        self
1217    }
1218
1219    /// Set extra ABP/uBO filter rules for the adblock engine.
1220    /// Pass EasyList/EasyPrivacy content lines for richer blocking coverage.
1221    #[cfg(feature = "adblock")]
1222    pub fn set_adblock_filter_rules(mut self, rules: Vec<String>) -> Self {
1223        self.adblock_filter_rules = Some(rules);
1224        self
1225    }
1226
1227    /// Set the capacity of the channel between browser handle and handler.
1228    /// Defaults to 1000.
1229    pub fn channel_capacity(mut self, capacity: usize) -> Self {
1230        self.channel_capacity = capacity;
1231        self
1232    }
1233
1234    /// Set the number of WebSocket connection retry attempts with exponential backoff.
1235    /// Defaults to 4. Set to 0 for a single attempt with no retries.
1236    pub fn connection_retries(mut self, retries: u32) -> Self {
1237        self.connection_retries = retries;
1238        self
1239    }
1240
1241    /// Build the browser.
1242    pub fn build(self) -> std::result::Result<BrowserConfig, String> {
1243        let executable = if let Some(e) = self.executable {
1244            e
1245        } else {
1246            detection::default_executable(self.executation_detection)?
1247        };
1248
1249        Ok(BrowserConfig {
1250            headless: self.headless,
1251            sandbox: self.sandbox,
1252            window_size: self.window_size,
1253            port: self.port,
1254            executable,
1255            extensions: self.extensions,
1256            process_envs: self.process_envs,
1257            user_data_dir: self.user_data_dir,
1258            incognito: self.incognito,
1259            launch_timeout: self.launch_timeout,
1260            ignore_https_errors: self.ignore_https_errors,
1261            viewport: self.viewport,
1262            request_timeout: self.request_timeout,
1263            args: self.args,
1264            disable_default_args: self.disable_default_args,
1265            request_intercept: self.request_intercept,
1266            cache_enabled: self.cache_enabled,
1267            ignore_visuals: self.ignore_visuals,
1268            ignore_ads: self.ignore_ads,
1269            ignore_javascript: self.ignore_javascript,
1270            ignore_analytics: self.ignore_analytics,
1271            ignore_stylesheets: self.ignore_stylesheets,
1272            ignore_prefetch: self.ignore_prefetch,
1273            extra_headers: self.extra_headers,
1274            only_html: self.only_html,
1275            intercept_manager: self.intercept_manager,
1276            service_worker_enabled: self.service_worker_enabled,
1277            max_bytes_allowed: self.max_bytes_allowed,
1278            whitelist_patterns: self.whitelist_patterns,
1279            blacklist_patterns: self.blacklist_patterns,
1280            #[cfg(feature = "adblock")]
1281            adblock_filter_rules: self.adblock_filter_rules,
1282            channel_capacity: self.channel_capacity,
1283            connection_retries: self.connection_retries,
1284        })
1285    }
1286}
1287
1288impl BrowserConfig {
1289    pub fn launch(&self) -> io::Result<Child> {
1290        let mut cmd = async_process::Command::new(&self.executable);
1291
1292        if self.disable_default_args {
1293            cmd.args(&self.args);
1294        } else {
1295            cmd.args(DEFAULT_ARGS).args(&self.args);
1296        }
1297
1298        if !self
1299            .args
1300            .iter()
1301            .any(|arg| arg.contains("--remote-debugging-port="))
1302        {
1303            cmd.arg(format!("--remote-debugging-port={}", self.port));
1304        }
1305
1306        cmd.args(
1307            self.extensions
1308                .iter()
1309                .map(|e| format!("--load-extension={e}")),
1310        );
1311
1312        if let Some(ref user_data) = self.user_data_dir {
1313            cmd.arg(format!("--user-data-dir={}", user_data.display()));
1314        } else {
1315            // If the user did not specify a data directory, this would default to the systems default
1316            // data directory. In most cases, we would rather have a fresh instance of Chromium. Specify
1317            // a temp dir just for chromiumoxide instead.
1318            cmd.arg(format!(
1319                "--user-data-dir={}",
1320                std::env::temp_dir().join("chromiumoxide-runner").display()
1321            ));
1322        }
1323
1324        if let Some((width, height)) = self.window_size {
1325            cmd.arg(format!("--window-size={width},{height}"));
1326        }
1327
1328        if !self.sandbox {
1329            cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
1330        }
1331
1332        match self.headless {
1333            HeadlessMode::False => (),
1334            HeadlessMode::True => {
1335                cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
1336            }
1337            HeadlessMode::New => {
1338                cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
1339            }
1340        }
1341
1342        if self.incognito {
1343            cmd.arg("--incognito");
1344        }
1345
1346        if let Some(ref envs) = self.process_envs {
1347            cmd.envs(envs);
1348        }
1349        cmd.stderr(Stdio::piped()).spawn()
1350    }
1351}
1352
1353/// Returns the path to Chrome's executable.
1354///
1355/// If the `CHROME` environment variable is set, `default_executable` will
1356/// use it as the default path. Otherwise, the filenames `google-chrome-stable`
1357/// `chromium`, `chromium-browser`, `chrome` and `chrome-browser` are
1358/// searched for in standard places. If that fails,
1359/// `/Applications/Google Chrome.app/...` (on MacOS) or the registry (on
1360/// Windows) is consulted. If all of the above fail, an error is returned.
1361#[deprecated(note = "Use detection::default_executable instead")]
1362pub fn default_executable() -> Result<std::path::PathBuf, String> {
1363    let options = DetectionOptions {
1364        msedge: false,
1365        unstable: false,
1366    };
1367    detection::default_executable(options)
1368}
1369
1370/// These are passed to the Chrome binary by default.
1371/// Via https://github.com/puppeteer/puppeteer/blob/4846b8723cf20d3551c0d755df394cc5e0c82a94/src/node/Launcher.ts#L157
1372static DEFAULT_ARGS: [&str; 26] = [
1373    "--disable-background-networking",
1374    "--enable-features=NetworkService,NetworkServiceInProcess",
1375    "--disable-background-timer-throttling",
1376    "--disable-backgrounding-occluded-windows",
1377    "--disable-breakpad",
1378    "--disable-client-side-phishing-detection",
1379    "--disable-component-extensions-with-background-pages",
1380    "--disable-default-apps",
1381    "--disable-dev-shm-usage",
1382    "--disable-extensions",
1383    "--disable-features=TranslateUI",
1384    "--disable-hang-monitor",
1385    "--disable-ipc-flooding-protection",
1386    "--disable-popup-blocking",
1387    "--disable-prompt-on-repost",
1388    "--disable-renderer-backgrounding",
1389    "--disable-sync",
1390    "--force-color-profile=srgb",
1391    "--metrics-recording-only",
1392    "--no-first-run",
1393    "--enable-automation",
1394    "--password-store=basic",
1395    "--use-mock-keychain",
1396    "--enable-blink-features=IdleDetection",
1397    "--lang=en_US",
1398    "--disable-blink-features=AutomationControlled",
1399];