chrome_remote_interface/
browser.rs

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/// Operate browser error.
19#[derive(Debug, thiserror::Error)]
20pub enum BrowserError {
21    /// IO error.
22    #[error(transparent)]
23    Io(#[from] io::Error),
24
25    /// Cannot detect url for Chrome DevTools Protocol URL.
26    #[error("cannot detect url.")]
27    CannotDetectUrl,
28
29    /// Unexpected format for DevToolsActivePort.
30    #[error("unexpected format.")]
31    UnexpectedFormat,
32
33    /// Failed to parse URL.
34    #[error(transparent)]
35    UrlParse(#[from] url::ParseError),
36
37    /// Browser not found.
38    #[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            // Newer Ubunts chromium runs in snapcraft.
55            // Snapcraft chromium can not access /tmp dir.
56            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/// Browser type.
68#[derive(Debug, Clone)]
69pub enum BrowserType {
70    /// Chromium
71    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/// Launcher (Builder) for Browser.
88#[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    /// Specify launching browser type. (Default: Chromium)
99    pub fn browser_type(&mut self, value: BrowserType) -> &mut Self {
100        self.browser_type = Some(value);
101        self
102    }
103
104    /// Specify user data dir. (If not specified: using temporary file)
105    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    /// Use default user data dir. (If not specified: using temporary file)
111    pub fn user_data_dir_default(&mut self) -> &mut Self {
112        self.user_data_dir = Some(UserDataDir::Default);
113        self
114    }
115
116    /// Specify headless mode or not. (Default: headless)
117    pub fn headless(&mut self, value: bool) -> &mut Self {
118        self.headless = Some(value);
119        self
120    }
121
122    /// Specify protocol transport using pipe or not (websocket). (Default: Windows/Mac: false,
123    /// Other: true)
124    pub fn use_pipe(&mut self, value: bool) -> &mut Self {
125        self.use_pipe = Some(value);
126        self
127    }
128
129    /// Whether or not browser process stdout / stderr. (Default: false)
130    pub fn output(&mut self, value: bool) -> &mut Self {
131        self.output = Some(value);
132        self
133    }
134
135    /// Launching browser.
136    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        // https://github.com/puppeteer/puppeteer/blob/9a8479a52a7d8b51690b0732b2a10816cd1b8aef/src/node/Launcher.ts#L159
188        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/// Represent instance.
237///
238/// Make drop on kill (TERM) and clean generated user data dir best effort.
239#[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    /// Construct [`Launcher`] instance.
250    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                // https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md
260                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    /// Connect Chrome DevTools Protocol Client.
305    ///
306    /// This instance Ownership move to Client.
307    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    /// Connect Chrome DevTools Protocol Client and run async block.
319    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    /// Close browser.
334    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}