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