Skip to main content

chromiumoxide/
browser.rs

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