1use std::env;
2use std::future::Future;
3use std::io;
4use std::path::{Path, PathBuf};
5use std::time::{Duration, SystemTime};
6
7use dirs::home_dir;
8use futures::TryFutureExt;
9use tempfile::TempDir;
10use tokio::fs::{create_dir_all, metadata, File};
11use tokio::io::{AsyncBufReadExt, BufReader};
12use tokio::time::sleep;
13use url::Url;
14use which::which;
15
16use crate::process::{Process, ProcessBuilder, ProcessStdio};
17
18#[derive(Debug, thiserror::Error)]
20pub enum BrowserError {
21 #[error(transparent)]
23 Io(#[from] io::Error),
24
25 #[error("cannot detect url.")]
27 CannotDetectUrl,
28
29 #[error("unexpected format.")]
31 UnexpectedFormat,
32
33 #[error(transparent)]
35 UrlParse(#[from] url::ParseError),
36
37 #[error("browser not found.")]
39 BrowserNotFound,
40}
41
42type Result<T> = std::result::Result<T, BrowserError>;
43
44#[derive(Debug)]
45enum UserDataDir {
46 Generated(TempDir),
47 Specified(PathBuf),
48 Default,
49}
50
51impl UserDataDir {
52 async fn generated() -> Result<Self> {
53 if let Ok(..) = metadata("/snap").await {
54 let snapdir = home_dir()
57 .unwrap_or_else(|| "".into())
58 .join("snap/chromium/common");
59 create_dir_all(&snapdir).await?;
60 Ok(Self::Generated(TempDir::new_in(&snapdir)?))
61 } else {
62 Ok(Self::Generated(TempDir::new()?))
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub enum BrowserType {
70 Chromium,
72}
73
74#[derive(Debug)]
75enum RemoteDebugging {
76 Pipe(Option<crate::pipe::OsPipe>),
77 Ws,
78}
79
80fn which_browser(browser: &BrowserType) -> Option<PathBuf> {
81 if let Ok(val) = env::var(crate::BROWSER_BIN) {
82 return which(val).ok();
83 }
84 crate::os::find_browser(browser)
85}
86
87#[derive(Debug, Default)]
89pub struct Launcher {
90 browser_type: Option<BrowserType>,
91 user_data_dir: Option<UserDataDir>,
92 headless: Option<bool>,
93 use_pipe: Option<bool>,
94 output: Option<bool>,
95}
96
97impl Launcher {
98 pub fn browser_type(&mut self, value: BrowserType) -> &mut Self {
100 self.browser_type = Some(value);
101 self
102 }
103
104 pub fn user_data_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
106 self.user_data_dir = Some(UserDataDir::Specified(path.as_ref().to_path_buf()));
107 self
108 }
109
110 pub fn user_data_dir_default(&mut self) -> &mut Self {
112 self.user_data_dir = Some(UserDataDir::Default);
113 self
114 }
115
116 pub fn headless(&mut self, value: bool) -> &mut Self {
118 self.headless = Some(value);
119 self
120 }
121
122 pub fn use_pipe(&mut self, value: bool) -> &mut Self {
125 self.use_pipe = Some(value);
126 self
127 }
128
129 pub fn output(&mut self, value: bool) -> &mut Self {
131 self.output = Some(value);
132 self
133 }
134
135 pub async fn launch(&mut self) -> Result<Browser> {
137 let now = SystemTime::now();
138
139 let user_data_dir = if let Some(dir) = &self.user_data_dir {
140 match dir {
141 UserDataDir::Specified(dir) => UserDataDir::Specified(dir.clone()),
142 UserDataDir::Default => UserDataDir::Default,
143 _ => unreachable!(),
144 }
145 } else {
146 UserDataDir::generated().await?
147 };
148 let headless = self.headless.unwrap_or(true);
149
150 let browser_type = self
151 .browser_type
152 .to_owned()
153 .unwrap_or(BrowserType::Chromium);
154
155 let mut command = if let Some(bin) = which_browser(&browser_type) {
156 ProcessBuilder::new(bin)
157 } else {
158 return Err(BrowserError::BrowserNotFound);
159 };
160
161 command.stdin(ProcessStdio::null());
162 if self.output.unwrap_or(false) {
163 command
164 .stdout(ProcessStdio::inherit())
165 .stderr(ProcessStdio::inherit());
166 } else {
167 command
168 .stdout(ProcessStdio::null())
169 .stderr(ProcessStdio::null());
170 }
171
172 if headless {
173 command.args(&["--headless", "--disable-gpu"]);
174 }
175 match &user_data_dir {
176 UserDataDir::Default => &mut command,
177 UserDataDir::Generated(p) => command.arg(&format!(
178 "--user-data-dir={}",
179 p.as_ref().to_string_lossy().to_string()
180 )),
181 UserDataDir::Specified(p) => command.arg(&format!(
182 "--user-data-dir={}",
183 p.to_string_lossy().to_string()
184 )),
185 };
186
187 command.args(&[
189 "--disable-background-networking",
190 "--enable-features=NetworkService,NetworkServiceInProcess",
191 "--disable-background-timer-throttling",
192 "--disable-backgrounding-occluded-windows",
193 "--disable-breakpad",
194 "--disable-client-side-phishing-detection",
195 "--disable-component-extensions-with-background-pages",
196 "--disable-default-apps",
197 "--disable-dev-shm-usage",
198 "--disable-extensions",
199 "--disable-features=Translate",
200 "--disable-hang-monitor",
201 "--disable-ipc-flooding-protection",
202 "--disable-popup-blocking",
203 "--disable-prompt-on-repost",
204 "--disable-renderer-backgrounding",
205 "--disable-sync",
206 "--force-color-profile=srgb",
207 "--metrics-recording-only",
208 "--no-first-run",
209 "--enable-automation",
210 "--password-store=basic",
211 "--use-mock-keychain",
212 ]);
213
214 let (proc, remote_debugging) = if self.use_pipe.unwrap_or(true) {
215 command.arg("--remote-debugging-pipe");
216 log::debug!("browser spawned {:?}", command);
217 let (proc, ospipe) = command.spawn_with_pipe().await?;
218 (proc, RemoteDebugging::Pipe(Some(ospipe)))
219 } else {
220 command.arg("--remote-debugging-port=0");
221 log::debug!("browser spawned {:?}", command);
222 let proc = command.spawn()?;
223 (proc, RemoteDebugging::Ws)
224 };
225
226 Ok(Browser {
227 when: now,
228 proc: Some(proc),
229 browser_type,
230 user_data_dir: Some(user_data_dir),
231 remote_debugging,
232 })
233 }
234}
235
236#[derive(Debug)]
240pub struct Browser {
241 when: SystemTime,
242 proc: Option<Process>,
243 browser_type: BrowserType,
244 user_data_dir: Option<UserDataDir>,
245 remote_debugging: RemoteDebugging,
246}
247
248impl Browser {
249 pub fn launcher() -> Launcher {
251 Default::default()
252 }
253
254 fn user_data_dir(&self) -> PathBuf {
255 match self.user_data_dir.as_ref().expect("already closed.") {
256 UserDataDir::Generated(path) => path.as_ref().to_path_buf(),
257 UserDataDir::Specified(path) => path.to_path_buf(),
258 UserDataDir::Default => {
259 todo!()
261 }
262 }
263 }
264
265 pub(crate) async fn cdp_url(&mut self) -> Result<Url> {
266 let f = self.user_data_dir().join("DevToolsActivePort");
267
268 let interval = Duration::from_millis(200);
269 for n in 0..50usize {
270 match File::open(&f).await {
271 Ok(f) => {
272 let metadata = f.metadata().await?;
273 if metadata.modified()? >= self.when {
274 let mut f = BufReader::new(f).lines();
275 let maybe_port = f.next_line().await?;
276 let maybe_path = f.next_line().await?;
277 let maybe_eof = f.next_line().await?;
278 if let (Some(port), Some(path), None) = (maybe_port, maybe_path, maybe_eof)
279 {
280 return Ok(Url::parse(&format!("ws://127.0.0.1:{}{}", port, path))?);
281 } else {
282 return Err(BrowserError::UnexpectedFormat);
283 }
284 }
285 }
286 Err(e) if e.kind() == io::ErrorKind::NotFound => {
287 if self.proc.as_mut().unwrap().try_wait()? {
288 return Err(io::Error::new(
289 io::ErrorKind::Other,
290 "process may be terminated.",
291 )
292 .into());
293 }
294 log::trace!("{}: {:?} not found. wait {}.", n, f, interval.as_millis());
295 }
296 Err(e) => return Err(e.into()),
297 }
298 sleep(interval).await;
299 }
300
301 Err(BrowserError::CannotDetectUrl)
302 }
303
304 pub async fn connect(mut self) -> super::Result<(super::CdpClient, super::Loop)> {
308 let maybe_channel = match &mut self.remote_debugging {
309 RemoteDebugging::Ws => None,
310 RemoteDebugging::Pipe(inner) => Some(inner.take().unwrap().into()),
311 };
312 match maybe_channel {
313 None => super::CdpClient::connect_ws(&self.cdp_url().await?, Some(self)).await,
314 Some(channel) => super::CdpClient::connect_pipe(self, channel).await,
315 }
316 }
317
318 pub async fn run_with<F, E, R, Fut>(self, fun: F) -> std::result::Result<R, E>
320 where
321 F: FnOnce(super::CdpClient) -> Fut,
322 E: From<super::Error>,
323 Fut: Future<Output = std::result::Result<R, E>>,
324 {
325 let (client, r#loop) = self.connect().await?;
326 let (_, result) = tokio::try_join!(r#loop.map_err(E::from), fun(client))?;
327
328 Ok(result)
329 }
330}
331
332impl Browser {
333 pub async fn close(&mut self) {
335 if let Some(proc) = self.proc.take() {
336 proc.kill().await;
337 }
338 self.user_data_dir.take();
339 }
340
341 pub async fn wait(&mut self) -> io::Result<()> {
342 if let Some(proc) = self.proc.as_mut() {
343 proc.wait().await?;
344 }
345 Ok(())
346 }
347}
348
349impl Drop for Browser {
350 fn drop(&mut self) {
351 if let Some(proc) = self.proc.take() {
352 proc.kill_sync();
353 }
354 self.user_data_dir.take();
355 }
356}