use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use serde_json::Value;
use crate::browser::browser_process::{BrowserCloser, BrowserProcess};
use crate::browser::connector::{
connect_browser_with, refuse_unappliable_fingerprint, AttachSettings,
};
use crate::browser::engine_launch::launch_with_engine;
use crate::browser::launch_executable::DefaultLaunchHooks;
use crate::browser::media::ColorScheme;
use crate::browser::real_browser::{launch_real_browser_with, RealBrowserOptions};
use crate::browser::restrictions::{merge_feature_switches, resolve_restrictions};
use crate::browser::storage_state::StorageStateInput;
use crate::core::engine::{EngineAdapter, EngineType};
use crate::downloads::{normalize_download_options, supported_engine};
use crate::downloads::{DownloadManager, DownloadSetting};
use crate::fingerprint::automation_parity::{
apply_automation_parity_args, parity_ignored_default_args,
};
use crate::fingerprint::profile::FingerprintProfile;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum LaunchMode {
#[default]
Real,
Engine,
}
pub const LAUNCH_MODES: [LaunchMode; 2] = [LaunchMode::Real, LaunchMode::Engine];
impl LaunchMode {
pub fn as_str(&self) -> &'static str {
match self {
Self::Real => "real",
Self::Engine => "engine",
}
}
}
impl std::fmt::Display for LaunchMode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl std::str::FromStr for LaunchMode {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
LAUNCH_MODES
.into_iter()
.find(|mode| mode.as_str() == value)
.ok_or_else(|| {
anyhow::anyhow!("Invalid launch mode: {value}. Expected 'real' or 'engine'")
})
}
}
#[derive(Debug, Clone)]
pub struct LaunchOptions {
pub engine: EngineType,
pub webdriver: super::webdriver::WebDriverOptions,
pub launch: LaunchMode,
pub user_data_dir: Option<PathBuf>,
pub default_browser_check: Option<bool>,
pub first_run: bool,
pub preferences: Value,
pub local_state: Value,
pub storage_state: Option<StorageStateInput>,
pub headless: bool,
pub slow_mo: u64,
pub verbose: bool,
pub restrictions: Vec<String>,
pub args: Vec<String>,
pub extra_args: Vec<String>,
pub ignore_default_args: Vec<String>,
pub ignore_all_default_args: bool,
pub env: Option<HashMap<String, String>>,
pub channel: Option<String>,
pub executable_path: Option<PathBuf>,
pub remote_debugging_port: Option<u16>,
pub color_scheme: Option<ColorScheme>,
pub launch_timeout: Option<Duration>,
pub sandbox: bool,
pub node_executable: Option<PathBuf>,
pub node_working_dir: Option<PathBuf>,
pub automation_parity: bool,
pub fingerprint: Option<FingerprintProfile>,
pub downloads: DownloadSetting,
}
impl Default for LaunchOptions {
fn default() -> Self {
Self {
engine: EngineType::Chromiumoxide,
webdriver: Default::default(),
launch: LaunchMode::Real,
user_data_dir: None,
default_browser_check: None,
first_run: false,
preferences: serde_json::json!({}),
local_state: serde_json::json!({}),
storage_state: None,
headless: false,
slow_mo: 0,
verbose: false,
restrictions: Vec::new(),
args: Vec::new(),
extra_args: Vec::new(),
ignore_default_args: Vec::new(),
ignore_all_default_args: false,
env: None,
channel: None,
executable_path: None,
remote_debugging_port: None,
color_scheme: None,
launch_timeout: None,
sandbox: true,
node_executable: None,
node_working_dir: None,
automation_parity: true,
fingerprint: None,
downloads: DownloadSetting::Off,
}
}
}
impl LaunchOptions {
pub fn engine(mut self, engine: EngineType) -> Self {
self.engine = engine;
if engine == EngineType::Fantoccini {
self.launch = LaunchMode::Engine;
}
self
}
pub fn storage_state(mut self, state: impl Into<StorageStateInput>) -> Self {
self.storage_state = Some(state.into());
self
}
pub fn chromiumoxide() -> Self {
Self::default().engine(EngineType::Chromiumoxide)
}
pub fn fantoccini() -> Self {
Self::default().engine(EngineType::Fantoccini)
}
pub fn playwright() -> Self {
Self::default().engine(EngineType::Playwright)
}
pub fn puppeteer() -> Self {
Self::default().engine(EngineType::Puppeteer)
}
pub fn launch(mut self, launch: LaunchMode) -> Self {
self.launch = launch;
self
}
pub fn headless(mut self, headless: bool) -> Self {
self.headless = headless;
self
}
pub fn user_data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.user_data_dir = Some(dir.into());
self
}
pub fn slow_mo(mut self, ms: u64) -> Self {
self.slow_mo = ms;
self
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn restrictions<I, S>(mut self, restrictions: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.restrictions = restrictions.into_iter().map(Into::into).collect();
self
}
pub fn with_args(mut self, args: Vec<String>) -> Self {
self.args = args;
self
}
pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
self.extra_args = args;
self
}
pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
self.ignore_default_args = args;
self
}
pub fn ignore_all_default_args(mut self) -> Self {
self.ignore_all_default_args = true;
self
}
pub fn env(mut self, env: HashMap<String, String>) -> Self {
self.env = Some(env);
self
}
pub fn channel(mut self, channel: impl Into<String>) -> Self {
self.channel = Some(channel.into());
self
}
pub fn executable_path(mut self, executable_path: impl Into<PathBuf>) -> Self {
self.executable_path = Some(executable_path.into());
self
}
pub fn remote_debugging_port(mut self, port: u16) -> Self {
self.remote_debugging_port = Some(port);
self
}
pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
self.color_scheme = Some(color_scheme);
self
}
pub fn launch_timeout(mut self, timeout: Duration) -> Self {
self.launch_timeout = Some(timeout);
self
}
pub fn sandbox(mut self, sandbox: bool) -> Self {
self.sandbox = sandbox;
self
}
pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
self.node_executable = Some(executable.into());
self
}
pub fn node_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.node_working_dir = Some(dir.into());
self
}
pub fn automation_parity(mut self, automation_parity: bool) -> Self {
self.automation_parity = automation_parity;
self
}
pub fn fingerprint(mut self, fingerprint: FingerprintProfile) -> Self {
self.fingerprint = Some(fingerprint);
self
}
pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
self.downloads = downloads.into();
self
}
pub fn all_chrome_args(&self) -> anyhow::Result<Vec<String>> {
let mut args = resolve_restrictions(&self.restrictions)?.args;
args.extend(self.args.iter().cloned());
args.extend(self.extra_args.iter().cloned());
let args = merge_feature_switches(&args);
Ok(if self.automation_parity {
apply_automation_parity_args(&args)
} else {
args
})
}
pub fn all_ignored_default_args(&self) -> Vec<String> {
let mut ignored = if self.automation_parity {
parity_ignored_default_args(self.engine, self.headless)
} else {
Vec::new()
};
for argument in &self.ignore_default_args {
if !ignored.contains(argument) {
ignored.push(argument.clone());
}
}
ignored
}
pub(crate) fn browser_env(&self) -> anyhow::Result<Option<HashMap<String, String>>> {
let mut env = resolve_restrictions(&self.restrictions)?.env;
if let Some(extra) = &self.env {
env.extend(
extra
.iter()
.map(|(key, value)| (key.clone(), value.clone())),
);
}
Ok((!env.is_empty() || self.env.is_some()).then_some(env))
}
#[deprecated(
since = "0.13.0",
note = "launch_browser uses a fresh temporary profile unless user_data_dir is set; read LaunchResult::browser.user_data_dir"
)]
pub fn get_user_data_dir(&self) -> PathBuf {
if let Some(ref dir) = self.user_data_dir {
dir.clone()
} else {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join(".browser-commander")
.join(format!("{}-data", self.engine))
}
}
pub(crate) fn real_browser_options(&self) -> RealBrowserOptions {
let defaults = RealBrowserOptions::default();
let mut extra_args = self.extra_args.clone();
if !self.sandbox
&& !self
.args
.iter()
.chain(&extra_args)
.any(|a| a == "--no-sandbox")
{
extra_args.push("--no-sandbox".to_string());
}
RealBrowserOptions {
engine: self.engine,
channel: self.channel.clone().unwrap_or(defaults.channel.clone()),
executable_path: self.executable_path.clone(),
user_data_dir: self.user_data_dir.clone(),
default_browser_check: self.default_browser_check,
first_run: self.first_run,
preferences: self.preferences.clone(),
local_state: self.local_state.clone(),
storage_state: self.storage_state.clone(),
remote_debugging_port: self.remote_debugging_port,
headless: self.headless,
restrictions: self.restrictions.clone(),
args: self.args.clone(),
extra_args,
env: self.env.clone(),
automation_parity: self.automation_parity,
startup_timeout: self.launch_timeout.unwrap_or(defaults.startup_timeout),
slow_mo: self.slow_mo,
verbose: self.verbose,
node_executable: self.node_executable.clone(),
node_working_dir: self.node_working_dir.clone(),
downloads: self.downloads.clone(),
..defaults
}
}
}
#[derive(Debug, Clone)]
pub struct Browser {
pub engine: EngineType,
pub user_data_dir: PathBuf,
pub headless: bool,
}
pub struct LaunchResult {
pub browser: Browser,
pub page: Arc<dyn EngineAdapter>,
pub downloads: Option<Arc<DownloadManager>>,
pub launch: Option<LaunchMode>,
pub temporary_profile: bool,
pub args: Vec<String>,
pub cdp_endpoint: Option<String>,
pub remote_debugging_port: Option<u16>,
pub executable_path: Option<PathBuf>,
pub browser_process: Option<BrowserProcess>,
closer: Option<Arc<dyn BrowserCloser>>,
}
impl LaunchResult {
pub(crate) fn attached(
browser: Browser,
page: Arc<dyn EngineAdapter>,
downloads: Option<Arc<DownloadManager>>,
) -> Self {
Self {
browser,
page,
downloads,
launch: None,
temporary_profile: false,
args: Vec::new(),
cdp_endpoint: None,
remote_debugging_port: None,
executable_path: None,
browser_process: None,
closer: None,
}
}
pub async fn close(&self) -> anyhow::Result<()> {
match &self.closer {
Some(closer) => closer.close().await,
None => Ok(()),
}
}
}
impl std::fmt::Debug for LaunchResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LaunchResult")
.field("browser", &self.browser)
.field("page", &"<dyn EngineAdapter>")
.field("downloads", &self.downloads)
.field("launch", &self.launch)
.field("temporary_profile", &self.temporary_profile)
.field("args", &self.args)
.field("cdp_endpoint", &self.cdp_endpoint)
.field("remote_debugging_port", &self.remote_debugging_port)
.field("executable_path", &self.executable_path)
.field("browser_process", &self.browser_process)
.finish()
}
}
pub async fn launch_browser(options: LaunchOptions) -> Result<LaunchResult, anyhow::Error> {
if options.engine == EngineType::Fantoccini && options.launch == LaunchMode::Real {
return Err(anyhow::anyhow!(
"fantoccini requires LaunchMode::Engine; use LaunchOptions::fantoccini()"
));
}
options.all_chrome_args()?;
if let Some(state) = &options.storage_state {
state.load()?;
}
refuse_unappliable_fingerprint(options.engine, options.fingerprint.as_ref())?;
normalize_download_options(options.downloads.clone())
.map(|_| supported_engine(options.engine))
.transpose()
.map_err(|error| anyhow::anyhow!("{error}"))?;
if options.verbose {
tracing::info!(
"Launching browser with {} engine ({})...",
options.engine,
options.launch
);
}
let result = match options.launch {
LaunchMode::Real => launch_real(&options).await?,
LaunchMode::Engine => launch_with_engine(&options).await?,
};
if options.verbose {
tracing::info!("Browser launched with {} engine", options.engine);
}
Ok(result)
}
async fn launch_real(options: &LaunchOptions) -> Result<LaunchResult, anyhow::Error> {
let real = options.real_browser_options();
let hooks = Arc::new(DefaultLaunchHooks {
explicit_selection: options.channel.is_some() || options.executable_path.is_some(),
});
let settings = AttachSettings {
fingerprint: options.fingerprint.as_ref(),
color_scheme: options.color_scheme.as_ref(),
};
let (mut result, launched) = launch_real_browser_with(&real, hooks, |connect| {
connect_browser_with(connect, settings)
})
.await?;
if !options.headless {
if let Err(error) = result.page.bring_to_front().await {
if options.verbose {
tracing::debug!(%error, "bring_to_front failed");
}
}
}
result.browser.user_data_dir = launched.user_data_dir;
result.browser.headless = options.headless;
result.launch = Some(LaunchMode::Real);
result.temporary_profile = launched.temporary_profile;
result.args = launched.args;
result.cdp_endpoint = Some(launched.cdp_endpoint);
result.remote_debugging_port = Some(launched.remote_debugging_port);
result.executable_path = Some(launched.executable_path);
result.browser_process = Some(launched.browser_process);
result.closer = Some(launched.closer as Arc<dyn BrowserCloser>);
Ok(result)
}
impl LaunchResult {
pub(crate) fn launched_by_engine(
mut self,
args: Vec<String>,
temporary_profile: bool,
executable_path: Option<PathBuf>,
closer: Arc<dyn BrowserCloser>,
) -> Self {
self.launch = Some(LaunchMode::Engine);
self.args = args;
self.temporary_profile = temporary_profile;
self.executable_path = executable_path;
self.closer = Some(closer);
self
}
}
#[cfg(test)]
#[path = "launcher_tests.rs"]
mod tests;