1use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12use std::time::Duration;
13
14use serde_json::Value;
15
16use crate::browser::browser_process::{BrowserCloser, BrowserProcess};
17use crate::browser::connector::{
18 connect_browser_with, refuse_unappliable_fingerprint, AttachSettings,
19};
20use crate::browser::engine_launch::launch_with_engine;
21use crate::browser::launch_executable::DefaultLaunchHooks;
22use crate::browser::media::ColorScheme;
23use crate::browser::real_browser::{launch_real_browser_with, RealBrowserOptions};
24use crate::browser::restrictions::{merge_feature_switches, resolve_restrictions};
25use crate::browser::storage_state::StorageStateInput;
26use crate::core::engine::{EngineAdapter, EngineType};
27use crate::downloads::{normalize_download_options, supported_engine};
28use crate::downloads::{DownloadManager, DownloadSetting};
29use crate::fingerprint::automation_parity::{
30 apply_automation_parity_args, parity_ignored_default_args,
31};
32use crate::fingerprint::profile::FingerprintProfile;
33
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
36pub enum LaunchMode {
37 #[default]
41 Real,
42 Engine,
45}
46
47pub const LAUNCH_MODES: [LaunchMode; 2] = [LaunchMode::Real, LaunchMode::Engine];
49
50impl LaunchMode {
51 pub fn as_str(&self) -> &'static str {
53 match self {
54 Self::Real => "real",
55 Self::Engine => "engine",
56 }
57 }
58}
59
60impl std::fmt::Display for LaunchMode {
61 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 formatter.write_str(self.as_str())
63 }
64}
65
66impl std::str::FromStr for LaunchMode {
67 type Err = anyhow::Error;
68
69 fn from_str(value: &str) -> Result<Self, Self::Err> {
70 LAUNCH_MODES
71 .into_iter()
72 .find(|mode| mode.as_str() == value)
73 .ok_or_else(|| {
74 anyhow::anyhow!("Invalid launch mode: {value}. Expected 'real' or 'engine'")
75 })
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct LaunchOptions {
82 pub engine: EngineType,
84 pub webdriver: super::webdriver::WebDriverOptions,
87 pub launch: LaunchMode,
89 pub user_data_dir: Option<PathBuf>,
92 pub default_browser_check: Option<bool>,
94 pub first_run: bool,
96 pub preferences: Value,
98 pub local_state: Value,
100 pub storage_state: Option<StorageStateInput>,
102 pub headless: bool,
104 pub slow_mo: u64,
106 pub verbose: bool,
108 pub restrictions: Vec<String>,
111 pub args: Vec<String>,
113 pub extra_args: Vec<String>,
115 pub ignore_default_args: Vec<String>,
117 pub ignore_all_default_args: bool,
119 pub env: Option<HashMap<String, String>>,
122 pub channel: Option<String>,
125 pub executable_path: Option<PathBuf>,
127 pub remote_debugging_port: Option<u16>,
129 pub color_scheme: Option<ColorScheme>,
131 pub launch_timeout: Option<Duration>,
133 pub sandbox: bool,
139 pub node_executable: Option<PathBuf>,
141 pub node_working_dir: Option<PathBuf>,
143 pub automation_parity: bool,
149 pub fingerprint: Option<FingerprintProfile>,
157 pub downloads: DownloadSetting,
164}
165
166impl Default for LaunchOptions {
167 fn default() -> Self {
168 Self {
169 engine: EngineType::Chromiumoxide,
170 webdriver: Default::default(),
171 launch: LaunchMode::Real,
172 user_data_dir: None,
173 default_browser_check: None,
174 first_run: false,
175 preferences: serde_json::json!({}),
176 local_state: serde_json::json!({}),
177 storage_state: None,
178 headless: false,
179 slow_mo: 0,
180 verbose: false,
181 restrictions: Vec::new(),
182 args: Vec::new(),
183 extra_args: Vec::new(),
184 ignore_default_args: Vec::new(),
185 ignore_all_default_args: false,
186 env: None,
187 channel: None,
188 executable_path: None,
189 remote_debugging_port: None,
190 color_scheme: None,
191 launch_timeout: None,
192 sandbox: true,
193 node_executable: None,
194 node_working_dir: None,
195 automation_parity: true,
196 fingerprint: None,
197 downloads: DownloadSetting::Off,
198 }
199 }
200}
201
202impl LaunchOptions {
203 pub fn engine(mut self, engine: EngineType) -> Self {
205 self.engine = engine;
206 if engine == EngineType::Fantoccini {
207 self.launch = LaunchMode::Engine;
208 }
209 self
210 }
211
212 pub fn storage_state(mut self, state: impl Into<StorageStateInput>) -> Self {
214 self.storage_state = Some(state.into());
215 self
216 }
217
218 pub fn chromiumoxide() -> Self {
220 Self::default().engine(EngineType::Chromiumoxide)
221 }
222
223 pub fn fantoccini() -> Self {
225 Self::default().engine(EngineType::Fantoccini)
226 }
227
228 pub fn playwright() -> Self {
230 Self::default().engine(EngineType::Playwright)
231 }
232
233 pub fn puppeteer() -> Self {
235 Self::default().engine(EngineType::Puppeteer)
236 }
237
238 pub fn launch(mut self, launch: LaunchMode) -> Self {
240 self.launch = launch;
241 self
242 }
243
244 pub fn headless(mut self, headless: bool) -> Self {
246 self.headless = headless;
247 self
248 }
249
250 pub fn user_data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
252 self.user_data_dir = Some(dir.into());
253 self
254 }
255
256 pub fn slow_mo(mut self, ms: u64) -> Self {
258 self.slow_mo = ms;
259 self
260 }
261
262 pub fn verbose(mut self, verbose: bool) -> Self {
264 self.verbose = verbose;
265 self
266 }
267
268 pub fn restrictions<I, S>(mut self, restrictions: I) -> Self
270 where
271 I: IntoIterator<Item = S>,
272 S: Into<String>,
273 {
274 self.restrictions = restrictions.into_iter().map(Into::into).collect();
275 self
276 }
277
278 pub fn with_args(mut self, args: Vec<String>) -> Self {
280 self.args = args;
281 self
282 }
283
284 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
286 self.extra_args = args;
287 self
288 }
289
290 pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
292 self.ignore_default_args = args;
293 self
294 }
295
296 pub fn ignore_all_default_args(mut self) -> Self {
298 self.ignore_all_default_args = true;
299 self
300 }
301
302 pub fn env(mut self, env: HashMap<String, String>) -> Self {
304 self.env = Some(env);
305 self
306 }
307
308 pub fn channel(mut self, channel: impl Into<String>) -> Self {
310 self.channel = Some(channel.into());
311 self
312 }
313
314 pub fn executable_path(mut self, executable_path: impl Into<PathBuf>) -> Self {
316 self.executable_path = Some(executable_path.into());
317 self
318 }
319
320 pub fn remote_debugging_port(mut self, port: u16) -> Self {
322 self.remote_debugging_port = Some(port);
323 self
324 }
325
326 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
328 self.color_scheme = Some(color_scheme);
329 self
330 }
331
332 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
334 self.launch_timeout = Some(timeout);
335 self
336 }
337
338 pub fn sandbox(mut self, sandbox: bool) -> Self {
340 self.sandbox = sandbox;
341 self
342 }
343
344 pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
346 self.node_executable = Some(executable.into());
347 self
348 }
349
350 pub fn node_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
352 self.node_working_dir = Some(dir.into());
353 self
354 }
355
356 pub fn automation_parity(mut self, automation_parity: bool) -> Self {
358 self.automation_parity = automation_parity;
359 self
360 }
361
362 pub fn fingerprint(mut self, fingerprint: FingerprintProfile) -> Self {
364 self.fingerprint = Some(fingerprint);
365 self
366 }
367
368 pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
375 self.downloads = downloads.into();
376 self
377 }
378
379 pub fn all_chrome_args(&self) -> anyhow::Result<Vec<String>> {
391 let mut args = resolve_restrictions(&self.restrictions)?.args;
392 args.extend(self.args.iter().cloned());
393 args.extend(self.extra_args.iter().cloned());
394 let args = merge_feature_switches(&args);
395 Ok(if self.automation_parity {
396 apply_automation_parity_args(&args)
397 } else {
398 args
399 })
400 }
401
402 pub fn all_ignored_default_args(&self) -> Vec<String> {
409 let mut ignored = if self.automation_parity {
410 parity_ignored_default_args(self.engine, self.headless)
411 } else {
412 Vec::new()
413 };
414 for argument in &self.ignore_default_args {
415 if !ignored.contains(argument) {
416 ignored.push(argument.clone());
417 }
418 }
419 ignored
420 }
421
422 pub(crate) fn browser_env(&self) -> anyhow::Result<Option<HashMap<String, String>>> {
426 let mut env = resolve_restrictions(&self.restrictions)?.env;
427 if let Some(extra) = &self.env {
428 env.extend(
429 extra
430 .iter()
431 .map(|(key, value)| (key.clone(), value.clone())),
432 );
433 }
434 Ok((!env.is_empty() || self.env.is_some()).then_some(env))
435 }
436
437 #[deprecated(
439 since = "0.13.0",
440 note = "launch_browser uses a fresh temporary profile unless user_data_dir is set; read LaunchResult::browser.user_data_dir"
441 )]
442 pub fn get_user_data_dir(&self) -> PathBuf {
443 if let Some(ref dir) = self.user_data_dir {
444 dir.clone()
445 } else {
446 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
447 home.join(".browser-commander")
448 .join(format!("{}-data", self.engine))
449 }
450 }
451
452 pub(crate) fn real_browser_options(&self) -> RealBrowserOptions {
454 let defaults = RealBrowserOptions::default();
455 let mut extra_args = self.extra_args.clone();
456 if !self.sandbox
457 && !self
458 .args
459 .iter()
460 .chain(&extra_args)
461 .any(|a| a == "--no-sandbox")
462 {
463 extra_args.push("--no-sandbox".to_string());
464 }
465 RealBrowserOptions {
466 engine: self.engine,
467 channel: self.channel.clone().unwrap_or(defaults.channel.clone()),
468 executable_path: self.executable_path.clone(),
469 user_data_dir: self.user_data_dir.clone(),
470 default_browser_check: self.default_browser_check,
471 first_run: self.first_run,
472 preferences: self.preferences.clone(),
473 local_state: self.local_state.clone(),
474 storage_state: self.storage_state.clone(),
475 remote_debugging_port: self.remote_debugging_port,
476 headless: self.headless,
477 restrictions: self.restrictions.clone(),
478 args: self.args.clone(),
479 extra_args,
480 env: self.env.clone(),
481 automation_parity: self.automation_parity,
482 startup_timeout: self.launch_timeout.unwrap_or(defaults.startup_timeout),
483 slow_mo: self.slow_mo,
484 verbose: self.verbose,
485 node_executable: self.node_executable.clone(),
486 node_working_dir: self.node_working_dir.clone(),
487 downloads: self.downloads.clone(),
488 ..defaults
489 }
490 }
491}
492
493#[derive(Debug, Clone)]
495pub struct Browser {
496 pub engine: EngineType,
498 pub user_data_dir: PathBuf,
500 pub headless: bool,
502}
503
504pub struct LaunchResult {
511 pub browser: Browser,
513 pub page: Arc<dyn EngineAdapter>,
520 pub downloads: Option<Arc<DownloadManager>>,
526 pub launch: Option<LaunchMode>,
529 pub temporary_profile: bool,
532 pub args: Vec<String>,
535 pub cdp_endpoint: Option<String>,
537 pub remote_debugging_port: Option<u16>,
539 pub executable_path: Option<PathBuf>,
542 pub browser_process: Option<BrowserProcess>,
544 closer: Option<Arc<dyn BrowserCloser>>,
545}
546
547impl LaunchResult {
548 pub(crate) fn attached(
550 browser: Browser,
551 page: Arc<dyn EngineAdapter>,
552 downloads: Option<Arc<DownloadManager>>,
553 ) -> Self {
554 Self {
555 browser,
556 page,
557 downloads,
558 launch: None,
559 temporary_profile: false,
560 args: Vec::new(),
561 cdp_endpoint: None,
562 remote_debugging_port: None,
563 executable_path: None,
564 browser_process: None,
565 closer: None,
566 }
567 }
568
569 pub async fn close(&self) -> anyhow::Result<()> {
576 match &self.closer {
577 Some(closer) => closer.close().await,
578 None => Ok(()),
579 }
580 }
581}
582
583impl std::fmt::Debug for LaunchResult {
584 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585 f.debug_struct("LaunchResult")
586 .field("browser", &self.browser)
587 .field("page", &"<dyn EngineAdapter>")
588 .field("downloads", &self.downloads)
589 .field("launch", &self.launch)
590 .field("temporary_profile", &self.temporary_profile)
591 .field("args", &self.args)
592 .field("cdp_endpoint", &self.cdp_endpoint)
593 .field("remote_debugging_port", &self.remote_debugging_port)
594 .field("executable_path", &self.executable_path)
595 .field("browser_process", &self.browser_process)
596 .finish()
597 }
598}
599
600pub async fn launch_browser(options: LaunchOptions) -> Result<LaunchResult, anyhow::Error> {
631 if options.engine == EngineType::Fantoccini && options.launch == LaunchMode::Real {
632 return Err(anyhow::anyhow!(
633 "fantoccini requires LaunchMode::Engine; use LaunchOptions::fantoccini()"
634 ));
635 }
636 options.all_chrome_args()?;
638 if let Some(state) = &options.storage_state {
639 state.load()?;
640 }
641 refuse_unappliable_fingerprint(options.engine, options.fingerprint.as_ref())?;
642 normalize_download_options(options.downloads.clone())
645 .map(|_| supported_engine(options.engine))
646 .transpose()
647 .map_err(|error| anyhow::anyhow!("{error}"))?;
648
649 if options.verbose {
650 tracing::info!(
651 "Launching browser with {} engine ({})...",
652 options.engine,
653 options.launch
654 );
655 }
656 let result = match options.launch {
657 LaunchMode::Real => launch_real(&options).await?,
658 LaunchMode::Engine => launch_with_engine(&options).await?,
659 };
660 if options.verbose {
661 tracing::info!("Browser launched with {} engine", options.engine);
662 }
663 Ok(result)
664}
665
666async fn launch_real(options: &LaunchOptions) -> Result<LaunchResult, anyhow::Error> {
667 let real = options.real_browser_options();
668 let hooks = Arc::new(DefaultLaunchHooks {
669 explicit_selection: options.channel.is_some() || options.executable_path.is_some(),
670 });
671 let settings = AttachSettings {
672 fingerprint: options.fingerprint.as_ref(),
673 color_scheme: options.color_scheme.as_ref(),
674 };
675 let (mut result, launched) = launch_real_browser_with(&real, hooks, |connect| {
676 connect_browser_with(connect, settings)
677 })
678 .await?;
679
680 if !options.headless {
683 if let Err(error) = result.page.bring_to_front().await {
684 if options.verbose {
685 tracing::debug!(%error, "bring_to_front failed");
686 }
687 }
688 }
689
690 result.browser.user_data_dir = launched.user_data_dir;
691 result.browser.headless = options.headless;
692 result.launch = Some(LaunchMode::Real);
693 result.temporary_profile = launched.temporary_profile;
694 result.args = launched.args;
695 result.cdp_endpoint = Some(launched.cdp_endpoint);
696 result.remote_debugging_port = Some(launched.remote_debugging_port);
697 result.executable_path = Some(launched.executable_path);
698 result.browser_process = Some(launched.browser_process);
699 result.closer = Some(launched.closer as Arc<dyn BrowserCloser>);
700 Ok(result)
701}
702
703impl LaunchResult {
704 pub(crate) fn launched_by_engine(
706 mut self,
707 args: Vec<String>,
708 temporary_profile: bool,
709 executable_path: Option<PathBuf>,
710 closer: Arc<dyn BrowserCloser>,
711 ) -> Self {
712 self.launch = Some(LaunchMode::Engine);
713 self.args = args;
714 self.temporary_profile = temporary_profile;
715 self.executable_path = executable_path;
716 self.closer = Some(closer);
717 self
718 }
719}
720
721#[cfg(test)]
722#[path = "launcher_tests.rs"]
723mod tests;