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