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::{connect_browser, 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::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,
134 pub slow_mo: u64,
136 pub timeout: Option<Duration>,
138 pub protocol_timeout: Option<Duration>,
140 pub seed_cookies: Vec<Value>,
142 pub migrate_from: Option<MigrationSource>,
144 pub migrate_include: Option<Vec<String>>,
146 pub migrate_domains: Vec<String>,
148 pub verbose: bool,
150 pub node_executable: Option<PathBuf>,
152 pub node_working_dir: Option<PathBuf>,
154 pub downloads: DownloadSetting,
162}
163
164impl Default for RealBrowserOptions {
165 fn default() -> Self {
166 Self {
167 engine: EngineType::Chromiumoxide,
168 channel: "chrome".to_string(),
169 executable_path: None,
170 user_data_dir: None,
171 profile_directory: "Default".into(),
172 default_browser_check: None,
173 first_run: false,
174 preferences: serde_json::json!({}),
175 local_state: serde_json::json!({}),
176 storage_state: None,
177 remote_debugging_port: None,
178 port_attempts: DEFAULT_PORT_ATTEMPTS,
179 headless: false,
180 restrictions: Vec::new(),
181 args: Vec::new(),
182 extra_args: Vec::new(),
183 ignore_default_args: Vec::new(),
184 ignore_all_default_args: false,
185 env: None,
186 automation_parity: true,
187 startup_timeout: Duration::from_secs(30),
188 close_timeout: DEFAULT_CLOSE_TIMEOUT,
189 slow_mo: 0,
190 timeout: None,
191 protocol_timeout: None,
192 seed_cookies: Vec::new(),
193 migrate_from: None,
194 migrate_include: None,
195 migrate_domains: Vec::new(),
196 verbose: false,
197 node_executable: None,
198 node_working_dir: None,
199 downloads: DownloadSetting::Off,
200 }
201 }
202}
203
204impl RealBrowserOptions {
205 pub fn chromiumoxide() -> Self {
207 Self::default()
208 }
209
210 pub fn playwright() -> Self {
212 Self {
213 engine: EngineType::Playwright,
214 ..Self::default()
215 }
216 }
217
218 pub fn puppeteer() -> Self {
220 Self {
221 engine: EngineType::Puppeteer,
222 ..Self::default()
223 }
224 }
225
226 pub fn channel(mut self, channel: impl Into<String>) -> Self {
228 self.channel = channel.into();
229 self
230 }
231
232 pub fn executable_path(mut self, executable_path: impl Into<PathBuf>) -> Self {
234 self.executable_path = Some(executable_path.into());
235 self
236 }
237
238 pub fn user_data_dir(mut self, user_data_dir: impl Into<PathBuf>) -> Self {
240 self.user_data_dir = Some(user_data_dir.into());
241 self
242 }
243 pub fn storage_state(mut self, state: impl Into<StorageStateInput>) -> Self {
245 self.storage_state = Some(state.into());
246 self
247 }
248 pub fn remote_debugging_port(mut self, port: u16) -> Self {
251 self.remote_debugging_port = Some(port);
252 self
253 }
254
255 pub fn port_attempts(mut self, attempts: u32) -> Self {
257 self.port_attempts = attempts;
258 self
259 }
260
261 pub fn headless(mut self, headless: bool) -> Self {
263 self.headless = headless;
264 self
265 }
266
267 pub fn restrictions<I, S>(mut self, restrictions: I) -> Self
269 where
270 I: IntoIterator<Item = S>,
271 S: Into<String>,
272 {
273 self.restrictions = restrictions.into_iter().map(Into::into).collect();
274 self
275 }
276
277 pub fn with_args(mut self, args: Vec<String>) -> Self {
279 self.args = args;
280 self
281 }
282
283 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
285 self.extra_args = args;
286 self
287 }
288
289 #[deprecated(
291 since = "0.13.0",
292 note = "Browser Commander adds no default switches any more; opt in with `restrictions` instead"
293 )]
294 pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
295 self.ignore_default_args = args;
296 self
297 }
298
299 #[deprecated(
301 since = "0.13.0",
302 note = "Browser Commander adds no default switches any more; opt in with `restrictions` instead"
303 )]
304 pub fn ignore_all_default_args(mut self) -> Self {
305 self.ignore_all_default_args = true;
306 self
307 }
308
309 pub fn env(mut self, env: HashMap<String, String>) -> Self {
311 self.env = Some(env);
312 self
313 }
314
315 pub fn automation_parity(mut self, enabled: bool) -> Self {
317 self.automation_parity = enabled;
318 self
319 }
320
321 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
323 self.startup_timeout = timeout;
324 self
325 }
326
327 pub fn close_timeout(mut self, timeout: Duration) -> Self {
329 self.close_timeout = timeout;
330 self
331 }
332
333 pub fn slow_mo(mut self, milliseconds: u64) -> Self {
335 self.slow_mo = milliseconds;
336 self
337 }
338
339 pub fn timeout(mut self, timeout: Duration) -> Self {
341 self.timeout = Some(timeout);
342 self
343 }
344
345 pub fn protocol_timeout(mut self, timeout: Duration) -> Self {
347 self.protocol_timeout = Some(timeout);
348 self
349 }
350
351 pub fn seed_cookies(mut self, cookies: Vec<Value>) -> Self {
353 self.seed_cookies = cookies;
354 self
355 }
356
357 #[must_use]
359 pub fn migrate_from(mut self, source: MigrationSource) -> Self {
360 self.migrate_from = Some(source);
361 self
362 }
363
364 #[must_use]
366 pub fn migrate_include<I, S>(mut self, include: I) -> Self
367 where
368 I: IntoIterator<Item = S>,
369 S: Into<String>,
370 {
371 self.migrate_include = Some(include.into_iter().map(Into::into).collect());
372 self
373 }
374
375 #[must_use]
377 pub fn migrate_domains<I, S>(mut self, domains: I) -> Self
378 where
379 I: IntoIterator<Item = S>,
380 S: Into<String>,
381 {
382 self.migrate_domains = domains.into_iter().map(Into::into).collect();
383 self
384 }
385
386 pub fn verbose(mut self, verbose: bool) -> Self {
388 self.verbose = verbose;
389 self
390 }
391
392 pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
394 self.node_executable = Some(executable.into());
395 self
396 }
397
398 pub fn node_working_dir(mut self, directory: impl Into<PathBuf>) -> Self {
400 self.node_working_dir = Some(directory.into());
401 self
402 }
403
404 pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
411 self.downloads = downloads.into();
412 self
413 }
414
415 #[deprecated(
418 since = "0.13.0",
419 note = "launch_real_browser uses a fresh temporary profile unless user_data_dir is set; read RealBrowserLaunchResult::user_data_dir"
420 )]
421 pub fn get_user_data_dir(&self) -> PathBuf {
422 self.user_data_dir
423 .clone()
424 .unwrap_or_else(|| default_real_browser_user_data_dir(&self.channel))
425 }
426}
427
428pub struct RealBrowserLaunchResult {
430 pub browser: Browser,
432 pub page: Arc<dyn EngineAdapter>,
434 pub cdp_endpoint: String,
436 pub remote_debugging_port: u16,
438 pub executable_path: PathBuf,
440 pub user_data_dir: PathBuf,
442 pub temporary_profile: bool,
445 pub args: Vec<String>,
447 pub browser_process: BrowserProcess,
450 pub downloads: Option<Arc<DownloadManager>>,
455 pub migration: Option<MigrationSummary>,
457 closer: Arc<RealBrowserCloser>,
458}
459
460impl RealBrowserLaunchResult {
461 pub async fn close(&self) -> Result<()> {
465 self.closer.close().await
466 }
467}
468
469impl std::fmt::Debug for RealBrowserLaunchResult {
470 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 formatter
472 .debug_struct("RealBrowserLaunchResult")
473 .field("browser", &self.browser)
474 .field("page", &"<dyn EngineAdapter>")
475 .field("cdp_endpoint", &self.cdp_endpoint)
476 .field("remote_debugging_port", &self.remote_debugging_port)
477 .field("executable_path", &self.executable_path)
478 .field("user_data_dir", &self.user_data_dir)
479 .field("temporary_profile", &self.temporary_profile)
480 .field("args", &self.args)
481 .field("browser_process", &self.browser_process)
482 .field("downloads", &self.downloads)
483 .field("migration", &self.migration)
484 .finish()
485 }
486}
487
488pub fn resolve_system_browser_executable(options: &RealBrowserOptions) -> Result<PathBuf> {
490 resolve_browser_executable(&options.channel, options.executable_path.as_deref())
491}
492
493fn assert_no_managed_arguments(options: &RealBrowserOptions) -> Result<()> {
494 for argument in options.args.iter().chain(&options.extra_args) {
495 if MANAGED_ARGUMENTS
496 .iter()
497 .any(|managed| argument == managed || argument.starts_with(&format!("{managed}=")))
498 {
499 return Err(anyhow!("{argument} is managed by launch_real_browser"));
500 }
501 }
502 Ok(())
503}
504
505pub const START_URL: &str = "about:blank";
514
515pub(crate) fn browser_args(
516 options: &RealBrowserOptions,
517 user_data_dir: &Path,
518 remote_debugging_port: u16,
519) -> Result<Vec<String>> {
520 let port = assert_fixed_debugging_port(remote_debugging_port)?;
521 assert_no_managed_arguments(options)?;
522 let mut arguments = vec![
523 format!("--user-data-dir={}", user_data_dir.display()),
524 format!("--remote-debugging-port={port}"),
525 ];
526 if options.headless {
527 arguments.push("--headless=new".to_string());
528 }
529 arguments.extend(resolve_restrictions(&options.restrictions)?.args);
530 arguments.extend(options.args.iter().cloned());
531 arguments.extend(options.extra_args.iter().cloned());
532 let arguments = merge_feature_switches(&arguments);
533 let mut arguments = if options.automation_parity
534 && !detect_automation_controlled_triggers(&arguments).is_empty()
535 {
536 apply_automation_parity_args(&arguments)
537 } else {
538 arguments
539 };
540 if arguments.iter().all(|argument| argument.starts_with('-')) {
541 arguments.push(START_URL.to_string());
542 }
543 Ok(arguments)
544}
545
546pub fn build_real_browser_args(options: &RealBrowserOptions) -> Result<Vec<String>> {
555 let user_data_dir = options.user_data_dir.as_deref().ok_or_else(|| {
556 anyhow!("build_real_browser_args needs user_data_dir; launch_real_browser creates a temporary profile when it is not set")
557 })?;
558 let port = options.remote_debugging_port.ok_or_else(|| {
559 anyhow!("build_real_browser_args needs remote_debugging_port; launch_real_browser reserves a free port when it is not set")
560 })?;
561 browser_args(options, user_data_dir, port)
562}
563
564fn validate_launch_request(options: &RealBrowserOptions) -> Result<()> {
565 if options.engine == EngineType::Fantoccini {
566 return Err(anyhow!(FANTOCCINI_OVER_CDP));
567 }
568 if let Some(port) = options.remote_debugging_port {
569 assert_fixed_debugging_port(port)?;
570 }
571 browser_args(
572 options,
573 options
574 .user_data_dir
575 .as_deref()
576 .unwrap_or_else(|| Path::new("validation")),
577 options.remote_debugging_port.unwrap_or(1),
578 )?;
579 if let Some(user_data_dir) = &options.user_data_dir {
580 assert_dedicated_user_data_dir(user_data_dir)?;
581 }
582 Ok(())
583}
584
585fn browser_environment(options: &RealBrowserOptions) -> Result<Option<HashMap<String, String>>> {
586 let mut env = resolve_restrictions(&options.restrictions)?.env;
587 if let Some(extra) = &options.env {
588 env.extend(
589 extra
590 .iter()
591 .map(|(key, value)| (key.clone(), value.clone())),
592 );
593 }
594 Ok((!env.is_empty() || options.env.is_some()).then_some(env))
595}
596
597pub(crate) struct SpawnedBrowser {
600 pub(crate) process: BrowserProcess,
601 pub(crate) dev_tools_output: Option<DevToolsOutputWatcher>,
602}
603
604#[async_trait]
606pub(crate) trait LaunchHooks: Send + Sync {
607 fn resolve_executable(&self, options: &RealBrowserOptions) -> Result<PathBuf> {
608 resolve_system_browser_executable(options)
609 }
610
611 fn reserve_port(&self) -> Result<u16> {
612 reserve_loopback_port()
613 }
614
615 async fn spawn_browser(
616 &self,
617 executable_path: &Path,
618 args: &[String],
619 env: Option<HashMap<String, String>>,
620 verbose: bool,
621 ) -> Result<SpawnedBrowser> {
622 let file = executable_path.to_str().ok_or_else(|| {
623 anyhow!(
624 "browser executable path is not valid UTF-8: {}",
625 executable_path.display()
626 )
627 })?;
628 let watcher = DevToolsOutputWatcher::new();
629 let process = start_process(
630 file,
631 args,
632 StartProcessOptions {
633 env,
634 forward_output: verbose,
635 on_stderr: vec![watcher.listener()],
636 ..StartProcessOptions::default()
637 },
638 )
639 .await
640 .map_err(|error| anyhow!("failed to start installed browser {file}: {error}"))?;
641 Ok(SpawnedBrowser {
642 process: BrowserProcess::from_managed(process),
643 dev_tools_output: Some(watcher),
644 })
645 }
646
647 async fn wait_for_endpoint(&self, request: CdpEndpointRequest<'_>) -> Result<String> {
648 wait_for_cdp_endpoint(request).await
649 }
650
651 async fn request_close(&self, cdp_endpoint: &str, timeout: Duration) -> Result<()> {
653 let endpoint = cdp_endpoint.to_owned();
654 let close = async move {
655 let (mut browser, mut handler) = CdpBrowser::connect(endpoint).await?;
656 let handler_task = tokio::spawn(async move { while handler.next().await.is_some() {} });
657 let result = browser.close().await;
658 handler_task.abort();
659 result.map(|_| ()).map_err(anyhow::Error::from)
660 };
661 tokio::time::timeout(timeout, close)
662 .await
663 .map_err(|_| anyhow!("Browser.close did not answer within {timeout:?}"))?
664 }
665}
666
667pub(crate) struct SystemLaunchHooks;
669
670impl LaunchHooks for SystemLaunchHooks {}
671
672pub(crate) struct RealBrowserCloser {
673 hooks: Arc<dyn LaunchHooks>,
674 process: BrowserProcess,
675 cdp_endpoint: String,
676 user_data_dir: PathBuf,
677 temporary_profile: bool,
678 close_timeout: Duration,
679 closed: tokio::sync::OnceCell<()>,
680}
681
682#[async_trait]
683impl BrowserCloser for RealBrowserCloser {
684 async fn close(&self) -> Result<()> {
685 self.closed
686 .get_or_try_init(|| async {
687 if self.process.is_running() {
688 let _ = self
691 .hooks
692 .request_close(&self.cdp_endpoint, self.close_timeout)
693 .await;
694 }
695 if self
696 .process
697 .wait_timeout(self.close_timeout)
698 .await
699 .is_none()
700 {
701 self.process.kill();
702 self.process.wait_timeout(self.close_timeout).await;
703 }
704 if self.temporary_profile {
705 remove_user_data_dir(&self.user_data_dir).await?;
706 }
707 Ok::<(), anyhow::Error>(())
708 })
709 .await
710 .map(|_| ())
711 }
712}
713
714pub(crate) struct LaunchedRealBrowser {
716 pub(crate) cdp_endpoint: String,
717 pub(crate) remote_debugging_port: u16,
718 pub(crate) executable_path: PathBuf,
719 pub(crate) user_data_dir: PathBuf,
720 pub(crate) temporary_profile: bool,
721 pub(crate) args: Vec<String>,
722 pub(crate) browser_process: BrowserProcess,
723 pub(crate) migration: Option<MigrationSummary>,
724 pub(crate) closer: Arc<RealBrowserCloser>,
725}
726
727impl std::fmt::Debug for LaunchedRealBrowser {
728 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729 formatter
730 .debug_struct("LaunchedRealBrowser")
731 .field("cdp_endpoint", &self.cdp_endpoint)
732 .field("remote_debugging_port", &self.remote_debugging_port)
733 .field("executable_path", &self.executable_path)
734 .field("user_data_dir", &self.user_data_dir)
735 .field("temporary_profile", &self.temporary_profile)
736 .field("args", &self.args)
737 .field("browser_process", &self.browser_process)
738 .field("migration", &self.migration)
739 .finish()
740 }
741}
742
743struct Spawned {
744 process: BrowserProcess,
745 cdp_endpoint: String,
746 port: u16,
747 args: Vec<String>,
748}
749
750async fn spawn_on_free_port(
751 options: &RealBrowserOptions,
752 hooks: &dyn LaunchHooks,
753 executable_path: &Path,
754 user_data_dir: &Path,
755 env: Option<HashMap<String, String>>,
756) -> Result<Spawned> {
757 let attempts = match options.remote_debugging_port {
758 Some(_) => 1,
759 None => options.port_attempts.max(1),
760 };
761 let mut attempt = 1;
762 loop {
763 let port = match options.remote_debugging_port {
764 Some(port) => port,
765 None => hooks.reserve_port()?,
766 };
767 let args = browser_args(options, user_data_dir, port)?;
768 if options.verbose {
769 tracing::info!(executable = %executable_path.display(), ?args, "starting installed browser");
770 }
771 let spawned = hooks
772 .spawn_browser(executable_path, &args, env.clone(), options.verbose)
773 .await?;
774 let waited = hooks
775 .wait_for_endpoint(CdpEndpointRequest {
776 remote_debugging_port: port,
777 user_data_dir,
778 browser_process: &spawned.process,
779 dev_tools_output: spawned.dev_tools_output.as_ref(),
780 timeout: options.startup_timeout,
781 })
782 .await;
783 match waited {
784 Ok(cdp_endpoint) => {
785 return Ok(Spawned {
786 process: spawned.process,
787 cdp_endpoint,
788 port,
789 args,
790 })
791 }
792 Err(error) => {
793 spawned.process.kill();
794 if error.downcast_ref::<PortRaceError>().is_none() || attempt >= attempts {
795 return Err(error);
796 }
797 if options.verbose {
798 tracing::info!("{error}; retrying with a new port");
799 }
800 spawned.process.wait_timeout(RACE_EXIT_WAIT).await;
801 attempt += 1;
802 }
803 }
804 }
805}
806
807pub(crate) async fn launch_real_browser_with_owned<T, C, F>(
809 options: &RealBrowserOptions,
810 hooks: Arc<dyn LaunchHooks>,
811 connect: C,
812 owned_profile: bool,
813) -> Result<(T, LaunchedRealBrowser)>
814where
815 C: FnOnce(ConnectOptions) -> F,
816 F: Future<Output = Result<T>>,
817{
818 validate_launch_request(options)?;
819 if let Some(state) = &options.storage_state {
820 state.load()?;
821 }
822 let executable_path = hooks.resolve_executable(options)?;
823 let temporary_profile = options.user_data_dir.is_none() || owned_profile;
824 let user_data_dir = match &options.user_data_dir {
825 Some(user_data_dir) => {
826 prepare_user_data_dir_with_first_run(user_data_dir, options.first_run)?
827 }
828 None => create_temporary_user_data_dir_with_first_run(None, options.first_run)?,
829 };
830 let (migration, migrated_cookies) = if let Some(source) = options.migrate_from.clone() {
831 let target = user_data_dir.join("Default");
832 let mut migrate_options = MigrateProfileOptions::new(source, target);
833 migrate_options.target_browser = Some(options.channel.clone());
834 if let Some(include) = &options.migrate_include {
835 migrate_options.include = include.clone();
836 }
837 migrate_options.domains = options.migrate_domains.clone();
838 let result = tokio::task::spawn_blocking(move || migrate_profile(migrate_options)).await;
839 let report = match result {
840 Ok(Ok(report)) => report,
841 Ok(Err(error)) => {
842 if temporary_profile {
843 let _ = remove_user_data_dir(&user_data_dir).await;
844 }
845 return Err(error);
846 }
847 Err(error) => {
848 if temporary_profile {
849 let _ = remove_user_data_dir(&user_data_dir).await;
850 }
851 return Err(error.into());
852 }
853 };
854 let (cookies, summary) = report.into_parts();
855 let values = cookies
856 .into_iter()
857 .map(serde_json::to_value)
858 .collect::<std::result::Result<Vec<_>, _>>()?;
859 (Some(summary), values)
860 } else {
861 (None, Vec::new())
862 };
863 if let Err(error) = configure_user_data_dir_for_profile(
864 &user_data_dir,
865 &options.profile_directory,
866 options.default_browser_check,
867 &options.preferences,
868 &options.local_state,
869 ) {
870 if temporary_profile {
871 let _ = remove_user_data_dir(&user_data_dir).await;
872 }
873 return Err(error);
874 }
875 let env = browser_environment(options)?;
876
877 let spawned = match spawn_on_free_port(
878 options,
879 hooks.as_ref(),
880 &executable_path,
881 &user_data_dir,
882 env,
883 )
884 .await
885 {
886 Ok(spawned) => spawned,
887 Err(error) => {
888 if temporary_profile {
889 let _ = remove_user_data_dir(&user_data_dir).await;
890 }
891 return Err(error);
892 }
893 };
894 let process = spawned.process;
895 if temporary_profile {
896 let exited = process.exited();
899 let directory = user_data_dir.clone();
900 tokio::spawn(async move {
901 exited.await;
902 let _ = remove_user_data_dir(&directory).await;
903 });
904 }
905
906 let connection = match connection_options(options, &spawned.cdp_endpoint) {
907 Ok(mut connect_options) => {
908 connect_options.seed_cookies.extend(migrated_cookies);
909 connect(connect_options).await
910 }
911 Err(error) => Err(error),
912 };
913 let connection = match connection {
914 Ok(connection) => connection,
915 Err(error) => {
916 process.kill();
917 process.wait_timeout(options.close_timeout).await;
918 if temporary_profile {
919 let _ = remove_user_data_dir(&user_data_dir).await;
920 }
921 return Err(error);
922 }
923 };
924
925 let closer = Arc::new(RealBrowserCloser {
926 hooks,
927 process: process.clone(),
928 cdp_endpoint: spawned.cdp_endpoint.clone(),
929 user_data_dir: user_data_dir.clone(),
930 temporary_profile,
931 close_timeout: options.close_timeout,
932 closed: tokio::sync::OnceCell::new(),
933 });
934 Ok((
935 connection,
936 LaunchedRealBrowser {
937 cdp_endpoint: spawned.cdp_endpoint,
938 remote_debugging_port: spawned.port,
939 executable_path,
940 user_data_dir,
941 temporary_profile,
942 args: spawned.args,
943 browser_process: process,
944 migration,
945 closer,
946 },
947 ))
948}
949
950pub async fn launch_real_browser(options: RealBrowserOptions) -> Result<RealBrowserLaunchResult> {
960 launch_real_browser_owned(options, false).await
961}
962
963pub(crate) async fn launch_real_browser_owned(
964 options: RealBrowserOptions,
965 owned_profile: bool,
966) -> Result<RealBrowserLaunchResult> {
967 let (connection, launched) = launch_real_browser_with_owned(
968 &options,
969 Arc::new(SystemLaunchHooks),
970 connect_browser,
971 owned_profile,
972 )
973 .await?;
974 Ok(real_browser_result(connection, launched, options.headless))
975}
976
977pub async fn launch_and_connect_real_browser(
979 options: RealBrowserOptions,
980) -> Result<RealBrowserLaunchResult> {
981 launch_real_browser(options).await
982}
983
984#[path = "real_browser_result.rs"]
985mod result;
986use result::real_browser_result;
987pub(crate) use result::{connection_options, launch_real_browser_with};
988
989#[cfg(test)]
990#[path = "real_browser_tests.rs"]
991mod tests;