1use std::collections::HashMap;
18use std::future::Future;
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21use std::time::Duration;
22
23use anyhow::{anyhow, Result};
24use async_trait::async_trait;
25use chromiumoxide::Browser as CdpBrowser;
26use futures::StreamExt;
27use serde_json::Value;
28
29use crate::browser::browser_process::{BrowserCloser, BrowserProcess};
30use crate::browser::cdp_endpoint::{wait_for_cdp_endpoint, CdpEndpointRequest};
31use crate::browser::connector::ConnectOptions;
32use crate::browser::debugging_port::{
33 assert_fixed_debugging_port, reserve_loopback_port, DevToolsOutputWatcher, PortRaceError,
34};
35use crate::browser::launcher::Browser;
36use crate::browser::migration::{
37 migrate_profile, MigrateProfileOptions, MigrationSource, MigrationSummary,
38};
39use crate::browser::profile_directory::{
40 configure_user_data_dir_for_profile, create_temporary_user_data_dir_with_first_run,
41 prepare_user_data_dir_with_first_run, remove_user_data_dir,
42};
43use crate::browser::restrictions::{merge_feature_switches, resolve_restrictions};
44use crate::browser::storage_state::StorageStateInput;
45use crate::core::engine::{EngineAdapter, EngineType};
46use crate::downloads::{DownloadManager, DownloadSetting};
47use crate::fingerprint::automation_parity::{
48 apply_automation_parity_args, detect_automation_controlled_triggers,
49};
50use crate::utilities::{start_process, StartProcessOptions};
51
52use crate::browser::system_browser::{assert_cdp_browser, resolve_browser_executable};
53pub use crate::browser::system_browser::{
54 assert_dedicated_user_data_dir, default_real_browser_user_data_dir,
55};
56
57const MANAGED_ARGUMENTS: [&str; 4] = [
58 "--remote-debugging-address",
59 "--remote-debugging-port",
60 "--remote-debugging-pipe",
61 "--user-data-dir",
62];
63
64pub const DEFAULT_PORT_ATTEMPTS: u32 = 3;
66
67pub const DEFAULT_CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
69
70const RACE_EXIT_WAIT: Duration = Duration::from_secs(5);
72
73const FANTOCCINI_OVER_CDP: &str =
74 "fantoccini does not connect over CDP; use chromiumoxide, playwright, or puppeteer";
75
76#[derive(Debug, Clone)]
78pub struct RealBrowserOptions {
79 pub engine: EngineType,
81 pub channel: String,
83 pub executable_path: Option<PathBuf>,
85 pub user_data_dir: Option<PathBuf>,
88 pub profile_directory: String,
90 pub default_browser_check: Option<bool>,
92 pub first_run: bool,
94 pub preferences: Value,
96 pub local_state: Value,
98 pub storage_state: Option<StorageStateInput>,
100 pub remote_debugging_port: Option<u16>,
104 pub port_attempts: u32,
106 pub headless: bool,
108 pub restrictions: Vec<String>,
112 pub args: Vec<String>,
114 pub extra_args: Vec<String>,
116 pub ignore_default_args: Vec<String>,
119 pub ignore_all_default_args: bool,
121 pub env: Option<HashMap<String, String>>,
124 pub automation_parity: bool,
129 pub startup_timeout: Duration,
131 pub close_timeout: Duration,
133 pub slow_mo: u64,
135 pub timeout: Option<Duration>,
137 pub protocol_timeout: Option<Duration>,
139 pub seed_cookies: Vec<Value>,
141 pub migrate_from: Option<MigrationSource>,
143 pub migrate_include: Option<Vec<String>>,
145 pub migrate_domains: Vec<String>,
147 pub migrate_password_csv: Option<PathBuf>,
149 pub migrate_include_payment_cards: bool,
151 pub verbose: bool,
153 pub node_executable: Option<PathBuf>,
155 pub node_working_dir: Option<PathBuf>,
157 pub downloads: DownloadSetting,
165}
166
167impl Default for RealBrowserOptions {
168 fn default() -> Self {
169 Self {
170 engine: EngineType::Chromiumoxide,
171 channel: "chrome".to_string(),
172 executable_path: None,
173 user_data_dir: None,
174 profile_directory: "Default".into(),
175 default_browser_check: None,
176 first_run: false,
177 preferences: serde_json::json!({}),
178 local_state: serde_json::json!({}),
179 storage_state: None,
180 remote_debugging_port: None,
181 port_attempts: DEFAULT_PORT_ATTEMPTS,
182 headless: false,
183 restrictions: Vec::new(),
184 args: Vec::new(),
185 extra_args: Vec::new(),
186 ignore_default_args: Vec::new(),
187 ignore_all_default_args: false,
188 env: None,
189 automation_parity: true,
190 startup_timeout: Duration::from_secs(30),
191 close_timeout: DEFAULT_CLOSE_TIMEOUT,
192 slow_mo: 0,
193 timeout: None,
194 protocol_timeout: None,
195 seed_cookies: Vec::new(),
196 migrate_from: None,
197 migrate_include: None,
198 migrate_domains: Vec::new(),
199 migrate_password_csv: None,
200 migrate_include_payment_cards: false,
201 verbose: false,
202 node_executable: None,
203 node_working_dir: None,
204 downloads: DownloadSetting::Off,
205 }
206 }
207}
208
209impl RealBrowserOptions {
210 pub fn chromiumoxide() -> Self {
212 Self::default()
213 }
214
215 pub fn playwright() -> Self {
217 Self {
218 engine: EngineType::Playwright,
219 ..Self::default()
220 }
221 }
222
223 pub fn puppeteer() -> Self {
225 Self {
226 engine: EngineType::Puppeteer,
227 ..Self::default()
228 }
229 }
230
231 pub fn channel(mut self, channel: impl Into<String>) -> Self {
233 self.channel = channel.into();
234 self
235 }
236
237 pub fn executable_path(mut self, executable_path: impl Into<PathBuf>) -> Self {
239 self.executable_path = Some(executable_path.into());
240 self
241 }
242
243 pub fn user_data_dir(mut self, user_data_dir: impl Into<PathBuf>) -> Self {
245 self.user_data_dir = Some(user_data_dir.into());
246 self
247 }
248 pub fn storage_state(mut self, state: impl Into<StorageStateInput>) -> Self {
250 self.storage_state = Some(state.into());
251 self
252 }
253 pub fn remote_debugging_port(mut self, port: u16) -> Self {
256 self.remote_debugging_port = Some(port);
257 self
258 }
259
260 pub fn port_attempts(mut self, attempts: u32) -> Self {
262 self.port_attempts = attempts;
263 self
264 }
265
266 pub fn headless(mut self, headless: bool) -> Self {
268 self.headless = headless;
269 self
270 }
271
272 pub fn restrictions<I, S>(mut self, restrictions: I) -> Self
274 where
275 I: IntoIterator<Item = S>,
276 S: Into<String>,
277 {
278 self.restrictions = restrictions.into_iter().map(Into::into).collect();
279 self
280 }
281
282 pub fn with_args(mut self, args: Vec<String>) -> Self {
284 self.args = args;
285 self
286 }
287
288 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
290 self.extra_args = args;
291 self
292 }
293
294 #[deprecated(
296 since = "0.13.0",
297 note = "Browser Commander adds no default switches any more; opt in with `restrictions` instead"
298 )]
299 pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
300 self.ignore_default_args = args;
301 self
302 }
303
304 #[deprecated(
306 since = "0.13.0",
307 note = "Browser Commander adds no default switches any more; opt in with `restrictions` instead"
308 )]
309 pub fn ignore_all_default_args(mut self) -> Self {
310 self.ignore_all_default_args = true;
311 self
312 }
313
314 pub fn env(mut self, env: HashMap<String, String>) -> Self {
316 self.env = Some(env);
317 self
318 }
319
320 pub fn automation_parity(mut self, enabled: bool) -> Self {
322 self.automation_parity = enabled;
323 self
324 }
325
326 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
328 self.startup_timeout = timeout;
329 self
330 }
331
332 pub fn close_timeout(mut self, timeout: Duration) -> Self {
334 self.close_timeout = timeout;
335 self
336 }
337
338 pub fn slow_mo(mut self, milliseconds: u64) -> Self {
340 self.slow_mo = milliseconds;
341 self
342 }
343
344 pub fn timeout(mut self, timeout: Duration) -> Self {
346 self.timeout = Some(timeout);
347 self
348 }
349
350 pub fn protocol_timeout(mut self, timeout: Duration) -> Self {
352 self.protocol_timeout = Some(timeout);
353 self
354 }
355
356 pub fn seed_cookies(mut self, cookies: Vec<Value>) -> Self {
358 self.seed_cookies = cookies;
359 self
360 }
361
362 #[must_use]
364 pub fn migrate_from(mut self, source: MigrationSource) -> Self {
365 self.migrate_from = Some(source);
366 self
367 }
368
369 #[must_use]
371 pub fn migrate_include<I, S>(mut self, include: I) -> Self
372 where
373 I: IntoIterator<Item = S>,
374 S: Into<String>,
375 {
376 self.migrate_include = Some(include.into_iter().map(Into::into).collect());
377 self
378 }
379
380 #[must_use]
382 pub fn migrate_domains<I, S>(mut self, domains: I) -> Self
383 where
384 I: IntoIterator<Item = S>,
385 S: Into<String>,
386 {
387 self.migrate_domains = domains.into_iter().map(Into::into).collect();
388 self
389 }
390
391 pub fn verbose(mut self, verbose: bool) -> Self {
393 self.verbose = verbose;
394 self
395 }
396
397 pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
399 self.node_executable = Some(executable.into());
400 self
401 }
402
403 pub fn node_working_dir(mut self, directory: impl Into<PathBuf>) -> Self {
405 self.node_working_dir = Some(directory.into());
406 self
407 }
408
409 pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
416 self.downloads = downloads.into();
417 self
418 }
419
420 #[deprecated(
423 since = "0.13.0",
424 note = "launch_real_browser uses a fresh temporary profile unless user_data_dir is set; read RealBrowserLaunchResult::user_data_dir"
425 )]
426 pub fn get_user_data_dir(&self) -> PathBuf {
427 self.user_data_dir
428 .clone()
429 .unwrap_or_else(|| default_real_browser_user_data_dir(&self.channel))
430 }
431}
432
433pub struct RealBrowserLaunchResult {
435 pub browser: Browser,
437 pub page: Arc<dyn EngineAdapter>,
439 pub cdp_endpoint: String,
441 pub remote_debugging_port: u16,
443 pub executable_path: PathBuf,
445 pub user_data_dir: PathBuf,
447 pub temporary_profile: bool,
450 pub args: Vec<String>,
452 pub browser_process: BrowserProcess,
455 pub downloads: Option<Arc<DownloadManager>>,
460 pub migration: Option<MigrationSummary>,
462 pub(crate) closer: Arc<dyn BrowserCloser>,
463 pub webdriver: Option<Arc<crate::browser::webdriver::ManagedWebDriver>>,
465}
466
467impl RealBrowserLaunchResult {
468 pub async fn close(&self) -> Result<()> {
472 self.closer.close().await
473 }
474}
475
476impl std::fmt::Debug for RealBrowserLaunchResult {
477 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 formatter
479 .debug_struct("RealBrowserLaunchResult")
480 .field("browser", &self.browser)
481 .field("page", &"<dyn EngineAdapter>")
482 .field("cdp_endpoint", &self.cdp_endpoint)
483 .field("remote_debugging_port", &self.remote_debugging_port)
484 .field("executable_path", &self.executable_path)
485 .field("user_data_dir", &self.user_data_dir)
486 .field("temporary_profile", &self.temporary_profile)
487 .field("args", &self.args)
488 .field("browser_process", &self.browser_process)
489 .field("downloads", &self.downloads)
490 .field("migration", &self.migration)
491 .finish()
492 }
493}
494
495pub fn resolve_system_browser_executable(options: &RealBrowserOptions) -> Result<PathBuf> {
497 resolve_browser_executable(&options.channel, options.executable_path.as_deref())
498}
499
500fn assert_no_managed_arguments(options: &RealBrowserOptions) -> Result<()> {
501 for argument in options.args.iter().chain(&options.extra_args) {
502 if MANAGED_ARGUMENTS
503 .iter()
504 .any(|managed| argument == managed || argument.starts_with(&format!("{managed}=")))
505 {
506 return Err(anyhow!("{argument} is managed by launch_real_browser"));
507 }
508 }
509 Ok(())
510}
511
512pub const START_URL: &str = "about:blank";
521
522pub(crate) fn browser_args(
523 options: &RealBrowserOptions,
524 user_data_dir: &Path,
525 remote_debugging_port: u16,
526) -> Result<Vec<String>> {
527 let port = assert_fixed_debugging_port(remote_debugging_port)?;
528 assert_no_managed_arguments(options)?;
529 let mut arguments = vec![
530 format!("--user-data-dir={}", user_data_dir.display()),
531 format!("--remote-debugging-port={port}"),
532 ];
533 if options.headless {
534 arguments.push("--headless=new".to_string());
535 }
536 arguments.extend(resolve_restrictions(&options.restrictions)?.args);
537 arguments.extend(options.args.iter().cloned());
538 arguments.extend(options.extra_args.iter().cloned());
539 let arguments = merge_feature_switches(&arguments);
540 let mut arguments = if options.automation_parity
541 && !detect_automation_controlled_triggers(&arguments).is_empty()
542 {
543 apply_automation_parity_args(&arguments)
544 } else {
545 arguments
546 };
547 if arguments.iter().all(|argument| argument.starts_with('-')) {
548 arguments.push(START_URL.to_string());
549 }
550 Ok(arguments)
551}
552
553pub fn build_real_browser_args(options: &RealBrowserOptions) -> Result<Vec<String>> {
562 let user_data_dir = options.user_data_dir.as_deref().ok_or_else(|| {
563 anyhow!("build_real_browser_args needs user_data_dir; launch_real_browser creates a temporary profile when it is not set")
564 })?;
565 let port = options.remote_debugging_port.ok_or_else(|| {
566 anyhow!("build_real_browser_args needs remote_debugging_port; launch_real_browser reserves a free port when it is not set")
567 })?;
568 browser_args(options, user_data_dir, port)
569}
570
571fn validate_launch_request(options: &RealBrowserOptions) -> Result<()> {
572 assert_cdp_browser(&options.channel)?;
573 if options.engine == EngineType::Fantoccini {
574 return Err(anyhow!(FANTOCCINI_OVER_CDP));
575 }
576 if let Some(port) = options.remote_debugging_port {
577 assert_fixed_debugging_port(port)?;
578 }
579 browser_args(
580 options,
581 options
582 .user_data_dir
583 .as_deref()
584 .unwrap_or_else(|| Path::new("validation")),
585 options.remote_debugging_port.unwrap_or(1),
586 )?;
587 if let Some(user_data_dir) = &options.user_data_dir {
588 assert_dedicated_user_data_dir(user_data_dir)?;
589 }
590 Ok(())
591}
592
593fn browser_environment(options: &RealBrowserOptions) -> Result<Option<HashMap<String, String>>> {
594 let mut env = resolve_restrictions(&options.restrictions)?.env;
595 if let Some(extra) = &options.env {
596 env.extend(
597 extra
598 .iter()
599 .map(|(key, value)| (key.clone(), value.clone())),
600 );
601 }
602 Ok((!env.is_empty() || options.env.is_some()).then_some(env))
603}
604
605pub(crate) struct SpawnedBrowser {
608 pub(crate) process: BrowserProcess,
609 pub(crate) dev_tools_output: Option<DevToolsOutputWatcher>,
610}
611
612#[async_trait]
614pub(crate) trait LaunchHooks: Send + Sync {
615 fn resolve_executable(&self, options: &RealBrowserOptions) -> Result<PathBuf> {
616 resolve_system_browser_executable(options)
617 }
618
619 fn reserve_port(&self) -> Result<u16> {
620 reserve_loopback_port()
621 }
622
623 async fn spawn_browser(
624 &self,
625 executable_path: &Path,
626 args: &[String],
627 env: Option<HashMap<String, String>>,
628 verbose: bool,
629 ) -> Result<SpawnedBrowser> {
630 let file = executable_path.to_str().ok_or_else(|| {
631 anyhow!(
632 "browser executable path is not valid UTF-8: {}",
633 executable_path.display()
634 )
635 })?;
636 let watcher = DevToolsOutputWatcher::new();
637 let process = start_process(
638 file,
639 args,
640 StartProcessOptions {
641 env,
642 forward_output: verbose,
643 on_stderr: vec![watcher.listener()],
644 ..StartProcessOptions::default()
645 },
646 )
647 .await
648 .map_err(|error| anyhow!("failed to start installed browser {file}: {error}"))?;
649 Ok(SpawnedBrowser {
650 process: BrowserProcess::from_managed(process),
651 dev_tools_output: Some(watcher),
652 })
653 }
654
655 async fn wait_for_endpoint(&self, request: CdpEndpointRequest<'_>) -> Result<String> {
656 wait_for_cdp_endpoint(request).await
657 }
658
659 async fn request_close(&self, cdp_endpoint: &str, timeout: Duration) -> Result<()> {
661 let endpoint = cdp_endpoint.to_owned();
662 let close = async move {
663 let (mut browser, mut handler) = CdpBrowser::connect(endpoint).await?;
664 let handler_task = tokio::spawn(async move { while handler.next().await.is_some() {} });
665 let result = browser.close().await;
666 handler_task.abort();
667 result.map(|_| ()).map_err(anyhow::Error::from)
668 };
669 tokio::time::timeout(timeout, close)
670 .await
671 .map_err(|_| anyhow!("Browser.close did not answer within {timeout:?}"))?
672 }
673}
674
675pub(crate) struct SystemLaunchHooks;
677
678impl LaunchHooks for SystemLaunchHooks {}
679
680pub(crate) struct RealBrowserCloser {
681 hooks: Arc<dyn LaunchHooks>,
682 process: BrowserProcess,
683 cdp_endpoint: String,
684 user_data_dir: PathBuf,
685 temporary_profile: bool,
686 close_timeout: Duration,
687 closed: tokio::sync::OnceCell<()>,
688}
689
690#[async_trait]
691impl BrowserCloser for RealBrowserCloser {
692 async fn close(&self) -> Result<()> {
693 self.closed
694 .get_or_try_init(|| async {
695 if self.process.is_running() {
696 let _ = self
699 .hooks
700 .request_close(&self.cdp_endpoint, self.close_timeout)
701 .await;
702 }
703 if self
704 .process
705 .wait_timeout(self.close_timeout)
706 .await
707 .is_none()
708 {
709 self.process.kill();
710 self.process.wait_timeout(self.close_timeout).await;
711 }
712 if self.temporary_profile {
713 remove_user_data_dir(&self.user_data_dir).await?;
714 }
715 Ok::<(), anyhow::Error>(())
716 })
717 .await
718 .map(|_| ())
719 }
720}
721
722pub(crate) struct LaunchedRealBrowser {
724 pub(crate) cdp_endpoint: String,
725 pub(crate) remote_debugging_port: u16,
726 pub(crate) executable_path: PathBuf,
727 pub(crate) user_data_dir: PathBuf,
728 pub(crate) temporary_profile: bool,
729 pub(crate) args: Vec<String>,
730 pub(crate) browser_process: BrowserProcess,
731 pub(crate) migration: Option<MigrationSummary>,
732 pub(crate) closer: Arc<RealBrowserCloser>,
733}
734
735impl std::fmt::Debug for LaunchedRealBrowser {
736 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737 formatter
738 .debug_struct("LaunchedRealBrowser")
739 .field("cdp_endpoint", &self.cdp_endpoint)
740 .field("remote_debugging_port", &self.remote_debugging_port)
741 .field("executable_path", &self.executable_path)
742 .field("user_data_dir", &self.user_data_dir)
743 .field("temporary_profile", &self.temporary_profile)
744 .field("args", &self.args)
745 .field("browser_process", &self.browser_process)
746 .field("migration", &self.migration)
747 .finish()
748 }
749}
750
751struct Spawned {
752 process: BrowserProcess,
753 cdp_endpoint: String,
754 port: u16,
755 args: Vec<String>,
756}
757
758async fn spawn_on_free_port(
759 options: &RealBrowserOptions,
760 hooks: &dyn LaunchHooks,
761 executable_path: &Path,
762 user_data_dir: &Path,
763 env: Option<HashMap<String, String>>,
764) -> Result<Spawned> {
765 let attempts = match options.remote_debugging_port {
766 Some(_) => 1,
767 None => options.port_attempts.max(1),
768 };
769 let mut attempt = 1;
770 loop {
771 let port = match options.remote_debugging_port {
772 Some(port) => port,
773 None => hooks.reserve_port()?,
774 };
775 let args = browser_args(options, user_data_dir, port)?;
776 if options.verbose {
777 tracing::info!(executable = %executable_path.display(), ?args, "starting installed browser");
778 }
779 let spawned = hooks
780 .spawn_browser(executable_path, &args, env.clone(), options.verbose)
781 .await?;
782 let waited = hooks
783 .wait_for_endpoint(CdpEndpointRequest {
784 remote_debugging_port: port,
785 user_data_dir,
786 browser_process: &spawned.process,
787 dev_tools_output: spawned.dev_tools_output.as_ref(),
788 timeout: options.startup_timeout,
789 })
790 .await;
791 match waited {
792 Ok(cdp_endpoint) => {
793 return Ok(Spawned {
794 process: spawned.process,
795 cdp_endpoint,
796 port,
797 args,
798 })
799 }
800 Err(error) => {
801 spawned.process.kill();
802 if error.downcast_ref::<PortRaceError>().is_none() || attempt >= attempts {
803 return Err(error);
804 }
805 if options.verbose {
806 tracing::info!("{error}; retrying with a new port");
807 }
808 spawned.process.wait_timeout(RACE_EXIT_WAIT).await;
809 attempt += 1;
810 }
811 }
812 }
813}
814
815pub(crate) async fn launch_real_browser_with_owned<T, C, F>(
817 options: &RealBrowserOptions,
818 hooks: Arc<dyn LaunchHooks>,
819 connect: C,
820 owned_profile: bool,
821) -> Result<(T, LaunchedRealBrowser)>
822where
823 C: FnOnce(ConnectOptions) -> F,
824 F: Future<Output = Result<T>>,
825{
826 validate_launch_request(options)?;
827 if let Some(state) = &options.storage_state {
828 state.load()?;
829 }
830 let executable_path = hooks.resolve_executable(options)?;
831 let temporary_profile = options.user_data_dir.is_none() || owned_profile;
832 let user_data_dir = match &options.user_data_dir {
833 Some(user_data_dir) => {
834 prepare_user_data_dir_with_first_run(user_data_dir, options.first_run)?
835 }
836 None => create_temporary_user_data_dir_with_first_run(None, options.first_run)?,
837 };
838 let (migration, migrated_cookies) = if let Some(source) = options.migrate_from.clone() {
839 let target = user_data_dir.join("Default");
840 let mut migrate_options = MigrateProfileOptions::new(source, target);
841 migrate_options.target_browser = Some(options.channel.clone());
842 if let Some(include) = &options.migrate_include {
843 migrate_options.include = include.clone();
844 }
845 migrate_options.domains = options.migrate_domains.clone();
846 migrate_options.password_csv = options.migrate_password_csv.clone();
847 migrate_options.include_payment_cards = options.migrate_include_payment_cards;
848 let result = tokio::task::spawn_blocking(move || migrate_profile(migrate_options)).await;
849 let report = match result {
850 Ok(Ok(report)) => report,
851 Ok(Err(error)) => {
852 if temporary_profile {
853 let _ = remove_user_data_dir(&user_data_dir).await;
854 }
855 return Err(error);
856 }
857 Err(error) => {
858 if temporary_profile {
859 let _ = remove_user_data_dir(&user_data_dir).await;
860 }
861 return Err(error.into());
862 }
863 };
864 let (cookies, summary) = report.into_parts();
865 let values = cookies
866 .into_iter()
867 .map(serde_json::to_value)
868 .collect::<std::result::Result<Vec<_>, _>>()?;
869 (Some(summary), values)
870 } else {
871 (None, Vec::new())
872 };
873 if let Err(error) = configure_user_data_dir_for_profile(
874 &user_data_dir,
875 &options.profile_directory,
876 options.default_browser_check,
877 &options.preferences,
878 &options.local_state,
879 ) {
880 if temporary_profile {
881 let _ = remove_user_data_dir(&user_data_dir).await;
882 }
883 return Err(error);
884 }
885 let env = browser_environment(options)?;
886
887 let spawned = match spawn_on_free_port(
888 options,
889 hooks.as_ref(),
890 &executable_path,
891 &user_data_dir,
892 env,
893 )
894 .await
895 {
896 Ok(spawned) => spawned,
897 Err(error) => {
898 if temporary_profile {
899 let _ = remove_user_data_dir(&user_data_dir).await;
900 }
901 return Err(error);
902 }
903 };
904 let process = spawned.process;
905 if temporary_profile {
906 let exited = process.exited();
909 let directory = user_data_dir.clone();
910 tokio::spawn(async move {
911 exited.await;
912 let _ = remove_user_data_dir(&directory).await;
913 });
914 }
915
916 let connection = match connection_options(options, &spawned.cdp_endpoint) {
917 Ok(mut connect_options) => {
918 connect_options.seed_cookies.extend(migrated_cookies);
919 connect(connect_options).await
920 }
921 Err(error) => Err(error),
922 };
923 let connection = match connection {
924 Ok(connection) => connection,
925 Err(error) => {
926 process.kill();
927 process.wait_timeout(options.close_timeout).await;
928 if temporary_profile {
929 let _ = remove_user_data_dir(&user_data_dir).await;
930 }
931 return Err(error);
932 }
933 };
934
935 let closer = Arc::new(RealBrowserCloser {
936 hooks,
937 process: process.clone(),
938 cdp_endpoint: spawned.cdp_endpoint.clone(),
939 user_data_dir: user_data_dir.clone(),
940 temporary_profile,
941 close_timeout: options.close_timeout,
942 closed: tokio::sync::OnceCell::new(),
943 });
944 Ok((
945 connection,
946 LaunchedRealBrowser {
947 cdp_endpoint: spawned.cdp_endpoint,
948 remote_debugging_port: spawned.port,
949 executable_path,
950 user_data_dir,
951 temporary_profile,
952 args: spawned.args,
953 browser_process: process,
954 migration,
955 closer,
956 },
957 ))
958}
959
960pub async fn launch_real_browser(options: RealBrowserOptions) -> Result<RealBrowserLaunchResult> {
970 if crate::browser::safari::is_safari_channel(&options.channel) {
971 return crate::browser::safari::launch_safari_real(options).await;
972 }
973 launch_real_browser_owned(options, false).await
974}
975
976pub async fn launch_and_connect_real_browser(
978 options: RealBrowserOptions,
979) -> Result<RealBrowserLaunchResult> {
980 launch_real_browser(options).await
981}
982
983#[path = "real_browser_result.rs"]
984mod result;
985pub(crate) use result::{connection_options, launch_real_browser_owned, launch_real_browser_with};
986
987#[cfg(test)]
988#[path = "real_browser_tests.rs"]
989mod tests;