1use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12use std::time::Duration;
13
14use crate::browser::browser_process::{BrowserCloser, BrowserProcess};
15use crate::browser::connector::{
16 connect_browser_with, refuse_unappliable_fingerprint, AttachSettings,
17};
18use crate::browser::engine_launch::launch_with_engine;
19use crate::browser::launch_executable::DefaultLaunchHooks;
20use crate::browser::media::ColorScheme;
21use crate::browser::real_browser::{launch_real_browser_with, RealBrowserOptions};
22use crate::browser::restrictions::{merge_feature_switches, resolve_restrictions};
23use crate::core::engine::{EngineAdapter, EngineType};
24use crate::downloads::{normalize_download_options, supported_engine};
25use crate::downloads::{DownloadManager, DownloadSetting};
26use crate::fingerprint::automation_parity::{
27 apply_automation_parity_args, parity_ignored_default_args,
28};
29use crate::fingerprint::profile::FingerprintProfile;
30
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
33pub enum LaunchMode {
34 #[default]
38 Real,
39 Engine,
42}
43
44pub const LAUNCH_MODES: [LaunchMode; 2] = [LaunchMode::Real, LaunchMode::Engine];
46
47impl LaunchMode {
48 pub fn as_str(&self) -> &'static str {
50 match self {
51 Self::Real => "real",
52 Self::Engine => "engine",
53 }
54 }
55}
56
57impl std::fmt::Display for LaunchMode {
58 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 formatter.write_str(self.as_str())
60 }
61}
62
63impl std::str::FromStr for LaunchMode {
64 type Err = anyhow::Error;
65
66 fn from_str(value: &str) -> Result<Self, Self::Err> {
67 LAUNCH_MODES
68 .into_iter()
69 .find(|mode| mode.as_str() == value)
70 .ok_or_else(|| {
71 anyhow::anyhow!("Invalid launch mode: {value}. Expected 'real' or 'engine'")
72 })
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct LaunchOptions {
79 pub engine: EngineType,
81 pub launch: LaunchMode,
83 pub user_data_dir: Option<PathBuf>,
86 pub headless: bool,
88 pub slow_mo: u64,
90 pub verbose: bool,
92 pub restrictions: Vec<String>,
95 pub args: Vec<String>,
97 pub extra_args: Vec<String>,
99 pub ignore_default_args: Vec<String>,
101 pub ignore_all_default_args: bool,
103 pub env: Option<HashMap<String, String>>,
106 pub channel: Option<String>,
109 pub executable_path: Option<PathBuf>,
111 pub remote_debugging_port: Option<u16>,
113 pub color_scheme: Option<ColorScheme>,
115 pub launch_timeout: Option<Duration>,
117 pub sandbox: bool,
123 pub node_executable: Option<PathBuf>,
125 pub node_working_dir: Option<PathBuf>,
127 pub automation_parity: bool,
133 pub fingerprint: Option<FingerprintProfile>,
141 pub downloads: DownloadSetting,
148}
149
150impl Default for LaunchOptions {
151 fn default() -> Self {
152 Self {
153 engine: EngineType::Chromiumoxide,
154 launch: LaunchMode::Real,
155 user_data_dir: None,
156 headless: false,
157 slow_mo: 0,
158 verbose: false,
159 restrictions: Vec::new(),
160 args: Vec::new(),
161 extra_args: Vec::new(),
162 ignore_default_args: Vec::new(),
163 ignore_all_default_args: false,
164 env: None,
165 channel: None,
166 executable_path: None,
167 remote_debugging_port: None,
168 color_scheme: None,
169 launch_timeout: None,
170 sandbox: true,
171 node_executable: None,
172 node_working_dir: None,
173 automation_parity: true,
174 fingerprint: None,
175 downloads: DownloadSetting::Off,
176 }
177 }
178}
179
180impl LaunchOptions {
181 pub fn engine(mut self, engine: EngineType) -> Self {
183 self.engine = engine;
184 self
185 }
186
187 pub fn chromiumoxide() -> Self {
189 Self::default().engine(EngineType::Chromiumoxide)
190 }
191
192 pub fn fantoccini() -> Self {
194 Self::default().engine(EngineType::Fantoccini)
195 }
196
197 pub fn playwright() -> Self {
199 Self::default().engine(EngineType::Playwright)
200 }
201
202 pub fn puppeteer() -> Self {
204 Self::default().engine(EngineType::Puppeteer)
205 }
206
207 pub fn launch(mut self, launch: LaunchMode) -> Self {
209 self.launch = launch;
210 self
211 }
212
213 pub fn headless(mut self, headless: bool) -> Self {
215 self.headless = headless;
216 self
217 }
218
219 pub fn user_data_dir(mut self, dir: impl Into<PathBuf>) -> Self {
221 self.user_data_dir = Some(dir.into());
222 self
223 }
224
225 pub fn slow_mo(mut self, ms: u64) -> Self {
227 self.slow_mo = ms;
228 self
229 }
230
231 pub fn verbose(mut self, verbose: bool) -> Self {
233 self.verbose = verbose;
234 self
235 }
236
237 pub fn restrictions<I, S>(mut self, restrictions: I) -> Self
239 where
240 I: IntoIterator<Item = S>,
241 S: Into<String>,
242 {
243 self.restrictions = restrictions.into_iter().map(Into::into).collect();
244 self
245 }
246
247 pub fn with_args(mut self, args: Vec<String>) -> Self {
249 self.args = args;
250 self
251 }
252
253 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
255 self.extra_args = args;
256 self
257 }
258
259 pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
261 self.ignore_default_args = args;
262 self
263 }
264
265 pub fn ignore_all_default_args(mut self) -> Self {
267 self.ignore_all_default_args = true;
268 self
269 }
270
271 pub fn env(mut self, env: HashMap<String, String>) -> Self {
273 self.env = Some(env);
274 self
275 }
276
277 pub fn channel(mut self, channel: impl Into<String>) -> Self {
279 self.channel = Some(channel.into());
280 self
281 }
282
283 pub fn executable_path(mut self, executable_path: impl Into<PathBuf>) -> Self {
285 self.executable_path = Some(executable_path.into());
286 self
287 }
288
289 pub fn remote_debugging_port(mut self, port: u16) -> Self {
291 self.remote_debugging_port = Some(port);
292 self
293 }
294
295 pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
297 self.color_scheme = Some(color_scheme);
298 self
299 }
300
301 pub fn launch_timeout(mut self, timeout: Duration) -> Self {
303 self.launch_timeout = Some(timeout);
304 self
305 }
306
307 pub fn sandbox(mut self, sandbox: bool) -> Self {
309 self.sandbox = sandbox;
310 self
311 }
312
313 pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
315 self.node_executable = Some(executable.into());
316 self
317 }
318
319 pub fn node_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
321 self.node_working_dir = Some(dir.into());
322 self
323 }
324
325 pub fn automation_parity(mut self, automation_parity: bool) -> Self {
327 self.automation_parity = automation_parity;
328 self
329 }
330
331 pub fn fingerprint(mut self, fingerprint: FingerprintProfile) -> Self {
333 self.fingerprint = Some(fingerprint);
334 self
335 }
336
337 pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
344 self.downloads = downloads.into();
345 self
346 }
347
348 pub fn all_chrome_args(&self) -> anyhow::Result<Vec<String>> {
360 let mut args = resolve_restrictions(&self.restrictions)?.args;
361 args.extend(self.args.iter().cloned());
362 args.extend(self.extra_args.iter().cloned());
363 let args = merge_feature_switches(&args);
364 Ok(if self.automation_parity {
365 apply_automation_parity_args(&args)
366 } else {
367 args
368 })
369 }
370
371 pub fn all_ignored_default_args(&self) -> Vec<String> {
378 let mut ignored = if self.automation_parity {
379 parity_ignored_default_args(self.engine, self.headless)
380 } else {
381 Vec::new()
382 };
383 for argument in &self.ignore_default_args {
384 if !ignored.contains(argument) {
385 ignored.push(argument.clone());
386 }
387 }
388 ignored
389 }
390
391 pub(crate) fn browser_env(&self) -> anyhow::Result<Option<HashMap<String, String>>> {
395 let mut env = resolve_restrictions(&self.restrictions)?.env;
396 if let Some(extra) = &self.env {
397 env.extend(
398 extra
399 .iter()
400 .map(|(key, value)| (key.clone(), value.clone())),
401 );
402 }
403 Ok((!env.is_empty() || self.env.is_some()).then_some(env))
404 }
405
406 #[deprecated(
408 since = "0.13.0",
409 note = "launch_browser uses a fresh temporary profile unless user_data_dir is set; read LaunchResult::browser.user_data_dir"
410 )]
411 pub fn get_user_data_dir(&self) -> PathBuf {
412 if let Some(ref dir) = self.user_data_dir {
413 dir.clone()
414 } else {
415 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
416 home.join(".browser-commander")
417 .join(format!("{}-data", self.engine))
418 }
419 }
420
421 pub(crate) fn real_browser_options(&self) -> RealBrowserOptions {
423 let defaults = RealBrowserOptions::default();
424 let mut extra_args = self.extra_args.clone();
425 if !self.sandbox
426 && !self
427 .args
428 .iter()
429 .chain(&extra_args)
430 .any(|a| a == "--no-sandbox")
431 {
432 extra_args.push("--no-sandbox".to_string());
433 }
434 RealBrowserOptions {
435 engine: self.engine,
436 channel: self.channel.clone().unwrap_or(defaults.channel.clone()),
437 executable_path: self.executable_path.clone(),
438 user_data_dir: self.user_data_dir.clone(),
439 remote_debugging_port: self.remote_debugging_port,
440 headless: self.headless,
441 restrictions: self.restrictions.clone(),
442 args: self.args.clone(),
443 extra_args,
444 env: self.env.clone(),
445 automation_parity: self.automation_parity,
446 startup_timeout: self.launch_timeout.unwrap_or(defaults.startup_timeout),
447 slow_mo: self.slow_mo,
448 verbose: self.verbose,
449 node_executable: self.node_executable.clone(),
450 node_working_dir: self.node_working_dir.clone(),
451 downloads: self.downloads.clone(),
452 ..defaults
453 }
454 }
455}
456
457#[derive(Debug, Clone)]
459pub struct Browser {
460 pub engine: EngineType,
462 pub user_data_dir: PathBuf,
464 pub headless: bool,
466}
467
468pub struct LaunchResult {
475 pub browser: Browser,
477 pub page: Arc<dyn EngineAdapter>,
484 pub downloads: Option<Arc<DownloadManager>>,
490 pub launch: Option<LaunchMode>,
493 pub temporary_profile: bool,
496 pub args: Vec<String>,
499 pub cdp_endpoint: Option<String>,
501 pub remote_debugging_port: Option<u16>,
503 pub executable_path: Option<PathBuf>,
506 pub browser_process: Option<BrowserProcess>,
508 closer: Option<Arc<dyn BrowserCloser>>,
509}
510
511impl LaunchResult {
512 pub(crate) fn attached(
514 browser: Browser,
515 page: Arc<dyn EngineAdapter>,
516 downloads: Option<Arc<DownloadManager>>,
517 ) -> Self {
518 Self {
519 browser,
520 page,
521 downloads,
522 launch: None,
523 temporary_profile: false,
524 args: Vec::new(),
525 cdp_endpoint: None,
526 remote_debugging_port: None,
527 executable_path: None,
528 browser_process: None,
529 closer: None,
530 }
531 }
532
533 pub async fn close(&self) -> anyhow::Result<()> {
540 match &self.closer {
541 Some(closer) => closer.close().await,
542 None => Ok(()),
543 }
544 }
545}
546
547impl std::fmt::Debug for LaunchResult {
548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549 f.debug_struct("LaunchResult")
550 .field("browser", &self.browser)
551 .field("page", &"<dyn EngineAdapter>")
552 .field("downloads", &self.downloads)
553 .field("launch", &self.launch)
554 .field("temporary_profile", &self.temporary_profile)
555 .field("args", &self.args)
556 .field("cdp_endpoint", &self.cdp_endpoint)
557 .field("remote_debugging_port", &self.remote_debugging_port)
558 .field("executable_path", &self.executable_path)
559 .field("browser_process", &self.browser_process)
560 .finish()
561 }
562}
563
564pub async fn launch_browser(options: LaunchOptions) -> Result<LaunchResult, anyhow::Error> {
595 if options.engine == EngineType::Fantoccini {
596 return Err(anyhow::anyhow!(
597 "fantoccini engine launch is not yet implemented; \
598 connect to an existing WebDriver session or use EngineType::Chromiumoxide"
599 ));
600 }
601 options.all_chrome_args()?;
603 refuse_unappliable_fingerprint(options.engine, options.fingerprint.as_ref())?;
604 normalize_download_options(options.downloads.clone())
607 .map(|_| supported_engine(options.engine))
608 .transpose()
609 .map_err(|error| anyhow::anyhow!("{error}"))?;
610
611 if options.verbose {
612 tracing::info!(
613 "Launching browser with {} engine ({})...",
614 options.engine,
615 options.launch
616 );
617 }
618 let result = match options.launch {
619 LaunchMode::Real => launch_real(&options).await?,
620 LaunchMode::Engine => launch_with_engine(&options).await?,
621 };
622 if options.verbose {
623 tracing::info!("Browser launched with {} engine", options.engine);
624 }
625 Ok(result)
626}
627
628async fn launch_real(options: &LaunchOptions) -> Result<LaunchResult, anyhow::Error> {
629 let real = options.real_browser_options();
630 let hooks = Arc::new(DefaultLaunchHooks {
631 explicit_selection: options.channel.is_some() || options.executable_path.is_some(),
632 });
633 let settings = AttachSettings {
634 fingerprint: options.fingerprint.as_ref(),
635 color_scheme: options.color_scheme.as_ref(),
636 };
637 let (mut result, launched) = launch_real_browser_with(&real, hooks, |connect| {
638 connect_browser_with(connect, settings)
639 })
640 .await?;
641
642 if !options.headless {
645 if let Err(error) = result.page.bring_to_front().await {
646 if options.verbose {
647 tracing::debug!(%error, "bring_to_front failed");
648 }
649 }
650 }
651
652 result.browser.user_data_dir = launched.user_data_dir;
653 result.browser.headless = options.headless;
654 result.launch = Some(LaunchMode::Real);
655 result.temporary_profile = launched.temporary_profile;
656 result.args = launched.args;
657 result.cdp_endpoint = Some(launched.cdp_endpoint);
658 result.remote_debugging_port = Some(launched.remote_debugging_port);
659 result.executable_path = Some(launched.executable_path);
660 result.browser_process = Some(launched.browser_process);
661 result.closer = Some(launched.closer as Arc<dyn BrowserCloser>);
662 Ok(result)
663}
664
665impl LaunchResult {
666 pub(crate) fn launched_by_engine(
668 mut self,
669 args: Vec<String>,
670 temporary_profile: bool,
671 executable_path: Option<PathBuf>,
672 closer: Arc<dyn BrowserCloser>,
673 ) -> Self {
674 self.launch = Some(LaunchMode::Engine);
675 self.args = args;
676 self.temporary_profile = temporary_profile;
677 self.executable_path = executable_path;
678 self.closer = Some(closer);
679 self
680 }
681}
682
683#[cfg(test)]
684#[path = "launcher_tests.rs"]
685mod tests;