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::{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 closer: Arc<RealBrowserCloser>,
463}
464
465impl RealBrowserLaunchResult {
466 pub async fn close(&self) -> Result<()> {
470 self.closer.close().await
471 }
472}
473
474impl std::fmt::Debug for RealBrowserLaunchResult {
475 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 formatter
477 .debug_struct("RealBrowserLaunchResult")
478 .field("browser", &self.browser)
479 .field("page", &"<dyn EngineAdapter>")
480 .field("cdp_endpoint", &self.cdp_endpoint)
481 .field("remote_debugging_port", &self.remote_debugging_port)
482 .field("executable_path", &self.executable_path)
483 .field("user_data_dir", &self.user_data_dir)
484 .field("temporary_profile", &self.temporary_profile)
485 .field("args", &self.args)
486 .field("browser_process", &self.browser_process)
487 .field("downloads", &self.downloads)
488 .field("migration", &self.migration)
489 .finish()
490 }
491}
492
493pub fn resolve_system_browser_executable(options: &RealBrowserOptions) -> Result<PathBuf> {
495 resolve_browser_executable(&options.channel, options.executable_path.as_deref())
496}
497
498fn assert_no_managed_arguments(options: &RealBrowserOptions) -> Result<()> {
499 for argument in options.args.iter().chain(&options.extra_args) {
500 if MANAGED_ARGUMENTS
501 .iter()
502 .any(|managed| argument == managed || argument.starts_with(&format!("{managed}=")))
503 {
504 return Err(anyhow!("{argument} is managed by launch_real_browser"));
505 }
506 }
507 Ok(())
508}
509
510pub const START_URL: &str = "about:blank";
519
520pub(crate) fn browser_args(
521 options: &RealBrowserOptions,
522 user_data_dir: &Path,
523 remote_debugging_port: u16,
524) -> Result<Vec<String>> {
525 let port = assert_fixed_debugging_port(remote_debugging_port)?;
526 assert_no_managed_arguments(options)?;
527 let mut arguments = vec![
528 format!("--user-data-dir={}", user_data_dir.display()),
529 format!("--remote-debugging-port={port}"),
530 ];
531 if options.headless {
532 arguments.push("--headless=new".to_string());
533 }
534 arguments.extend(resolve_restrictions(&options.restrictions)?.args);
535 arguments.extend(options.args.iter().cloned());
536 arguments.extend(options.extra_args.iter().cloned());
537 let arguments = merge_feature_switches(&arguments);
538 let mut arguments = if options.automation_parity
539 && !detect_automation_controlled_triggers(&arguments).is_empty()
540 {
541 apply_automation_parity_args(&arguments)
542 } else {
543 arguments
544 };
545 if arguments.iter().all(|argument| argument.starts_with('-')) {
546 arguments.push(START_URL.to_string());
547 }
548 Ok(arguments)
549}
550
551pub fn build_real_browser_args(options: &RealBrowserOptions) -> Result<Vec<String>> {
560 let user_data_dir = options.user_data_dir.as_deref().ok_or_else(|| {
561 anyhow!("build_real_browser_args needs user_data_dir; launch_real_browser creates a temporary profile when it is not set")
562 })?;
563 let port = options.remote_debugging_port.ok_or_else(|| {
564 anyhow!("build_real_browser_args needs remote_debugging_port; launch_real_browser reserves a free port when it is not set")
565 })?;
566 browser_args(options, user_data_dir, port)
567}
568
569fn validate_launch_request(options: &RealBrowserOptions) -> Result<()> {
570 assert_cdp_browser(&options.channel)?;
571 if options.engine == EngineType::Fantoccini {
572 return Err(anyhow!(FANTOCCINI_OVER_CDP));
573 }
574 if let Some(port) = options.remote_debugging_port {
575 assert_fixed_debugging_port(port)?;
576 }
577 browser_args(
578 options,
579 options
580 .user_data_dir
581 .as_deref()
582 .unwrap_or_else(|| Path::new("validation")),
583 options.remote_debugging_port.unwrap_or(1),
584 )?;
585 if let Some(user_data_dir) = &options.user_data_dir {
586 assert_dedicated_user_data_dir(user_data_dir)?;
587 }
588 Ok(())
589}
590
591fn browser_environment(options: &RealBrowserOptions) -> Result<Option<HashMap<String, String>>> {
592 let mut env = resolve_restrictions(&options.restrictions)?.env;
593 if let Some(extra) = &options.env {
594 env.extend(
595 extra
596 .iter()
597 .map(|(key, value)| (key.clone(), value.clone())),
598 );
599 }
600 Ok((!env.is_empty() || options.env.is_some()).then_some(env))
601}
602
603pub(crate) struct SpawnedBrowser {
606 pub(crate) process: BrowserProcess,
607 pub(crate) dev_tools_output: Option<DevToolsOutputWatcher>,
608}
609
610#[async_trait]
612pub(crate) trait LaunchHooks: Send + Sync {
613 fn resolve_executable(&self, options: &RealBrowserOptions) -> Result<PathBuf> {
614 resolve_system_browser_executable(options)
615 }
616
617 fn reserve_port(&self) -> Result<u16> {
618 reserve_loopback_port()
619 }
620
621 async fn spawn_browser(
622 &self,
623 executable_path: &Path,
624 args: &[String],
625 env: Option<HashMap<String, String>>,
626 verbose: bool,
627 ) -> Result<SpawnedBrowser> {
628 let file = executable_path.to_str().ok_or_else(|| {
629 anyhow!(
630 "browser executable path is not valid UTF-8: {}",
631 executable_path.display()
632 )
633 })?;
634 let watcher = DevToolsOutputWatcher::new();
635 let process = start_process(
636 file,
637 args,
638 StartProcessOptions {
639 env,
640 forward_output: verbose,
641 on_stderr: vec![watcher.listener()],
642 ..StartProcessOptions::default()
643 },
644 )
645 .await
646 .map_err(|error| anyhow!("failed to start installed browser {file}: {error}"))?;
647 Ok(SpawnedBrowser {
648 process: BrowserProcess::from_managed(process),
649 dev_tools_output: Some(watcher),
650 })
651 }
652
653 async fn wait_for_endpoint(&self, request: CdpEndpointRequest<'_>) -> Result<String> {
654 wait_for_cdp_endpoint(request).await
655 }
656
657 async fn request_close(&self, cdp_endpoint: &str, timeout: Duration) -> Result<()> {
659 let endpoint = cdp_endpoint.to_owned();
660 let close = async move {
661 let (mut browser, mut handler) = CdpBrowser::connect(endpoint).await?;
662 let handler_task = tokio::spawn(async move { while handler.next().await.is_some() {} });
663 let result = browser.close().await;
664 handler_task.abort();
665 result.map(|_| ()).map_err(anyhow::Error::from)
666 };
667 tokio::time::timeout(timeout, close)
668 .await
669 .map_err(|_| anyhow!("Browser.close did not answer within {timeout:?}"))?
670 }
671}
672
673pub(crate) struct SystemLaunchHooks;
675
676impl LaunchHooks for SystemLaunchHooks {}
677
678pub(crate) struct RealBrowserCloser {
679 hooks: Arc<dyn LaunchHooks>,
680 process: BrowserProcess,
681 cdp_endpoint: String,
682 user_data_dir: PathBuf,
683 temporary_profile: bool,
684 close_timeout: Duration,
685 closed: tokio::sync::OnceCell<()>,
686}
687
688#[async_trait]
689impl BrowserCloser for RealBrowserCloser {
690 async fn close(&self) -> Result<()> {
691 self.closed
692 .get_or_try_init(|| async {
693 if self.process.is_running() {
694 let _ = self
697 .hooks
698 .request_close(&self.cdp_endpoint, self.close_timeout)
699 .await;
700 }
701 if self
702 .process
703 .wait_timeout(self.close_timeout)
704 .await
705 .is_none()
706 {
707 self.process.kill();
708 self.process.wait_timeout(self.close_timeout).await;
709 }
710 if self.temporary_profile {
711 remove_user_data_dir(&self.user_data_dir).await?;
712 }
713 Ok::<(), anyhow::Error>(())
714 })
715 .await
716 .map(|_| ())
717 }
718}
719
720pub(crate) struct LaunchedRealBrowser {
722 pub(crate) cdp_endpoint: String,
723 pub(crate) remote_debugging_port: u16,
724 pub(crate) executable_path: PathBuf,
725 pub(crate) user_data_dir: PathBuf,
726 pub(crate) temporary_profile: bool,
727 pub(crate) args: Vec<String>,
728 pub(crate) browser_process: BrowserProcess,
729 pub(crate) migration: Option<MigrationSummary>,
730 pub(crate) closer: Arc<RealBrowserCloser>,
731}
732
733impl std::fmt::Debug for LaunchedRealBrowser {
734 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
735 formatter
736 .debug_struct("LaunchedRealBrowser")
737 .field("cdp_endpoint", &self.cdp_endpoint)
738 .field("remote_debugging_port", &self.remote_debugging_port)
739 .field("executable_path", &self.executable_path)
740 .field("user_data_dir", &self.user_data_dir)
741 .field("temporary_profile", &self.temporary_profile)
742 .field("args", &self.args)
743 .field("browser_process", &self.browser_process)
744 .field("migration", &self.migration)
745 .finish()
746 }
747}
748
749struct Spawned {
750 process: BrowserProcess,
751 cdp_endpoint: String,
752 port: u16,
753 args: Vec<String>,
754}
755
756async fn spawn_on_free_port(
757 options: &RealBrowserOptions,
758 hooks: &dyn LaunchHooks,
759 executable_path: &Path,
760 user_data_dir: &Path,
761 env: Option<HashMap<String, String>>,
762) -> Result<Spawned> {
763 let attempts = match options.remote_debugging_port {
764 Some(_) => 1,
765 None => options.port_attempts.max(1),
766 };
767 let mut attempt = 1;
768 loop {
769 let port = match options.remote_debugging_port {
770 Some(port) => port,
771 None => hooks.reserve_port()?,
772 };
773 let args = browser_args(options, user_data_dir, port)?;
774 if options.verbose {
775 tracing::info!(executable = %executable_path.display(), ?args, "starting installed browser");
776 }
777 let spawned = hooks
778 .spawn_browser(executable_path, &args, env.clone(), options.verbose)
779 .await?;
780 let waited = hooks
781 .wait_for_endpoint(CdpEndpointRequest {
782 remote_debugging_port: port,
783 user_data_dir,
784 browser_process: &spawned.process,
785 dev_tools_output: spawned.dev_tools_output.as_ref(),
786 timeout: options.startup_timeout,
787 })
788 .await;
789 match waited {
790 Ok(cdp_endpoint) => {
791 return Ok(Spawned {
792 process: spawned.process,
793 cdp_endpoint,
794 port,
795 args,
796 })
797 }
798 Err(error) => {
799 spawned.process.kill();
800 if error.downcast_ref::<PortRaceError>().is_none() || attempt >= attempts {
801 return Err(error);
802 }
803 if options.verbose {
804 tracing::info!("{error}; retrying with a new port");
805 }
806 spawned.process.wait_timeout(RACE_EXIT_WAIT).await;
807 attempt += 1;
808 }
809 }
810 }
811}
812
813pub(crate) async fn launch_real_browser_with_owned<T, C, F>(
815 options: &RealBrowserOptions,
816 hooks: Arc<dyn LaunchHooks>,
817 connect: C,
818 owned_profile: bool,
819) -> Result<(T, LaunchedRealBrowser)>
820where
821 C: FnOnce(ConnectOptions) -> F,
822 F: Future<Output = Result<T>>,
823{
824 validate_launch_request(options)?;
825 if let Some(state) = &options.storage_state {
826 state.load()?;
827 }
828 let executable_path = hooks.resolve_executable(options)?;
829 let temporary_profile = options.user_data_dir.is_none() || owned_profile;
830 let user_data_dir = match &options.user_data_dir {
831 Some(user_data_dir) => {
832 prepare_user_data_dir_with_first_run(user_data_dir, options.first_run)?
833 }
834 None => create_temporary_user_data_dir_with_first_run(None, options.first_run)?,
835 };
836 let (migration, migrated_cookies) = if let Some(source) = options.migrate_from.clone() {
837 let target = user_data_dir.join("Default");
838 let mut migrate_options = MigrateProfileOptions::new(source, target);
839 migrate_options.target_browser = Some(options.channel.clone());
840 if let Some(include) = &options.migrate_include {
841 migrate_options.include = include.clone();
842 }
843 migrate_options.domains = options.migrate_domains.clone();
844 migrate_options.password_csv = options.migrate_password_csv.clone();
845 migrate_options.include_payment_cards = options.migrate_include_payment_cards;
846 let result = tokio::task::spawn_blocking(move || migrate_profile(migrate_options)).await;
847 let report = match result {
848 Ok(Ok(report)) => report,
849 Ok(Err(error)) => {
850 if temporary_profile {
851 let _ = remove_user_data_dir(&user_data_dir).await;
852 }
853 return Err(error);
854 }
855 Err(error) => {
856 if temporary_profile {
857 let _ = remove_user_data_dir(&user_data_dir).await;
858 }
859 return Err(error.into());
860 }
861 };
862 let (cookies, summary) = report.into_parts();
863 let values = cookies
864 .into_iter()
865 .map(serde_json::to_value)
866 .collect::<std::result::Result<Vec<_>, _>>()?;
867 (Some(summary), values)
868 } else {
869 (None, Vec::new())
870 };
871 if let Err(error) = configure_user_data_dir_for_profile(
872 &user_data_dir,
873 &options.profile_directory,
874 options.default_browser_check,
875 &options.preferences,
876 &options.local_state,
877 ) {
878 if temporary_profile {
879 let _ = remove_user_data_dir(&user_data_dir).await;
880 }
881 return Err(error);
882 }
883 let env = browser_environment(options)?;
884
885 let spawned = match spawn_on_free_port(
886 options,
887 hooks.as_ref(),
888 &executable_path,
889 &user_data_dir,
890 env,
891 )
892 .await
893 {
894 Ok(spawned) => spawned,
895 Err(error) => {
896 if temporary_profile {
897 let _ = remove_user_data_dir(&user_data_dir).await;
898 }
899 return Err(error);
900 }
901 };
902 let process = spawned.process;
903 if temporary_profile {
904 let exited = process.exited();
907 let directory = user_data_dir.clone();
908 tokio::spawn(async move {
909 exited.await;
910 let _ = remove_user_data_dir(&directory).await;
911 });
912 }
913
914 let connection = match connection_options(options, &spawned.cdp_endpoint) {
915 Ok(mut connect_options) => {
916 connect_options.seed_cookies.extend(migrated_cookies);
917 connect(connect_options).await
918 }
919 Err(error) => Err(error),
920 };
921 let connection = match connection {
922 Ok(connection) => connection,
923 Err(error) => {
924 process.kill();
925 process.wait_timeout(options.close_timeout).await;
926 if temporary_profile {
927 let _ = remove_user_data_dir(&user_data_dir).await;
928 }
929 return Err(error);
930 }
931 };
932
933 let closer = Arc::new(RealBrowserCloser {
934 hooks,
935 process: process.clone(),
936 cdp_endpoint: spawned.cdp_endpoint.clone(),
937 user_data_dir: user_data_dir.clone(),
938 temporary_profile,
939 close_timeout: options.close_timeout,
940 closed: tokio::sync::OnceCell::new(),
941 });
942 Ok((
943 connection,
944 LaunchedRealBrowser {
945 cdp_endpoint: spawned.cdp_endpoint,
946 remote_debugging_port: spawned.port,
947 executable_path,
948 user_data_dir,
949 temporary_profile,
950 args: spawned.args,
951 browser_process: process,
952 migration,
953 closer,
954 },
955 ))
956}
957
958pub async fn launch_real_browser(options: RealBrowserOptions) -> Result<RealBrowserLaunchResult> {
968 launch_real_browser_owned(options, false).await
969}
970
971pub(crate) async fn launch_real_browser_owned(
972 options: RealBrowserOptions,
973 owned_profile: bool,
974) -> Result<RealBrowserLaunchResult> {
975 let (connection, launched) = launch_real_browser_with_owned(
976 &options,
977 Arc::new(SystemLaunchHooks),
978 connect_browser,
979 owned_profile,
980 )
981 .await?;
982 Ok(real_browser_result(connection, launched, options.headless))
983}
984
985pub async fn launch_and_connect_real_browser(
987 options: RealBrowserOptions,
988) -> Result<RealBrowserLaunchResult> {
989 launch_real_browser(options).await
990}
991
992#[path = "real_browser_result.rs"]
993mod result;
994use result::real_browser_result;
995pub(crate) use result::{connection_options, launch_real_browser_with};
996
997#[cfg(test)]
998#[path = "real_browser_tests.rs"]
999mod tests;