use hashbrown::HashMap;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use std::future::Future;
use std::time::Duration;
use std::{
io,
path::{Path, PathBuf},
};
use futures::channel::mpsc::{channel, unbounded, Sender};
use futures::channel::oneshot::channel as oneshot_channel;
use futures::select;
use futures::SinkExt;
use crate::async_process::{self, Child, ExitStatus, Stdio};
use crate::cmd::{to_command_response, CommandMessage};
use crate::conn::Connection;
use crate::detection::{self, DetectionOptions};
use crate::error::{BrowserStderr, CdpError, Result};
use crate::handler::browser::BrowserContext;
use crate::handler::viewport::Viewport;
use crate::handler::{Handler, HandlerConfig, HandlerMessage, REQUEST_TIMEOUT};
use crate::listeners::{EventListenerRequest, EventStream};
use crate::page::Page;
use crate::utils;
use chromiumoxide_cdp::cdp::browser_protocol::browser::{
BrowserContextId, CloseReturns, GetVersionParams, GetVersionReturns,
};
use chromiumoxide_cdp::cdp::browser_protocol::network::{Cookie, CookieParam};
use chromiumoxide_cdp::cdp::browser_protocol::storage::{
ClearCookiesParams, GetCookiesParams, SetCookiesParams,
};
use chromiumoxide_cdp::cdp::browser_protocol::target::{
CreateBrowserContextParams, CreateTargetParams, DisposeBrowserContextParams,
GetBrowserContextsParams, GetBrowserContextsReturns, TargetId, TargetInfo,
};
use chromiumoxide_cdp::cdp::{CdpEventMessage, IntoEventKind};
use chromiumoxide_types::*;
use spider_network_blocker::intercept_manager::NetworkInterceptManager;
pub const LAUNCH_TIMEOUT: u64 = 20_000;
lazy_static::lazy_static! {
static ref REQUEST_CLIENT: reqwest::Client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.default_headers({
let mut m = HeaderMap::new();
m.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
m
})
.tcp_keepalive(Some(Duration::from_secs(5)))
.pool_idle_timeout(Some(Duration::from_secs(60)))
.pool_max_idle_per_host(10)
.build()
.expect("client to build");
}
#[derive(Debug)]
pub struct Browser {
pub(crate) sender: Sender<HandlerMessage>,
config: Option<BrowserConfig>,
child: Option<Child>,
debug_ws_url: String,
pub browser_context: BrowserContext,
}
#[derive(serde::Deserialize, Debug, Default)]
pub struct BrowserConnection {
#[serde(rename = "Browser")]
pub browser: String,
#[serde(rename = "Protocol-Version")]
pub protocol_version: String,
#[serde(rename = "User-Agent")]
pub user_agent: String,
#[serde(rename = "V8-Version")]
pub v8_version: String,
#[serde(rename = "WebKit-Version")]
pub webkit_version: String,
#[serde(rename = "webSocketDebuggerUrl")]
pub web_socket_debugger_url: String,
}
impl Browser {
pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
Self::connect_with_config(url, HandlerConfig::default()).await
}
pub async fn connect_with_config(
url: impl Into<String>,
config: HandlerConfig,
) -> Result<(Self, Handler)> {
let mut debug_ws_url = url.into();
if debug_ws_url.starts_with("http") {
match REQUEST_CLIENT
.get(
if debug_ws_url.ends_with("/json/version")
|| debug_ws_url.ends_with("/json/version/")
{
debug_ws_url.to_owned()
} else {
format!(
"{}{}json/version",
&debug_ws_url,
if debug_ws_url.ends_with('/') { "" } else { "/" }
)
},
)
.send()
.await
{
Ok(req) => {
let connection: BrowserConnection =
crate::serde_json::from_slice(&req.bytes().await.unwrap_or_default())
.unwrap_or_default();
if !connection.web_socket_debugger_url.is_empty() {
debug_ws_url = connection.web_socket_debugger_url;
}
}
Err(_) => return Err(CdpError::NoResponse),
}
}
let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
let (tx, rx) = channel(1000);
let handler_config = BrowserConfig {
ignore_https_errors: config.ignore_https_errors,
viewport: config.viewport.clone(),
request_timeout: config.request_timeout,
request_intercept: config.request_intercept,
cache_enabled: config.cache_enabled,
ignore_visuals: config.ignore_visuals,
ignore_stylesheets: config.ignore_stylesheets,
ignore_javascript: config.ignore_javascript,
ignore_analytics: config.ignore_analytics,
ignore_ads: config.ignore_ads,
extra_headers: config.extra_headers.clone(),
only_html: config.only_html,
service_worker_enabled: config.service_worker_enabled,
intercept_manager: config.intercept_manager,
max_bytes_allowed: config.max_bytes_allowed,
..Default::default()
};
let fut = Handler::new(conn, rx, config);
let browser_context = fut.default_browser_context().clone();
let browser = Self {
sender: tx,
config: Some(handler_config),
child: None,
debug_ws_url,
browser_context,
};
Ok((browser, fut))
}
pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
config.executable = utils::canonicalize_except_snap(config.executable).await?;
let mut child = config.launch()?;
async fn with_child(
config: &BrowserConfig,
child: &mut Child,
) -> Result<(String, Connection<CdpEventMessage>)> {
let dur = config.launch_timeout;
let timeout_fut = Box::pin(tokio::time::sleep(dur));
let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
Ok((debug_ws_url, conn))
}
let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
Ok(conn) => conn,
Err(e) => {
if let Ok(Some(_)) = child.try_wait() {
} else {
child.kill().await.expect("`Browser::launch` failed but could not clean-up the child process (`kill`)");
child.wait().await.expect("`Browser::launch` failed but could not clean-up the child process (`wait`)");
}
return Err(e);
}
};
let (tx, rx) = channel(1000);
let handler_config = HandlerConfig {
ignore_https_errors: config.ignore_https_errors,
viewport: config.viewport.clone(),
context_ids: Vec::new(),
request_timeout: config.request_timeout,
request_intercept: config.request_intercept,
cache_enabled: config.cache_enabled,
ignore_visuals: config.ignore_visuals,
ignore_stylesheets: config.ignore_stylesheets,
ignore_javascript: config.ignore_javascript,
ignore_analytics: config.ignore_analytics,
ignore_ads: config.ignore_ads,
extra_headers: config.extra_headers.clone(),
only_html: config.only_html,
service_worker_enabled: config.service_worker_enabled,
created_first_target: false,
intercept_manager: config.intercept_manager,
max_bytes_allowed: config.max_bytes_allowed,
};
let fut = Handler::new(conn, rx, handler_config);
let browser_context = fut.default_browser_context().clone();
let browser = Self {
sender: tx,
config: Some(config),
child: Some(child),
debug_ws_url,
browser_context,
};
Ok((browser, fut))
}
pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
let (tx, rx) = oneshot_channel();
self.sender
.clone()
.send(HandlerMessage::FetchTargets(tx))
.await?;
rx.await?
}
pub async fn close(&self) -> Result<CloseReturns> {
let (tx, rx) = oneshot_channel();
self.sender
.clone()
.send(HandlerMessage::CloseBrowser(tx))
.await?;
rx.await?
}
pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
if let Some(child) = self.child.as_mut() {
Ok(Some(child.wait().await?))
} else {
Ok(None)
}
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
if let Some(child) = self.child.as_mut() {
child.try_wait()
} else {
Ok(None)
}
}
pub fn get_mut_child(&mut self) -> Option<&mut Child> {
self.child.as_mut()
}
pub fn has_child(&self) -> bool {
self.child.is_some()
}
pub async fn kill(&mut self) -> Option<io::Result<()>> {
match self.child.as_mut() {
Some(child) => Some(child.kill().await),
None => None,
}
}
pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
if !self.is_incognito_configured() {
let browser_context_id = self
.create_browser_context(CreateBrowserContextParams::default())
.await?;
self.browser_context = BrowserContext::from(browser_context_id);
self.sender
.clone()
.send(HandlerMessage::InsertContext(self.browser_context.clone()))
.await?;
}
Ok(self)
}
pub async fn quit_incognito_context_base(
&self,
browser_context_id: BrowserContextId,
) -> Result<&Self> {
self.dispose_browser_context(browser_context_id.clone())
.await?;
self.sender
.clone()
.send(HandlerMessage::DisposeContext(BrowserContext::from(
browser_context_id,
)))
.await?;
Ok(self)
}
pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
if let Some(id) = self.browser_context.take() {
let _ = self.quit_incognito_context_base(id).await;
}
Ok(self)
}
fn is_incognito_configured(&self) -> bool {
self.config
.as_ref()
.map(|c| c.incognito)
.unwrap_or_default()
}
pub fn websocket_address(&self) -> &String {
&self.debug_ws_url
}
pub fn is_incognito(&self) -> bool {
self.is_incognito_configured() || self.browser_context.is_incognito()
}
pub fn config(&self) -> Option<&BrowserConfig> {
self.config.as_ref()
}
pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
let (tx, rx) = oneshot_channel();
let mut params = params.into();
if let Some(id) = self.browser_context.id() {
if params.browser_context_id.is_none() {
params.browser_context_id = Some(id.clone());
}
}
let _ = self
.sender
.clone()
.send(HandlerMessage::CreatePage(params, tx))
.await;
rx.await?
}
pub async fn version(&self) -> Result<GetVersionReturns> {
Ok(self.execute(GetVersionParams::default()).await?.result)
}
pub async fn user_agent(&self) -> Result<String> {
Ok(self.version().await?.user_agent)
}
pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
let (tx, rx) = oneshot_channel();
let method = cmd.identifier();
let msg = CommandMessage::new(cmd, tx)?;
self.sender
.clone()
.send(HandlerMessage::Command(msg))
.await?;
let resp = rx.await??;
to_command_response::<T>(resp, method)
}
pub async fn pages(&self) -> Result<Vec<Page>> {
let (tx, rx) = oneshot_channel();
self.sender
.clone()
.send(HandlerMessage::GetPages(tx))
.await?;
Ok(rx.await?)
}
pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
let (tx, rx) = oneshot_channel();
self.sender
.clone()
.send(HandlerMessage::GetPage(target_id, tx))
.await?;
rx.await?.ok_or(CdpError::NotFound)
}
pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
let (tx, rx) = unbounded();
self.sender
.clone()
.send(HandlerMessage::AddEventListener(
EventListenerRequest::new::<T>(tx),
))
.await?;
Ok(EventStream::new(rx))
}
pub async fn create_browser_context(
&mut self,
params: CreateBrowserContextParams,
) -> Result<BrowserContextId> {
let response = self.execute(params).await?;
Ok(response.result.browser_context_id)
}
pub async fn get_browser_contexts(
&mut self,
params: GetBrowserContextsParams,
) -> Result<GetBrowserContextsReturns> {
let response = self.execute(params).await?;
Ok(response.result)
}
pub async fn send_new_context(&mut self, browser_context_id: BrowserContextId) -> Result<()> {
self.browser_context = BrowserContext::from(browser_context_id);
self.sender
.clone()
.send(HandlerMessage::InsertContext(self.browser_context.clone()))
.await?;
Ok(())
}
pub async fn dispose_browser_context(
&self,
browser_context_id: impl Into<BrowserContextId>,
) -> Result<()> {
self.execute(DisposeBrowserContextParams::new(browser_context_id))
.await?;
Ok(())
}
pub async fn clear_cookies(&self) -> Result<()> {
self.execute(ClearCookiesParams::default()).await?;
Ok(())
}
pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
let mut cmd = GetCookiesParams::default();
cmd.browser_context_id = self.browser_context.id.clone();
Ok(self.execute(cmd).await?.result.cookies)
}
pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
for cookie in &mut cookies {
if let Some(url) = cookie.url.as_ref() {
crate::page::validate_cookie_url(url)?;
}
}
let mut cookies_param = SetCookiesParams::new(cookies);
cookies_param.browser_context_id = self.browser_context.id.clone();
self.execute(cookies_param).await?;
Ok(self)
}
}
impl Drop for Browser {
fn drop(&mut self) {
if let Some(child) = self.child.as_mut() {
if let Ok(Some(_)) = child.try_wait() {
} else {
tracing::warn!("Browser was not closed manually, it will be killed automatically in the background");
}
}
}
}
async fn ws_url_from_output(
child_process: &mut Child,
timeout_fut: impl Future<Output = ()> + Unpin,
) -> Result<String> {
use futures::{AsyncBufReadExt, FutureExt};
let mut timeout_fut = timeout_fut.fuse();
let stderr = child_process.stderr.take().expect("no stderror");
let mut stderr_bytes = Vec::<u8>::new();
let mut exit_status_fut = Box::pin(child_process.wait()).fuse();
let mut buf = futures::io::BufReader::new(stderr);
loop {
select! {
_ = timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
exit_status = exit_status_fut => {
return Err(match exit_status {
Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
})
},
read_res = buf.read_until(b'\n', &mut stderr_bytes).fuse() => {
match read_res {
Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
Ok(byte_count) => {
if byte_count == 0 {
let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
}
let start_offset = stderr_bytes.len() - byte_count;
let new_bytes = &stderr_bytes[start_offset..];
match std::str::from_utf8(new_bytes) {
Err(_) => {
let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
}
Ok(line) => {
if let Some((_, ws)) = line.rsplit_once("listening on ") {
if ws.starts_with("ws") && ws.contains("devtools/browser") {
return Ok(ws.trim().to_string());
}
}
}
}
}
}
}
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum HeadlessMode {
False,
#[default]
True,
New,
}
#[derive(Debug, Clone, Default)]
pub struct BrowserConfig {
headless: HeadlessMode,
sandbox: bool,
window_size: Option<(u32, u32)>,
port: u16,
executable: std::path::PathBuf,
extensions: Vec<String>,
pub process_envs: Option<HashMap<String, String>>,
pub user_data_dir: Option<PathBuf>,
incognito: bool,
launch_timeout: Duration,
ignore_https_errors: bool,
pub viewport: Option<Viewport>,
request_timeout: Duration,
args: Vec<String>,
disable_default_args: bool,
pub request_intercept: bool,
pub cache_enabled: bool,
pub service_worker_enabled: bool,
pub ignore_visuals: bool,
pub ignore_stylesheets: bool,
pub ignore_javascript: bool,
pub ignore_analytics: bool,
pub ignore_ads: bool,
pub extra_headers: Option<std::collections::HashMap<String, String>>,
pub only_html: bool,
pub intercept_manager: NetworkInterceptManager,
pub max_bytes_allowed: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct BrowserConfigBuilder {
headless: HeadlessMode,
sandbox: bool,
window_size: Option<(u32, u32)>,
port: u16,
executable: Option<PathBuf>,
executation_detection: DetectionOptions,
extensions: Vec<String>,
process_envs: Option<HashMap<String, String>>,
user_data_dir: Option<PathBuf>,
incognito: bool,
launch_timeout: Duration,
ignore_https_errors: bool,
viewport: Option<Viewport>,
request_timeout: Duration,
args: Vec<String>,
disable_default_args: bool,
request_intercept: bool,
cache_enabled: bool,
service_worker_enabled: bool,
ignore_visuals: bool,
ignore_ads: bool,
ignore_javascript: bool,
ignore_stylesheets: bool,
ignore_analytics: bool,
only_html: bool,
extra_headers: Option<std::collections::HashMap<String, String>>,
intercept_manager: NetworkInterceptManager,
max_bytes_allowed: Option<u64>,
}
impl BrowserConfig {
pub fn builder() -> BrowserConfigBuilder {
BrowserConfigBuilder::default()
}
pub fn with_executable(path: impl AsRef<Path>) -> Self {
Self::builder()
.chrome_executable(path)
.build()
.expect("path to executable exist")
}
}
impl Default for BrowserConfigBuilder {
fn default() -> Self {
Self {
headless: HeadlessMode::True,
sandbox: true,
window_size: None,
port: 0,
executable: None,
executation_detection: DetectionOptions::default(),
extensions: Vec::new(),
process_envs: None,
user_data_dir: None,
incognito: false,
launch_timeout: Duration::from_millis(LAUNCH_TIMEOUT),
ignore_https_errors: true,
viewport: Some(Default::default()),
request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
args: Vec::new(),
disable_default_args: false,
request_intercept: false,
cache_enabled: true,
ignore_visuals: false,
ignore_ads: false,
ignore_javascript: false,
ignore_analytics: false,
ignore_stylesheets: false,
only_html: false,
extra_headers: Default::default(),
service_worker_enabled: true,
intercept_manager: NetworkInterceptManager::Unknown,
max_bytes_allowed: None,
}
}
}
impl BrowserConfigBuilder {
pub fn window_size(mut self, width: u32, height: u32) -> Self {
self.window_size = Some((width, height));
self
}
pub fn no_sandbox(mut self) -> Self {
self.sandbox = false;
self
}
pub fn with_head(mut self) -> Self {
self.headless = HeadlessMode::False;
self
}
pub fn new_headless_mode(mut self) -> Self {
self.headless = HeadlessMode::New;
self
}
pub fn headless_mode(mut self, mode: HeadlessMode) -> Self {
self.headless = mode;
self
}
pub fn incognito(mut self) -> Self {
self.incognito = true;
self
}
pub fn respect_https_errors(mut self) -> Self {
self.ignore_https_errors = false;
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn with_max_bytes_allowed(mut self, max_bytes_allowed: Option<u64>) -> Self {
self.max_bytes_allowed = max_bytes_allowed;
self
}
pub fn launch_timeout(mut self, timeout: Duration) -> Self {
self.launch_timeout = timeout;
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = timeout;
self
}
pub fn viewport(mut self, viewport: impl Into<Option<Viewport>>) -> Self {
self.viewport = viewport.into();
self
}
pub fn user_data_dir(mut self, data_dir: impl AsRef<Path>) -> Self {
self.user_data_dir = Some(data_dir.as_ref().to_path_buf());
self
}
pub fn chrome_executable(mut self, path: impl AsRef<Path>) -> Self {
self.executable = Some(path.as_ref().to_path_buf());
self
}
pub fn chrome_detection(mut self, options: DetectionOptions) -> Self {
self.executation_detection = options;
self
}
pub fn extension(mut self, extension: impl Into<String>) -> Self {
self.extensions.push(extension.into());
self
}
pub fn extensions<I, S>(mut self, extensions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for ext in extensions {
self.extensions.push(ext.into());
}
self
}
pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
self.process_envs
.get_or_insert(HashMap::new())
.insert(key.into(), val.into());
self
}
pub fn envs<I, K, V>(mut self, envs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
self.process_envs
.get_or_insert(HashMap::new())
.extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for arg in args {
self.args.push(arg.into());
}
self
}
pub fn disable_default_args(mut self) -> Self {
self.disable_default_args = true;
self
}
pub fn enable_request_intercept(mut self) -> Self {
self.request_intercept = true;
self
}
pub fn disable_request_intercept(mut self) -> Self {
self.request_intercept = false;
self
}
pub fn enable_cache(mut self) -> Self {
self.cache_enabled = true;
self
}
pub fn disable_cache(mut self) -> Self {
self.cache_enabled = false;
self
}
pub fn set_service_worker_enabled(mut self, bypass: bool) -> Self {
self.service_worker_enabled = bypass;
self
}
pub fn set_extra_headers(
mut self,
headers: Option<std::collections::HashMap<String, String>>,
) -> Self {
self.extra_headers = headers;
self
}
pub fn build(self) -> std::result::Result<BrowserConfig, String> {
let executable = if let Some(e) = self.executable {
e
} else {
detection::default_executable(self.executation_detection)?
};
Ok(BrowserConfig {
headless: self.headless,
sandbox: self.sandbox,
window_size: self.window_size,
port: self.port,
executable,
extensions: self.extensions,
process_envs: self.process_envs,
user_data_dir: self.user_data_dir,
incognito: self.incognito,
launch_timeout: self.launch_timeout,
ignore_https_errors: self.ignore_https_errors,
viewport: self.viewport,
request_timeout: self.request_timeout,
args: self.args,
disable_default_args: self.disable_default_args,
request_intercept: self.request_intercept,
cache_enabled: self.cache_enabled,
ignore_visuals: self.ignore_visuals,
ignore_ads: self.ignore_ads,
ignore_javascript: self.ignore_javascript,
ignore_analytics: self.ignore_analytics,
ignore_stylesheets: self.ignore_stylesheets,
extra_headers: self.extra_headers,
only_html: self.only_html,
intercept_manager: self.intercept_manager,
service_worker_enabled: self.service_worker_enabled,
max_bytes_allowed: self.max_bytes_allowed,
})
}
}
impl BrowserConfig {
pub fn launch(&self) -> io::Result<Child> {
let mut cmd = async_process::Command::new(&self.executable);
if self.disable_default_args {
cmd.args(&self.args);
} else {
cmd.args(DEFAULT_ARGS).args(&self.args);
}
if !self
.args
.iter()
.any(|arg| arg.contains("--remote-debugging-port="))
{
cmd.arg(format!("--remote-debugging-port={}", self.port));
}
cmd.args(
self.extensions
.iter()
.map(|e| format!("--load-extension={e}")),
);
if let Some(ref user_data) = self.user_data_dir {
cmd.arg(format!("--user-data-dir={}", user_data.display()));
} else {
cmd.arg(format!(
"--user-data-dir={}",
std::env::temp_dir().join("chromiumoxide-runner").display()
));
}
if let Some((width, height)) = self.window_size {
cmd.arg(format!("--window-size={width},{height}"));
}
if !self.sandbox {
cmd.args(["--no-sandbox", "--disable-setuid-sandbox"]);
}
match self.headless {
HeadlessMode::False => (),
HeadlessMode::True => {
cmd.args(["--headless", "--hide-scrollbars", "--mute-audio"]);
}
HeadlessMode::New => {
cmd.args(["--headless=new", "--hide-scrollbars", "--mute-audio"]);
}
}
if self.incognito {
cmd.arg("--incognito");
}
if let Some(ref envs) = self.process_envs {
cmd.envs(envs);
}
cmd.stderr(Stdio::piped()).spawn()
}
}
#[deprecated(note = "Use detection::default_executable instead")]
pub fn default_executable() -> Result<std::path::PathBuf, String> {
let options = DetectionOptions {
msedge: false,
unstable: false,
};
detection::default_executable(options)
}
static DEFAULT_ARGS: [&str; 26] = [
"--disable-background-networking",
"--enable-features=NetworkService,NetworkServiceInProcess",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-breakpad",
"--disable-client-side-phishing-detection",
"--disable-component-extensions-with-background-pages",
"--disable-default-apps",
"--disable-dev-shm-usage",
"--disable-extensions",
"--disable-features=TranslateUI",
"--disable-hang-monitor",
"--disable-ipc-flooding-protection",
"--disable-popup-blocking",
"--disable-prompt-on-repost",
"--disable-renderer-backgrounding",
"--disable-sync",
"--force-color-profile=srgb",
"--metrics-recording-only",
"--no-first-run",
"--enable-automation",
"--password-store=basic",
"--use-mock-keychain",
"--enable-blink-features=IdleDetection",
"--lang=en_US",
"--disable-blink-features=AutomationControlled",
];