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, LaunchResult};
36use crate::browser::migration::{
37 migrate_profile, MigrateProfileOptions, MigrationSource, MigrationSummary,
38};
39use crate::browser::profile_directory::{
40 create_temporary_user_data_dir, prepare_user_data_dir, remove_user_data_dir,
41};
42use crate::browser::restrictions::{merge_feature_switches, resolve_restrictions};
43use crate::core::engine::{EngineAdapter, EngineType};
44use crate::downloads::{DownloadManager, DownloadSetting};
45use crate::fingerprint::automation_parity::{
46 apply_automation_parity_args, detect_automation_controlled_triggers,
47};
48use crate::utilities::{start_process, StartProcessOptions};
49
50use crate::browser::system_browser::resolve_browser_executable;
51pub use crate::browser::system_browser::{
52 assert_dedicated_user_data_dir, default_real_browser_user_data_dir,
53};
54
55const MANAGED_ARGUMENTS: [&str; 4] = [
56 "--remote-debugging-address",
57 "--remote-debugging-port",
58 "--remote-debugging-pipe",
59 "--user-data-dir",
60];
61
62pub const DEFAULT_PORT_ATTEMPTS: u32 = 3;
64
65pub const DEFAULT_CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
67
68const RACE_EXIT_WAIT: Duration = Duration::from_secs(5);
70
71const FANTOCCINI_OVER_CDP: &str =
72 "fantoccini does not connect over CDP; use chromiumoxide, playwright, or puppeteer";
73
74#[derive(Debug, Clone)]
76pub struct RealBrowserOptions {
77 pub engine: EngineType,
79 pub channel: String,
81 pub executable_path: Option<PathBuf>,
83 pub user_data_dir: Option<PathBuf>,
86 pub remote_debugging_port: Option<u16>,
90 pub port_attempts: u32,
92 pub headless: bool,
94 pub restrictions: Vec<String>,
98 pub args: Vec<String>,
100 pub extra_args: Vec<String>,
102 pub ignore_default_args: Vec<String>,
105 pub ignore_all_default_args: bool,
107 pub env: Option<HashMap<String, String>>,
110 pub automation_parity: bool,
115 pub startup_timeout: Duration,
117 pub close_timeout: Duration,
120 pub slow_mo: u64,
122 pub timeout: Option<Duration>,
124 pub protocol_timeout: Option<Duration>,
126 pub seed_cookies: Vec<Value>,
128 pub migrate_from: Option<MigrationSource>,
130 pub migrate_include: Option<Vec<String>>,
132 pub migrate_domains: Vec<String>,
134 pub verbose: bool,
136 pub node_executable: Option<PathBuf>,
138 pub node_working_dir: Option<PathBuf>,
140 pub downloads: DownloadSetting,
148}
149
150impl Default for RealBrowserOptions {
151 fn default() -> Self {
152 Self {
153 engine: EngineType::Chromiumoxide,
154 channel: "chrome".to_string(),
155 executable_path: None,
156 user_data_dir: None,
157 remote_debugging_port: None,
158 port_attempts: DEFAULT_PORT_ATTEMPTS,
159 headless: false,
160 restrictions: Vec::new(),
161 args: Vec::new(),
162 extra_args: Vec::new(),
163 ignore_default_args: Vec::new(),
164 ignore_all_default_args: false,
165 env: None,
166 automation_parity: true,
167 startup_timeout: Duration::from_secs(30),
168 close_timeout: DEFAULT_CLOSE_TIMEOUT,
169 slow_mo: 0,
170 timeout: None,
171 protocol_timeout: None,
172 seed_cookies: Vec::new(),
173 migrate_from: None,
174 migrate_include: None,
175 migrate_domains: Vec::new(),
176 verbose: false,
177 node_executable: None,
178 node_working_dir: None,
179 downloads: DownloadSetting::Off,
180 }
181 }
182}
183
184impl RealBrowserOptions {
185 pub fn chromiumoxide() -> Self {
187 Self::default()
188 }
189
190 pub fn playwright() -> Self {
192 Self {
193 engine: EngineType::Playwright,
194 ..Self::default()
195 }
196 }
197
198 pub fn puppeteer() -> Self {
200 Self {
201 engine: EngineType::Puppeteer,
202 ..Self::default()
203 }
204 }
205
206 pub fn channel(mut self, channel: impl Into<String>) -> Self {
208 self.channel = channel.into();
209 self
210 }
211
212 pub fn executable_path(mut self, executable_path: impl Into<PathBuf>) -> Self {
214 self.executable_path = Some(executable_path.into());
215 self
216 }
217
218 pub fn user_data_dir(mut self, user_data_dir: impl Into<PathBuf>) -> Self {
220 self.user_data_dir = Some(user_data_dir.into());
221 self
222 }
223
224 pub fn remote_debugging_port(mut self, port: u16) -> Self {
227 self.remote_debugging_port = Some(port);
228 self
229 }
230
231 pub fn port_attempts(mut self, attempts: u32) -> Self {
233 self.port_attempts = attempts;
234 self
235 }
236
237 pub fn headless(mut self, headless: bool) -> Self {
239 self.headless = headless;
240 self
241 }
242
243 pub fn restrictions<I, S>(mut self, restrictions: I) -> Self
245 where
246 I: IntoIterator<Item = S>,
247 S: Into<String>,
248 {
249 self.restrictions = restrictions.into_iter().map(Into::into).collect();
250 self
251 }
252
253 pub fn with_args(mut self, args: Vec<String>) -> Self {
255 self.args = args;
256 self
257 }
258
259 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
261 self.extra_args = args;
262 self
263 }
264
265 #[deprecated(
267 since = "0.13.0",
268 note = "Browser Commander adds no default switches any more; opt in with `restrictions` instead"
269 )]
270 pub fn ignore_default_args(mut self, args: Vec<String>) -> Self {
271 self.ignore_default_args = args;
272 self
273 }
274
275 #[deprecated(
277 since = "0.13.0",
278 note = "Browser Commander adds no default switches any more; opt in with `restrictions` instead"
279 )]
280 pub fn ignore_all_default_args(mut self) -> Self {
281 self.ignore_all_default_args = true;
282 self
283 }
284
285 pub fn env(mut self, env: HashMap<String, String>) -> Self {
287 self.env = Some(env);
288 self
289 }
290
291 pub fn automation_parity(mut self, enabled: bool) -> Self {
293 self.automation_parity = enabled;
294 self
295 }
296
297 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
299 self.startup_timeout = timeout;
300 self
301 }
302
303 pub fn close_timeout(mut self, timeout: Duration) -> Self {
305 self.close_timeout = timeout;
306 self
307 }
308
309 pub fn slow_mo(mut self, milliseconds: u64) -> Self {
311 self.slow_mo = milliseconds;
312 self
313 }
314
315 pub fn timeout(mut self, timeout: Duration) -> Self {
317 self.timeout = Some(timeout);
318 self
319 }
320
321 pub fn protocol_timeout(mut self, timeout: Duration) -> Self {
323 self.protocol_timeout = Some(timeout);
324 self
325 }
326
327 pub fn seed_cookies(mut self, cookies: Vec<Value>) -> Self {
329 self.seed_cookies = cookies;
330 self
331 }
332
333 #[must_use]
335 pub fn migrate_from(mut self, source: MigrationSource) -> Self {
336 self.migrate_from = Some(source);
337 self
338 }
339
340 #[must_use]
342 pub fn migrate_include<I, S>(mut self, include: I) -> Self
343 where
344 I: IntoIterator<Item = S>,
345 S: Into<String>,
346 {
347 self.migrate_include = Some(include.into_iter().map(Into::into).collect());
348 self
349 }
350
351 #[must_use]
353 pub fn migrate_domains<I, S>(mut self, domains: I) -> Self
354 where
355 I: IntoIterator<Item = S>,
356 S: Into<String>,
357 {
358 self.migrate_domains = domains.into_iter().map(Into::into).collect();
359 self
360 }
361
362 pub fn verbose(mut self, verbose: bool) -> Self {
364 self.verbose = verbose;
365 self
366 }
367
368 pub fn node_executable(mut self, executable: impl Into<PathBuf>) -> Self {
370 self.node_executable = Some(executable.into());
371 self
372 }
373
374 pub fn node_working_dir(mut self, directory: impl Into<PathBuf>) -> Self {
376 self.node_working_dir = Some(directory.into());
377 self
378 }
379
380 pub fn downloads(mut self, downloads: impl Into<DownloadSetting>) -> Self {
387 self.downloads = downloads.into();
388 self
389 }
390
391 #[deprecated(
394 since = "0.13.0",
395 note = "launch_real_browser uses a fresh temporary profile unless user_data_dir is set; read RealBrowserLaunchResult::user_data_dir"
396 )]
397 pub fn get_user_data_dir(&self) -> PathBuf {
398 self.user_data_dir
399 .clone()
400 .unwrap_or_else(|| default_real_browser_user_data_dir(&self.channel))
401 }
402}
403
404pub struct RealBrowserLaunchResult {
406 pub browser: Browser,
408 pub page: Arc<dyn EngineAdapter>,
410 pub cdp_endpoint: String,
412 pub remote_debugging_port: u16,
414 pub executable_path: PathBuf,
416 pub user_data_dir: PathBuf,
418 pub temporary_profile: bool,
421 pub args: Vec<String>,
423 pub browser_process: BrowserProcess,
426 pub downloads: Option<Arc<DownloadManager>>,
431 pub migration: Option<MigrationSummary>,
433 closer: Arc<RealBrowserCloser>,
434}
435
436impl RealBrowserLaunchResult {
437 pub async fn close(&self) -> Result<()> {
441 self.closer.close().await
442 }
443}
444
445impl std::fmt::Debug for RealBrowserLaunchResult {
446 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 formatter
448 .debug_struct("RealBrowserLaunchResult")
449 .field("browser", &self.browser)
450 .field("page", &"<dyn EngineAdapter>")
451 .field("cdp_endpoint", &self.cdp_endpoint)
452 .field("remote_debugging_port", &self.remote_debugging_port)
453 .field("executable_path", &self.executable_path)
454 .field("user_data_dir", &self.user_data_dir)
455 .field("temporary_profile", &self.temporary_profile)
456 .field("args", &self.args)
457 .field("browser_process", &self.browser_process)
458 .field("downloads", &self.downloads)
459 .field("migration", &self.migration)
460 .finish()
461 }
462}
463
464pub fn resolve_system_browser_executable(options: &RealBrowserOptions) -> Result<PathBuf> {
466 resolve_browser_executable(&options.channel, options.executable_path.as_deref())
467}
468
469fn assert_no_managed_arguments(options: &RealBrowserOptions) -> Result<()> {
470 for argument in options.args.iter().chain(&options.extra_args) {
471 if MANAGED_ARGUMENTS
472 .iter()
473 .any(|managed| argument == managed || argument.starts_with(&format!("{managed}=")))
474 {
475 return Err(anyhow!("{argument} is managed by launch_real_browser"));
476 }
477 }
478 Ok(())
479}
480
481pub const START_URL: &str = "about:blank";
490
491pub(crate) fn browser_args(
492 options: &RealBrowserOptions,
493 user_data_dir: &Path,
494 remote_debugging_port: u16,
495) -> Result<Vec<String>> {
496 let port = assert_fixed_debugging_port(remote_debugging_port)?;
497 assert_no_managed_arguments(options)?;
498 let mut arguments = vec![
499 format!("--user-data-dir={}", user_data_dir.display()),
500 format!("--remote-debugging-port={port}"),
501 ];
502 if options.headless {
503 arguments.push("--headless=new".to_string());
504 }
505 arguments.extend(resolve_restrictions(&options.restrictions)?.args);
506 arguments.extend(options.args.iter().cloned());
507 arguments.extend(options.extra_args.iter().cloned());
508 let arguments = merge_feature_switches(&arguments);
509 let mut arguments = if options.automation_parity
510 && !detect_automation_controlled_triggers(&arguments).is_empty()
511 {
512 apply_automation_parity_args(&arguments)
513 } else {
514 arguments
515 };
516 if arguments.iter().all(|argument| argument.starts_with('-')) {
517 arguments.push(START_URL.to_string());
518 }
519 Ok(arguments)
520}
521
522pub fn build_real_browser_args(options: &RealBrowserOptions) -> Result<Vec<String>> {
531 let user_data_dir = options.user_data_dir.as_deref().ok_or_else(|| {
532 anyhow!("build_real_browser_args needs user_data_dir; launch_real_browser creates a temporary profile when it is not set")
533 })?;
534 let port = options.remote_debugging_port.ok_or_else(|| {
535 anyhow!("build_real_browser_args needs remote_debugging_port; launch_real_browser reserves a free port when it is not set")
536 })?;
537 browser_args(options, user_data_dir, port)
538}
539
540fn validate_launch_request(options: &RealBrowserOptions) -> Result<()> {
541 if options.engine == EngineType::Fantoccini {
542 return Err(anyhow!(FANTOCCINI_OVER_CDP));
543 }
544 if let Some(port) = options.remote_debugging_port {
545 assert_fixed_debugging_port(port)?;
546 }
547 browser_args(
548 options,
549 options
550 .user_data_dir
551 .as_deref()
552 .unwrap_or_else(|| Path::new("validation")),
553 options.remote_debugging_port.unwrap_or(1),
554 )?;
555 if let Some(user_data_dir) = &options.user_data_dir {
556 assert_dedicated_user_data_dir(user_data_dir)?;
557 }
558 Ok(())
559}
560
561fn browser_environment(options: &RealBrowserOptions) -> Result<Option<HashMap<String, String>>> {
562 let mut env = resolve_restrictions(&options.restrictions)?.env;
563 if let Some(extra) = &options.env {
564 env.extend(
565 extra
566 .iter()
567 .map(|(key, value)| (key.clone(), value.clone())),
568 );
569 }
570 Ok((!env.is_empty() || options.env.is_some()).then_some(env))
571}
572
573pub(crate) fn connection_options(
574 options: &RealBrowserOptions,
575 endpoint: &str,
576) -> Result<ConnectOptions> {
577 let mut connection = match options.engine {
578 EngineType::Chromiumoxide => ConnectOptions::chromiumoxide(),
579 EngineType::Playwright => ConnectOptions::playwright(),
580 EngineType::Puppeteer => ConnectOptions::puppeteer(),
581 EngineType::Fantoccini => return Err(anyhow!(FANTOCCINI_OVER_CDP)),
582 };
583 connection.cdp_endpoint = Some(endpoint.to_string());
584 connection.slow_mo = options.slow_mo;
585 connection.timeout = options.timeout;
586 connection.protocol_timeout = options.protocol_timeout;
587 connection.seed_cookies = options.seed_cookies.clone();
588 connection.verbose = options.verbose;
589 connection.node_executable = options.node_executable.clone();
590 connection.node_working_dir = options.node_working_dir.clone();
591 connection.downloads = options.downloads.clone();
592 Ok(connection)
593}
594
595pub(crate) struct SpawnedBrowser {
598 pub(crate) process: BrowserProcess,
599 pub(crate) dev_tools_output: Option<DevToolsOutputWatcher>,
600}
601
602#[async_trait]
604pub(crate) trait LaunchHooks: Send + Sync {
605 fn resolve_executable(&self, options: &RealBrowserOptions) -> Result<PathBuf> {
606 resolve_system_browser_executable(options)
607 }
608
609 fn reserve_port(&self) -> Result<u16> {
610 reserve_loopback_port()
611 }
612
613 async fn spawn_browser(
614 &self,
615 executable_path: &Path,
616 args: &[String],
617 env: Option<HashMap<String, String>>,
618 verbose: bool,
619 ) -> Result<SpawnedBrowser> {
620 let file = executable_path.to_str().ok_or_else(|| {
621 anyhow!(
622 "browser executable path is not valid UTF-8: {}",
623 executable_path.display()
624 )
625 })?;
626 let watcher = DevToolsOutputWatcher::new();
627 let process = start_process(
628 file,
629 args,
630 StartProcessOptions {
631 env,
632 forward_output: verbose,
633 on_stderr: vec![watcher.listener()],
634 ..StartProcessOptions::default()
635 },
636 )
637 .await
638 .map_err(|error| anyhow!("failed to start installed browser {file}: {error}"))?;
639 Ok(SpawnedBrowser {
640 process: BrowserProcess::from_managed(process),
641 dev_tools_output: Some(watcher),
642 })
643 }
644
645 async fn wait_for_endpoint(&self, request: CdpEndpointRequest<'_>) -> Result<String> {
646 wait_for_cdp_endpoint(request).await
647 }
648
649 async fn request_close(&self, cdp_endpoint: &str, timeout: Duration) -> Result<()> {
651 let endpoint = cdp_endpoint.to_owned();
652 let close = async move {
653 let (mut browser, mut handler) = CdpBrowser::connect(endpoint).await?;
654 let handler_task = tokio::spawn(async move { while handler.next().await.is_some() {} });
655 let result = browser.close().await;
656 handler_task.abort();
657 result.map(|_| ()).map_err(anyhow::Error::from)
658 };
659 tokio::time::timeout(timeout, close)
660 .await
661 .map_err(|_| anyhow!("Browser.close did not answer within {timeout:?}"))?
662 }
663}
664
665pub(crate) struct SystemLaunchHooks;
667
668impl LaunchHooks for SystemLaunchHooks {}
669
670pub(crate) struct RealBrowserCloser {
671 hooks: Arc<dyn LaunchHooks>,
672 process: BrowserProcess,
673 cdp_endpoint: String,
674 user_data_dir: PathBuf,
675 temporary_profile: bool,
676 close_timeout: Duration,
677 closed: tokio::sync::OnceCell<()>,
678}
679
680#[async_trait]
681impl BrowserCloser for RealBrowserCloser {
682 async fn close(&self) -> Result<()> {
683 self.closed
684 .get_or_try_init(|| async {
685 if self.process.is_running() {
686 let _ = self
689 .hooks
690 .request_close(&self.cdp_endpoint, self.close_timeout)
691 .await;
692 }
693 if self
694 .process
695 .wait_timeout(self.close_timeout)
696 .await
697 .is_none()
698 {
699 self.process.kill();
700 self.process.wait_timeout(self.close_timeout).await;
701 }
702 if self.temporary_profile {
703 remove_user_data_dir(&self.user_data_dir).await?;
704 }
705 Ok::<(), anyhow::Error>(())
706 })
707 .await
708 .map(|_| ())
709 }
710}
711
712pub(crate) struct LaunchedRealBrowser {
714 pub(crate) cdp_endpoint: String,
715 pub(crate) remote_debugging_port: u16,
716 pub(crate) executable_path: PathBuf,
717 pub(crate) user_data_dir: PathBuf,
718 pub(crate) temporary_profile: bool,
719 pub(crate) args: Vec<String>,
720 pub(crate) browser_process: BrowserProcess,
721 pub(crate) migration: Option<MigrationSummary>,
722 pub(crate) closer: Arc<RealBrowserCloser>,
723}
724
725impl std::fmt::Debug for LaunchedRealBrowser {
726 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
727 formatter
728 .debug_struct("LaunchedRealBrowser")
729 .field("cdp_endpoint", &self.cdp_endpoint)
730 .field("remote_debugging_port", &self.remote_debugging_port)
731 .field("executable_path", &self.executable_path)
732 .field("user_data_dir", &self.user_data_dir)
733 .field("temporary_profile", &self.temporary_profile)
734 .field("args", &self.args)
735 .field("browser_process", &self.browser_process)
736 .field("migration", &self.migration)
737 .finish()
738 }
739}
740
741struct Spawned {
742 process: BrowserProcess,
743 cdp_endpoint: String,
744 port: u16,
745 args: Vec<String>,
746}
747
748async fn spawn_on_free_port(
749 options: &RealBrowserOptions,
750 hooks: &dyn LaunchHooks,
751 executable_path: &Path,
752 user_data_dir: &Path,
753 env: Option<HashMap<String, String>>,
754) -> Result<Spawned> {
755 let attempts = match options.remote_debugging_port {
756 Some(_) => 1,
757 None => options.port_attempts.max(1),
758 };
759 let mut attempt = 1;
760 loop {
761 let port = match options.remote_debugging_port {
762 Some(port) => port,
763 None => hooks.reserve_port()?,
764 };
765 let args = browser_args(options, user_data_dir, port)?;
766 if options.verbose {
767 tracing::info!(executable = %executable_path.display(), ?args, "starting installed browser");
768 }
769 let spawned = hooks
770 .spawn_browser(executable_path, &args, env.clone(), options.verbose)
771 .await?;
772 let waited = hooks
773 .wait_for_endpoint(CdpEndpointRequest {
774 remote_debugging_port: port,
775 user_data_dir,
776 browser_process: &spawned.process,
777 dev_tools_output: spawned.dev_tools_output.as_ref(),
778 timeout: options.startup_timeout,
779 })
780 .await;
781 match waited {
782 Ok(cdp_endpoint) => {
783 return Ok(Spawned {
784 process: spawned.process,
785 cdp_endpoint,
786 port,
787 args,
788 })
789 }
790 Err(error) => {
791 spawned.process.kill();
792 if error.downcast_ref::<PortRaceError>().is_none() || attempt >= attempts {
793 return Err(error);
794 }
795 if options.verbose {
796 tracing::info!("{error}; retrying with a new port");
797 }
798 spawned.process.wait_timeout(RACE_EXIT_WAIT).await;
799 attempt += 1;
800 }
801 }
802 }
803}
804
805pub(crate) async fn launch_real_browser_with<T, C, F>(
809 options: &RealBrowserOptions,
810 hooks: Arc<dyn LaunchHooks>,
811 connect: C,
812) -> Result<(T, LaunchedRealBrowser)>
813where
814 C: FnOnce(ConnectOptions) -> F,
815 F: Future<Output = Result<T>>,
816{
817 validate_launch_request(options)?;
818 let executable_path = hooks.resolve_executable(options)?;
819 let temporary_profile = options.user_data_dir.is_none();
820 let user_data_dir = match &options.user_data_dir {
821 Some(user_data_dir) => prepare_user_data_dir(user_data_dir)?,
822 None => create_temporary_user_data_dir(None)?,
823 };
824 let (migration, migrated_cookies) = if let Some(source) = options.migrate_from.clone() {
825 let target = user_data_dir.join("Default");
826 let mut migrate_options = MigrateProfileOptions::new(source, target);
827 migrate_options.target_browser = Some(options.channel.clone());
828 if let Some(include) = &options.migrate_include {
829 migrate_options.include = include.clone();
830 }
831 migrate_options.domains = options.migrate_domains.clone();
832 let result = tokio::task::spawn_blocking(move || migrate_profile(migrate_options)).await;
833 let report = match result {
834 Ok(Ok(report)) => report,
835 Ok(Err(error)) => {
836 if temporary_profile {
837 let _ = remove_user_data_dir(&user_data_dir).await;
838 }
839 return Err(error);
840 }
841 Err(error) => {
842 if temporary_profile {
843 let _ = remove_user_data_dir(&user_data_dir).await;
844 }
845 return Err(error.into());
846 }
847 };
848 let (cookies, summary) = report.into_parts();
849 let values = cookies
850 .into_iter()
851 .map(serde_json::to_value)
852 .collect::<std::result::Result<Vec<_>, _>>()?;
853 (Some(summary), values)
854 } else {
855 (None, Vec::new())
856 };
857 let env = browser_environment(options)?;
858
859 let spawned = match spawn_on_free_port(
860 options,
861 hooks.as_ref(),
862 &executable_path,
863 &user_data_dir,
864 env,
865 )
866 .await
867 {
868 Ok(spawned) => spawned,
869 Err(error) => {
870 if temporary_profile {
871 let _ = remove_user_data_dir(&user_data_dir).await;
872 }
873 return Err(error);
874 }
875 };
876 let process = spawned.process;
877 if temporary_profile {
878 let exited = process.exited();
881 let directory = user_data_dir.clone();
882 tokio::spawn(async move {
883 exited.await;
884 let _ = remove_user_data_dir(&directory).await;
885 });
886 }
887
888 let connection = match connection_options(options, &spawned.cdp_endpoint) {
889 Ok(mut connect_options) => {
890 connect_options.seed_cookies.extend(migrated_cookies);
891 connect(connect_options).await
892 }
893 Err(error) => Err(error),
894 };
895 let connection = match connection {
896 Ok(connection) => connection,
897 Err(error) => {
898 process.kill();
899 process.wait_timeout(options.close_timeout).await;
900 if temporary_profile {
901 let _ = remove_user_data_dir(&user_data_dir).await;
902 }
903 return Err(error);
904 }
905 };
906
907 let closer = Arc::new(RealBrowserCloser {
908 hooks,
909 process: process.clone(),
910 cdp_endpoint: spawned.cdp_endpoint.clone(),
911 user_data_dir: user_data_dir.clone(),
912 temporary_profile,
913 close_timeout: options.close_timeout,
914 closed: tokio::sync::OnceCell::new(),
915 });
916 Ok((
917 connection,
918 LaunchedRealBrowser {
919 cdp_endpoint: spawned.cdp_endpoint,
920 remote_debugging_port: spawned.port,
921 executable_path,
922 user_data_dir,
923 temporary_profile,
924 args: spawned.args,
925 browser_process: process,
926 migration,
927 closer,
928 },
929 ))
930}
931
932pub async fn launch_real_browser(options: RealBrowserOptions) -> Result<RealBrowserLaunchResult> {
942 let (connection, launched) =
943 launch_real_browser_with(&options, Arc::new(SystemLaunchHooks), connect_browser).await?;
944 Ok(real_browser_result(connection, launched, options.headless))
945}
946
947pub(crate) fn real_browser_result(
948 connection: LaunchResult,
949 launched: LaunchedRealBrowser,
950 headless: bool,
951) -> RealBrowserLaunchResult {
952 let LaunchResult {
953 mut browser,
954 page,
955 downloads,
956 ..
957 } = connection;
958 browser.user_data_dir = launched.user_data_dir.clone();
959 browser.headless = headless;
960 RealBrowserLaunchResult {
961 browser,
962 page,
963 cdp_endpoint: launched.cdp_endpoint,
964 remote_debugging_port: launched.remote_debugging_port,
965 executable_path: launched.executable_path,
966 user_data_dir: launched.user_data_dir,
967 temporary_profile: launched.temporary_profile,
968 args: launched.args,
969 browser_process: launched.browser_process,
970 downloads,
971 migration: launched.migration,
972 closer: launched.closer,
973 }
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#[cfg(test)]
984#[path = "real_browser_tests.rs"]
985mod tests;