Skip to main content

browser_commander/browser/
launcher.rs

1//! Browser launcher for browser automation.
2//!
3//! By default ([`LaunchMode::Real`]) Browser Commander starts the installed
4//! Chrome itself, exactly like a person who wants to attach a debugger would,
5//! and attaches the engine over CDP (issues #101 and #103).
6//! [`LaunchMode::Engine`] keeps the engine-launched browser for CI and
7//! headless use. Mirrors `js/src/browser/launcher.js`.
8
9use 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/// Who starts the browser (issue #103).
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
36pub enum LaunchMode {
37    /// Browser Commander starts the installed browser with a hand-started
38    /// command line - `--user-data-dir=<fresh profile>
39    /// --remote-debugging-port=<reserved port> about:blank` - and attaches the engine.
40    #[default]
41    Real,
42    /// The automation engine starts the browser with its own switches, which
43    /// `limitations.json` lists (`engine-launch-switches`).
44    Engine,
45}
46
47/// Every [`LaunchMode`], matching the JavaScript `LAUNCH_MODES`.
48pub const LAUNCH_MODES: [LaunchMode; 2] = [LaunchMode::Real, LaunchMode::Engine];
49
50impl LaunchMode {
51    /// The name shared with the JavaScript and Python packages.
52    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/// Options for launching a browser.
80#[derive(Debug, Clone)]
81pub struct LaunchOptions {
82    /// The browser engine to use.
83    pub engine: EngineType,
84    /// Native WebDriver driver selection and W3C capabilities. Common launch
85    /// settings below override their corresponding WebDriver settings.
86    pub webdriver: super::webdriver::WebDriverOptions,
87    /// Who starts the browser; [`LaunchMode::Real`] by default.
88    pub launch: LaunchMode,
89    /// Persistent profile directory. When `None` a fresh temporary profile is
90    /// created for the launch and deleted by [`LaunchResult::close`].
91    pub user_data_dir: Option<PathBuf>,
92    /// Allow the disposable browser to ask to become the system default.
93    pub default_browser_check: Option<bool>,
94    /// Allow first-run UI in a fresh profile.
95    pub first_run: bool,
96    /// Preferences merged into Default/Preferences before launch.
97    pub preferences: Value,
98    /// Preferences merged into Local State before launch.
99    pub local_state: Value,
100    /// Playwright-compatible cookie and localStorage state to restore.
101    pub storage_state: Option<StorageStateInput>,
102    /// Run in headless mode.
103    pub headless: bool,
104    /// Slow down operations by this many milliseconds (default 0).
105    pub slow_mo: u64,
106    /// Enable verbose logging.
107    pub verbose: bool,
108    /// Opt-in restrictions from `launch-restrictions.json`, such as
109    /// `no-extensions` or the `legacy-defaults` preset (issue #103).
110    pub restrictions: Vec<String>,
111    /// Additional Chrome arguments.
112    pub args: Vec<String>,
113    /// Additional Chrome arguments appended after the compatibility `args`.
114    pub extra_args: Vec<String>,
115    /// Engine default switches to omit ([`LaunchMode::Engine`] only).
116    pub ignore_default_args: Vec<String>,
117    /// Omit every engine default switch ([`LaunchMode::Engine`] only).
118    pub ignore_all_default_args: bool,
119    /// Extra environment for the browser process only; the parent's
120    /// environment is never modified.
121    pub env: Option<HashMap<String, String>>,
122    /// Installed browser channel, such as `chrome`, `chrome-beta`, `msedge`,
123    /// `brave` or `chromium`.
124    pub channel: Option<String>,
125    /// Explicit path to a Chrome or Chromium executable.
126    pub executable_path: Option<PathBuf>,
127    /// Fixed CDP port for the real launch; a free one is reserved when `None`.
128    pub remote_debugging_port: Option<u16>,
129    /// Color scheme to emulate. `None` uses the system default.
130    pub color_scheme: Option<ColorScheme>,
131    /// Optional timeout for the browser launch handshake.
132    pub launch_timeout: Option<Duration>,
133    /// Whether to run the browser with the Chromium sandbox enabled.
134    ///
135    /// Defaults to `true`. Disable when running in environments where the
136    /// sandbox is unavailable (e.g. CI containers without the required
137    /// capabilities). This adds `--no-sandbox`.
138    pub sandbox: bool,
139    /// Node.js executable for Playwright/Puppeteer fallback engines.
140    pub node_executable: Option<PathBuf>,
141    /// Working directory used to resolve Playwright/Puppeteer Node packages.
142    pub node_working_dir: Option<PathBuf>,
143    /// Keep `navigator.webdriver` false where a launch switch would turn it on
144    /// (headless or engine launches).
145    ///
146    /// Defaults to `true`. Set to `false` to launch with the engine's own
147    /// defaults, which is what the parity tests use as a negative control.
148    pub automation_parity: bool,
149    /// The environment pages should see: user agent, time zone, locale, core
150    /// count, screen and the rest.
151    ///
152    /// Applied over CDP once the browser is up, so it only works for the
153    /// chromiumoxide engine; see
154    /// [`fingerprint::profile`](crate::fingerprint::profile) for the field list
155    /// and [`presets`](crate::fingerprint::presets) for ready-made machines.
156    pub fingerprint: Option<FingerprintProfile>,
157    /// Manage the browser's downloads: where they are saved, how they are
158    /// named, and whether they outlive the browser.
159    ///
160    /// Downloads are redirected over CDP, so this only works for the
161    /// chromiumoxide engine; the other engines refuse rather than accept a
162    /// setting they cannot honor. See [`downloads`](crate::downloads).
163    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    /// Set the browser automation engine.
204    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    /// Restore portable session state before the first caller navigation.
213    pub fn storage_state(mut self, state: impl Into<StorageStateInput>) -> Self {
214        self.storage_state = Some(state.into());
215        self
216    }
217
218    /// Create options for chromiumoxide engine.
219    pub fn chromiumoxide() -> Self {
220        Self::default().engine(EngineType::Chromiumoxide)
221    }
222
223    /// Create options for fantoccini (WebDriver) engine.
224    pub fn fantoccini() -> Self {
225        Self::default().engine(EngineType::Fantoccini)
226    }
227
228    /// Create options for Playwright through the Node.js CLI bridge.
229    pub fn playwright() -> Self {
230        Self::default().engine(EngineType::Playwright)
231    }
232
233    /// Create options for Puppeteer through the Node.js CLI bridge.
234    pub fn puppeteer() -> Self {
235        Self::default().engine(EngineType::Puppeteer)
236    }
237
238    /// Choose who starts the browser.
239    pub fn launch(mut self, launch: LaunchMode) -> Self {
240        self.launch = launch;
241        self
242    }
243
244    /// Set headless mode.
245    pub fn headless(mut self, headless: bool) -> Self {
246        self.headless = headless;
247        self
248    }
249
250    /// Use a persistent profile directory instead of a temporary one.
251    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    /// Set slow motion delay.
257    pub fn slow_mo(mut self, ms: u64) -> Self {
258        self.slow_mo = ms;
259        self
260    }
261
262    /// Enable verbose logging.
263    pub fn verbose(mut self, verbose: bool) -> Self {
264        self.verbose = verbose;
265        self
266    }
267
268    /// Opt in to restrictions or presets from `launch-restrictions.json`.
269    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    /// Add additional Chrome arguments.
279    pub fn with_args(mut self, args: Vec<String>) -> Self {
280        self.args = args;
281        self
282    }
283
284    /// Add Chrome arguments after the compatibility `args` field.
285    pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
286        self.extra_args = args;
287        self
288    }
289
290    /// Omit selected engine default switches ([`LaunchMode::Engine`] only).
291    pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
292        self.ignore_default_args = args;
293        self
294    }
295
296    /// Omit every engine default switch ([`LaunchMode::Engine`] only).
297    pub fn ignore_all_default_args(mut self) -> Self {
298        self.ignore_all_default_args = true;
299        self
300    }
301
302    /// Extra environment for the browser process only.
303    pub fn env(mut self, env: HashMap<String, String>) -> Self {
304        self.env = Some(env);
305        self
306    }
307
308    /// Select an installed browser channel.
309    pub fn channel(mut self, channel: impl Into<String>) -> Self {
310        self.channel = Some(channel.into());
311        self
312    }
313
314    /// Select an explicit Chrome or Chromium executable.
315    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    /// Use a fixed CDP port for the real launch.
321    pub fn remote_debugging_port(mut self, port: u16) -> Self {
322        self.remote_debugging_port = Some(port);
323        self
324    }
325
326    /// Set the color scheme for media emulation.
327    pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
328        self.color_scheme = Some(color_scheme);
329        self
330    }
331
332    /// Override the browser launch timeout.
333    pub fn launch_timeout(mut self, timeout: Duration) -> Self {
334        self.launch_timeout = Some(timeout);
335        self
336    }
337
338    /// Enable or disable the Chromium sandbox for the launched browser.
339    pub fn sandbox(mut self, sandbox: bool) -> Self {
340        self.sandbox = sandbox;
341        self
342    }
343
344    /// Override the Node.js executable used by Playwright/Puppeteer engines.
345    pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
346        self.node_executable = Some(executable.into());
347        self
348    }
349
350    /// Set the directory where Node resolves `playwright` or `puppeteer`.
351    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    /// Turn fingerprint parity with a hand-started Chrome on or off.
357    pub fn automation_parity(mut self, automation_parity: bool) -> Self {
358        self.automation_parity = automation_parity;
359        self
360    }
361
362    /// Set the environment pages should see.
363    pub fn fingerprint(mut self, fingerprint: FingerprintProfile) -> Self {
364        self.fingerprint = Some(fingerprint);
365        self
366    }
367
368    /// Manage this browser's downloads.
369    ///
370    /// # Arguments
371    ///
372    /// * `downloads` - `true` for the defaults, `false` for none, or
373    ///   [`DownloadOptions`](crate::downloads::DownloadOptions)
374    pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
375        self.downloads = downloads.into();
376        self
377    }
378
379    /// The Chrome arguments an engine launch passes: the opt-in restrictions,
380    /// then `args` and `extra_args`, with repeated feature-list switches
381    /// merged and, with automation parity, the `AutomationControlled` off
382    /// switch the engine's own switches need.
383    ///
384    /// Browser Commander adds nothing else since issue #103; the old defaults
385    /// are the `legacy-defaults` restriction preset.
386    ///
387    /// # Errors
388    ///
389    /// Returns an error for an unknown restriction.
390    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    /// Engine default switches to suppress so the command line matches a
403    /// hand-started Chrome ([`LaunchMode::Engine`] only).
404    ///
405    /// Merged with the caller's `ignore_default_args`, because a switch the
406    /// engine appends after the caller's arguments cannot be countered by
407    /// passing a different value for it.
408    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    /// The environment the browser process gets on top of the parent's: the
423    /// restrictions' variables, then `env`. `None` when there is nothing to
424    /// add.
425    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    /// Get the user data directory, using a default if not specified.
438    #[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    /// The same launch expressed as [`RealBrowserOptions`].
453    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/// Browser metadata returned alongside a launched page.
494#[derive(Debug, Clone)]
495pub struct Browser {
496    /// The engine type being used.
497    pub engine: EngineType,
498    /// The user data directory.
499    pub user_data_dir: PathBuf,
500    /// Whether the browser is running headless.
501    pub headless: bool,
502}
503
504/// Result of a browser launch.
505///
506/// Contains static metadata (`browser` and the launch fields), a live
507/// [`EngineAdapter`] (`page`) that can be passed to the navigation,
508/// interaction, and query helpers exposed by this crate, and
509/// [`close`](Self::close).
510pub struct LaunchResult {
511    /// The browser metadata.
512    pub browser: Browser,
513    /// A live page/adapter tied to the launched browser.
514    ///
515    /// For `Chromiumoxide`, this is a
516    /// [`ChromiumoxidePage`](super::chromiumoxide_adapter::ChromiumoxidePage)
517    /// implementing [`EngineAdapter`]. Pass `launch_result.page.as_ref()` to
518    /// `goto`, `click`, `evaluate`, and other helpers.
519    pub page: Arc<dyn EngineAdapter>,
520    /// The download manager, when the caller asked for managed downloads.
521    ///
522    /// Downloads keep arriving while the browser is open, so the manager
523    /// outlives any single call: hold on to it, and call
524    /// [`DownloadManager::dispose`] before closing the browser.
525    pub downloads: Option<Arc<DownloadManager>>,
526    /// Who started the browser; `None` for a browser attached with
527    /// [`connect_browser`](super::connector::connect_browser).
528    pub launch: Option<LaunchMode>,
529    /// Whether `browser.user_data_dir` is a temporary profile that
530    /// [`close`](Self::close) deletes.
531    pub temporary_profile: bool,
532    /// The Chrome arguments Browser Commander passed (the engine adds its own
533    /// in [`LaunchMode::Engine`]).
534    pub args: Vec<String>,
535    /// The DevTools HTTP endpoint of a real launch.
536    pub cdp_endpoint: Option<String>,
537    /// The DevTools port of a real launch.
538    pub remote_debugging_port: Option<u16>,
539    /// The browser binary a real launch started, or the one requested for an
540    /// engine launch.
541    pub executable_path: Option<PathBuf>,
542    /// The browser process of a real launch (the engine owns it otherwise).
543    pub browser_process: Option<BrowserProcess>,
544    closer: Option<Arc<dyn BrowserCloser>>,
545}
546
547impl LaunchResult {
548    /// A browser somebody else started, attached over CDP.
549    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    /// Close the browser this launch started and delete its temporary
570    /// profile. Idempotent.
571    ///
572    /// A browser attached with
573    /// [`connect_browser`](super::connector::connect_browser) is managed by
574    /// whoever started it, so this does nothing for one.
575    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
600/// Launch a browser with the given options.
601///
602/// By default ([`LaunchMode::Real`]) Browser Commander starts the installed
603/// Chrome itself: `--user-data-dir=<fresh temporary profile>
604/// --remote-debugging-port=<reserved port> about:blank` and nothing else (plus
605/// `--headless=new`, restrictions and the caller's arguments when asked for),
606/// then attaches the engine over CDP. The browser behaves like one a person
607/// started by hand and `navigator.webdriver` stays false. Without an explicit
608/// `channel` or `executable_path` the installed Google Chrome is preferred
609/// and the engine's own browser is the fallback. Every restriction the
610/// library used to add silently is an explicit opt-in through
611/// `restrictions`.
612///
613/// [`LaunchMode::Engine`] keeps the chromiumoxide-, Playwright- or
614/// Puppeteer-launched browser for CI and headless use. Playwright and
615/// Puppeteer run as a local Node.js subprocess using the official Node
616/// package as a CLI bridge; the package must be available to Node module
617/// resolution, usually by running `npm install playwright` or `npm install
618/// puppeteer` in the configured `node_working_dir`.
619///
620/// Either way the profile is a fresh temporary one unless `user_data_dir` is
621/// set, and [`LaunchResult::close`] closes the browser and deletes it.
622///
623/// `Fantoccini` starts chromedriver/geckodriver through command-stream with
624/// typed W3C WebDriver, optional BiDi and preference-based managed downloads.
625///
626/// # Errors
627///
628/// Returns an error if the options are invalid or the browser fails to
629/// launch. Invalid options are refused before anything is started.
630pub 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    // Validate before anything is started or written to disk.
637    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    // The node bridge has no CDP route, so a managed download would never be
643    // seen and every capture would time out.
644    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    // Bring the page to front so the address bar is not focused when running
681    // headful - mirrors the JS launcher's behavior.
682    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    /// Record an engine launch on an attached result.
705    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;