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 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/// Who starts the browser (issue #103).
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
33pub enum LaunchMode {
34    /// Browser Commander starts the installed browser with a hand-started
35    /// command line - `--user-data-dir=<fresh profile>
36    /// --remote-debugging-port=<reserved port> about:blank` - and attaches the engine.
37    #[default]
38    Real,
39    /// The automation engine starts the browser with its own switches, which
40    /// `limitations.json` lists (`engine-launch-switches`).
41    Engine,
42}
43
44/// Every [`LaunchMode`], matching the JavaScript `LAUNCH_MODES`.
45pub const LAUNCH_MODES: [LaunchMode; 2] = [LaunchMode::Real, LaunchMode::Engine];
46
47impl LaunchMode {
48    /// The name shared with the JavaScript and Python packages.
49    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/// Options for launching a browser.
77#[derive(Debug, Clone)]
78pub struct LaunchOptions {
79    /// The browser engine to use.
80    pub engine: EngineType,
81    /// Who starts the browser; [`LaunchMode::Real`] by default.
82    pub launch: LaunchMode,
83    /// Persistent profile directory. When `None` a fresh temporary profile is
84    /// created for the launch and deleted by [`LaunchResult::close`].
85    pub user_data_dir: Option<PathBuf>,
86    /// Run in headless mode.
87    pub headless: bool,
88    /// Slow down operations by this many milliseconds (default 0).
89    pub slow_mo: u64,
90    /// Enable verbose logging.
91    pub verbose: bool,
92    /// Opt-in restrictions from `launch-restrictions.json`, such as
93    /// `no-extensions` or the `legacy-defaults` preset (issue #103).
94    pub restrictions: Vec<String>,
95    /// Additional Chrome arguments.
96    pub args: Vec<String>,
97    /// Additional Chrome arguments appended after the compatibility `args`.
98    pub extra_args: Vec<String>,
99    /// Engine default switches to omit ([`LaunchMode::Engine`] only).
100    pub ignore_default_args: Vec<String>,
101    /// Omit every engine default switch ([`LaunchMode::Engine`] only).
102    pub ignore_all_default_args: bool,
103    /// Extra environment for the browser process only; the parent's
104    /// environment is never modified.
105    pub env: Option<HashMap<String, String>>,
106    /// Installed browser channel, such as `chrome`, `chrome-beta`, `msedge`,
107    /// `brave` or `chromium`.
108    pub channel: Option<String>,
109    /// Explicit path to a Chrome or Chromium executable.
110    pub executable_path: Option<PathBuf>,
111    /// Fixed CDP port for the real launch; a free one is reserved when `None`.
112    pub remote_debugging_port: Option<u16>,
113    /// Color scheme to emulate. `None` uses the system default.
114    pub color_scheme: Option<ColorScheme>,
115    /// Optional timeout for the browser launch handshake.
116    pub launch_timeout: Option<Duration>,
117    /// Whether to run the browser with the Chromium sandbox enabled.
118    ///
119    /// Defaults to `true`. Disable when running in environments where the
120    /// sandbox is unavailable (e.g. CI containers without the required
121    /// capabilities). This adds `--no-sandbox`.
122    pub sandbox: bool,
123    /// Node.js executable for Playwright/Puppeteer fallback engines.
124    pub node_executable: Option<PathBuf>,
125    /// Working directory used to resolve Playwright/Puppeteer Node packages.
126    pub node_working_dir: Option<PathBuf>,
127    /// Keep `navigator.webdriver` false where a launch switch would turn it on
128    /// (headless or engine launches).
129    ///
130    /// Defaults to `true`. Set to `false` to launch with the engine's own
131    /// defaults, which is what the parity tests use as a negative control.
132    pub automation_parity: bool,
133    /// The environment pages should see: user agent, time zone, locale, core
134    /// count, screen and the rest.
135    ///
136    /// Applied over CDP once the browser is up, so it only works for the
137    /// chromiumoxide engine; see
138    /// [`fingerprint::profile`](crate::fingerprint::profile) for the field list
139    /// and [`presets`](crate::fingerprint::presets) for ready-made machines.
140    pub fingerprint: Option<FingerprintProfile>,
141    /// Manage the browser's downloads: where they are saved, how they are
142    /// named, and whether they outlive the browser.
143    ///
144    /// Downloads are redirected over CDP, so this only works for the
145    /// chromiumoxide engine; the other engines refuse rather than accept a
146    /// setting they cannot honor. See [`downloads`](crate::downloads).
147    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    /// Set the browser automation engine.
182    pub fn engine(mut self, engine: EngineType) -> Self {
183        self.engine = engine;
184        self
185    }
186
187    /// Create options for chromiumoxide engine.
188    pub fn chromiumoxide() -> Self {
189        Self::default().engine(EngineType::Chromiumoxide)
190    }
191
192    /// Create options for fantoccini (WebDriver) engine.
193    pub fn fantoccini() -> Self {
194        Self::default().engine(EngineType::Fantoccini)
195    }
196
197    /// Create options for Playwright through the Node.js CLI bridge.
198    pub fn playwright() -> Self {
199        Self::default().engine(EngineType::Playwright)
200    }
201
202    /// Create options for Puppeteer through the Node.js CLI bridge.
203    pub fn puppeteer() -> Self {
204        Self::default().engine(EngineType::Puppeteer)
205    }
206
207    /// Choose who starts the browser.
208    pub fn launch(mut self, launch: LaunchMode) -> Self {
209        self.launch = launch;
210        self
211    }
212
213    /// Set headless mode.
214    pub fn headless(mut self, headless: bool) -> Self {
215        self.headless = headless;
216        self
217    }
218
219    /// Use a persistent profile directory instead of a temporary one.
220    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    /// Set slow motion delay.
226    pub fn slow_mo(mut self, ms: u64) -> Self {
227        self.slow_mo = ms;
228        self
229    }
230
231    /// Enable verbose logging.
232    pub fn verbose(mut self, verbose: bool) -> Self {
233        self.verbose = verbose;
234        self
235    }
236
237    /// Opt in to restrictions or presets from `launch-restrictions.json`.
238    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    /// Add additional Chrome arguments.
248    pub fn with_args(mut self, args: Vec<String>) -> Self {
249        self.args = args;
250        self
251    }
252
253    /// Add Chrome arguments after the compatibility `args` field.
254    pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
255        self.extra_args = args;
256        self
257    }
258
259    /// Omit selected engine default switches ([`LaunchMode::Engine`] only).
260    pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
261        self.ignore_default_args = args;
262        self
263    }
264
265    /// Omit every engine default switch ([`LaunchMode::Engine`] only).
266    pub fn ignore_all_default_args(mut self) -> Self {
267        self.ignore_all_default_args = true;
268        self
269    }
270
271    /// Extra environment for the browser process only.
272    pub fn env(mut self, env: HashMap<String, String>) -> Self {
273        self.env = Some(env);
274        self
275    }
276
277    /// Select an installed browser channel.
278    pub fn channel(mut self, channel: impl Into<String>) -> Self {
279        self.channel = Some(channel.into());
280        self
281    }
282
283    /// Select an explicit Chrome or Chromium executable.
284    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    /// Use a fixed CDP port for the real launch.
290    pub fn remote_debugging_port(mut self, port: u16) -> Self {
291        self.remote_debugging_port = Some(port);
292        self
293    }
294
295    /// Set the color scheme for media emulation.
296    pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
297        self.color_scheme = Some(color_scheme);
298        self
299    }
300
301    /// Override the browser launch timeout.
302    pub fn launch_timeout(mut self, timeout: Duration) -> Self {
303        self.launch_timeout = Some(timeout);
304        self
305    }
306
307    /// Enable or disable the Chromium sandbox for the launched browser.
308    pub fn sandbox(mut self, sandbox: bool) -> Self {
309        self.sandbox = sandbox;
310        self
311    }
312
313    /// Override the Node.js executable used by Playwright/Puppeteer engines.
314    pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
315        self.node_executable = Some(executable.into());
316        self
317    }
318
319    /// Set the directory where Node resolves `playwright` or `puppeteer`.
320    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    /// Turn fingerprint parity with a hand-started Chrome on or off.
326    pub fn automation_parity(mut self, automation_parity: bool) -> Self {
327        self.automation_parity = automation_parity;
328        self
329    }
330
331    /// Set the environment pages should see.
332    pub fn fingerprint(mut self, fingerprint: FingerprintProfile) -> Self {
333        self.fingerprint = Some(fingerprint);
334        self
335    }
336
337    /// Manage this browser's downloads.
338    ///
339    /// # Arguments
340    ///
341    /// * `downloads` - `true` for the defaults, `false` for none, or
342    ///   [`DownloadOptions`](crate::downloads::DownloadOptions)
343    pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
344        self.downloads = downloads.into();
345        self
346    }
347
348    /// The Chrome arguments an engine launch passes: the opt-in restrictions,
349    /// then `args` and `extra_args`, with repeated feature-list switches
350    /// merged and, with automation parity, the `AutomationControlled` off
351    /// switch the engine's own switches need.
352    ///
353    /// Browser Commander adds nothing else since issue #103; the old defaults
354    /// are the `legacy-defaults` restriction preset.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error for an unknown restriction.
359    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    /// Engine default switches to suppress so the command line matches a
372    /// hand-started Chrome ([`LaunchMode::Engine`] only).
373    ///
374    /// Merged with the caller's `ignore_default_args`, because a switch the
375    /// engine appends after the caller's arguments cannot be countered by
376    /// passing a different value for it.
377    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    /// The environment the browser process gets on top of the parent's: the
392    /// restrictions' variables, then `env`. `None` when there is nothing to
393    /// add.
394    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    /// Get the user data directory, using a default if not specified.
407    #[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    /// The same launch expressed as [`RealBrowserOptions`].
422    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/// Browser metadata returned alongside a launched page.
458#[derive(Debug, Clone)]
459pub struct Browser {
460    /// The engine type being used.
461    pub engine: EngineType,
462    /// The user data directory.
463    pub user_data_dir: PathBuf,
464    /// Whether the browser is running headless.
465    pub headless: bool,
466}
467
468/// Result of a browser launch.
469///
470/// Contains static metadata (`browser` and the launch fields), a live
471/// [`EngineAdapter`] (`page`) that can be passed to the navigation,
472/// interaction, and query helpers exposed by this crate, and
473/// [`close`](Self::close).
474pub struct LaunchResult {
475    /// The browser metadata.
476    pub browser: Browser,
477    /// A live page/adapter tied to the launched browser.
478    ///
479    /// For `Chromiumoxide`, this is a
480    /// [`ChromiumoxidePage`](super::chromiumoxide_adapter::ChromiumoxidePage)
481    /// implementing [`EngineAdapter`]. Pass `launch_result.page.as_ref()` to
482    /// `goto`, `click`, `evaluate`, and other helpers.
483    pub page: Arc<dyn EngineAdapter>,
484    /// The download manager, when the caller asked for managed downloads.
485    ///
486    /// Downloads keep arriving while the browser is open, so the manager
487    /// outlives any single call: hold on to it, and call
488    /// [`DownloadManager::dispose`] before closing the browser.
489    pub downloads: Option<Arc<DownloadManager>>,
490    /// Who started the browser; `None` for a browser attached with
491    /// [`connect_browser`](super::connector::connect_browser).
492    pub launch: Option<LaunchMode>,
493    /// Whether `browser.user_data_dir` is a temporary profile that
494    /// [`close`](Self::close) deletes.
495    pub temporary_profile: bool,
496    /// The Chrome arguments Browser Commander passed (the engine adds its own
497    /// in [`LaunchMode::Engine`]).
498    pub args: Vec<String>,
499    /// The DevTools HTTP endpoint of a real launch.
500    pub cdp_endpoint: Option<String>,
501    /// The DevTools port of a real launch.
502    pub remote_debugging_port: Option<u16>,
503    /// The browser binary a real launch started, or the one requested for an
504    /// engine launch.
505    pub executable_path: Option<PathBuf>,
506    /// The browser process of a real launch (the engine owns it otherwise).
507    pub browser_process: Option<BrowserProcess>,
508    closer: Option<Arc<dyn BrowserCloser>>,
509}
510
511impl LaunchResult {
512    /// A browser somebody else started, attached over CDP.
513    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    /// Close the browser this launch started and delete its temporary
534    /// profile. Idempotent.
535    ///
536    /// A browser attached with
537    /// [`connect_browser`](super::connector::connect_browser) is managed by
538    /// whoever started it, so this does nothing for one.
539    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
564/// Launch a browser with the given options.
565///
566/// By default ([`LaunchMode::Real`]) Browser Commander starts the installed
567/// Chrome itself: `--user-data-dir=<fresh temporary profile>
568/// --remote-debugging-port=<reserved port> about:blank` and nothing else (plus
569/// `--headless=new`, restrictions and the caller's arguments when asked for),
570/// then attaches the engine over CDP. The browser behaves like one a person
571/// started by hand and `navigator.webdriver` stays false. Without an explicit
572/// `channel` or `executable_path` the installed Google Chrome is preferred
573/// and the engine's own browser is the fallback. Every restriction the
574/// library used to add silently is an explicit opt-in through
575/// `restrictions`.
576///
577/// [`LaunchMode::Engine`] keeps the chromiumoxide-, Playwright- or
578/// Puppeteer-launched browser for CI and headless use. Playwright and
579/// Puppeteer run as a local Node.js subprocess using the official Node
580/// package as a CLI bridge; the package must be available to Node module
581/// resolution, usually by running `npm install playwright` or `npm install
582/// puppeteer` in the configured `node_working_dir`.
583///
584/// Either way the profile is a fresh temporary one unless `user_data_dir` is
585/// set, and [`LaunchResult::close`] closes the browser and deletes it.
586///
587/// The `Fantoccini` engine is not yet implemented as a managed launcher; use
588/// chromiumoxide or connect to an externally-managed WebDriver session.
589///
590/// # Errors
591///
592/// Returns an error if the options are invalid or the browser fails to
593/// launch. Invalid options are refused before anything is started.
594pub 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    // Validate before anything is started or written to disk.
602    options.all_chrome_args()?;
603    refuse_unappliable_fingerprint(options.engine, options.fingerprint.as_ref())?;
604    // The node bridge has no CDP route, so a managed download would never be
605    // seen and every capture would time out.
606    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    // Bring the page to front so the address bar is not focused when running
643    // headful - mirrors the JS launcher's behavior.
644    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    /// Record an engine launch on an attached result.
667    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;