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