millipede_browser_chromiumoxide/
provider.rs1use std::time::Duration;
4
5use async_trait::async_trait;
6use chromiumoxide::{Browser, BrowserConfig};
7use futures_util::StreamExt;
8use millipede_browser::{BrowserError, BrowserProvider, LaunchContext};
9
10use crate::{ChromiumLaunchOptions, ChromiumPage};
11
12fn page_was_already_closed(error: &chromiumoxide::error::CdpError) -> bool {
13 if matches!(
14 error,
15 chromiumoxide::error::CdpError::ChannelSendError(_)
16 | chromiumoxide::error::CdpError::NoResponse
17 | chromiumoxide::error::CdpError::NotFound
18 ) {
19 return true;
20 }
21 let message = error.to_string().to_ascii_lowercase();
22 message.contains("no target")
23 || message.contains("target closed")
24 || message.contains("target not found")
25}
26
27pub struct ChromiumBrowser {
32 browser: Browser,
33 handler_task: tokio::task::JoinHandle<()>,
34 _profile_dir: Option<tempfile::TempDir>,
35}
36
37#[derive(Debug, Default, Clone)]
39pub struct ChromiumoxideProvider;
40
41#[async_trait]
42impl BrowserProvider for ChromiumoxideProvider {
43 type Browser = ChromiumBrowser;
44 type Page = ChromiumPage;
45 type LaunchOptions = ChromiumLaunchOptions;
46
47 async fn launch(
48 &self,
49 opts: Self::LaunchOptions,
50 ctx: &LaunchContext,
51 ) -> Result<Self::Browser, BrowserError> {
52 let executable = opts
53 .executable_path()
54 .map(ToOwned::to_owned)
55 .or_else(crate::discovery::find_browser)
56 .ok_or_else(|| BrowserError::BrowserNotFound {
57 hint: "set MILLIPEDE_CHROME or install Google Chrome/Chromium".to_owned(),
58 })?;
59
60 let (profile_dir, user_data_dir) = if let Some(path) = opts.profile_path() {
61 (None, path.to_owned())
62 } else {
63 let directory = tempfile::Builder::new()
64 .prefix("millipede-cdp-profile")
65 .tempdir()
66 .map_err(|error| BrowserError::Launch(anyhow::Error::new(error)))?;
67 let path = directory.path().to_owned();
68 (Some(directory), path)
69 };
70
71 let mut builder = BrowserConfig::builder()
72 .chrome_executable(executable)
73 .user_data_dir(user_data_dir)
74 .launch_timeout(opts.browser_launch_timeout())
75 .request_timeout(opts.cdp_request_timeout());
76 if !opts.is_headless() {
77 builder = builder.with_head();
78 }
79 if let Some((width, height)) = opts.viewport() {
80 builder = builder.window_size(width, height);
81 }
82 if let Some(proxy) = &ctx.proxy {
83 builder = builder.arg(format!("--proxy-server={}", proxy.url));
84 }
85 for argument in opts.additional_args().iter().chain(&ctx.extra_args) {
86 builder = builder.arg(argument.clone());
87 }
88
89 let config = builder
90 .build()
91 .map_err(|error| BrowserError::Launch(anyhow::anyhow!(error)))?;
92 let (browser, mut handler) = Browser::launch(config)
93 .await
94 .map_err(|error| BrowserError::Launch(anyhow::Error::new(error)))?;
95 let handler_task = tokio::spawn(async move {
96 while let Some(event) = handler.next().await {
97 if event.is_err() {
98 tracing::debug!(?event, "cdp handler event error");
99 }
100 }
101 });
102
103 Ok(ChromiumBrowser {
104 browser,
105 handler_task,
106 _profile_dir: profile_dir,
107 })
108 }
109
110 async fn new_page(&self, browser: &Self::Browser) -> Result<Self::Page, BrowserError> {
111 let page = browser
112 .browser
113 .new_page("about:blank")
114 .await
115 .map_err(|error| BrowserError::PageCreate(anyhow::Error::new(error)))?;
116 Ok(ChromiumPage::new(page))
117 }
118
119 async fn close_page(&self, page: Self::Page) -> Result<(), BrowserError> {
120 match page.into_inner().close().await {
121 Ok(()) => Ok(()),
122 Err(error) if page_was_already_closed(&error) => Ok(()),
123 Err(error) => Err(BrowserError::Protocol(anyhow::Error::new(error))),
124 }
125 }
126
127 async fn close_browser(&self, mut browser: Self::Browser) -> Result<(), BrowserError> {
128 let close_error = browser
129 .browser
130 .close()
131 .await
132 .err()
133 .map(|error| BrowserError::Protocol(anyhow::Error::new(error)));
134
135 if let Err(error) = browser.browser.wait().await {
136 tracing::debug!(?error, "failed to wait for Chromium child process");
137 }
138
139 let mut handler_task = browser.handler_task;
140 if tokio::time::timeout(Duration::from_secs(5), &mut handler_task)
141 .await
142 .is_err()
143 {
144 handler_task.abort();
145 let _ = handler_task.await;
146 }
147
148 if let Some(error) = close_error {
149 Err(error)
150 } else {
151 Ok(())
152 }
153 }
154}