use std::env;
use std::future::Future;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use dirs::home_dir;
use futures::TryFutureExt;
use tempfile::TempDir;
use tokio::fs::{create_dir_all, metadata, File};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::time::sleep;
use url::Url;
use which::which;
use crate::process::{Process, ProcessBuilder, ProcessStdio};
#[derive(Debug, thiserror::Error)]
pub enum BrowserError {
#[error(transparent)]
Io(#[from] io::Error),
#[error("cannot detect url.")]
CannotDetectUrl,
#[error("unexpected format.")]
UnexpectedFormat,
#[error(transparent)]
UrlParse(#[from] url::ParseError),
#[error("browser not found.")]
BrowserNotFound,
}
type Result<T> = std::result::Result<T, BrowserError>;
#[derive(Debug)]
enum UserDataDir {
Generated(TempDir),
Specified(PathBuf),
Default,
}
impl UserDataDir {
async fn generated() -> Result<Self> {
if let Ok(..) = metadata("/snap").await {
let snapdir = home_dir()
.unwrap_or_else(|| "".into())
.join("snap/chromium/common");
create_dir_all(&snapdir).await?;
Ok(Self::Generated(TempDir::new_in(&snapdir)?))
} else {
Ok(Self::Generated(TempDir::new()?))
}
}
}
#[derive(Debug, Clone)]
pub enum BrowserType {
Chromium,
}
#[derive(Debug)]
enum RemoteDebugging {
Pipe(Option<crate::pipe::OsPipe>),
Ws,
}
fn which_browser(browser: &BrowserType) -> Option<PathBuf> {
if let Ok(val) = env::var(crate::BROWSER_BIN) {
return which(val).ok();
}
crate::os::find_browser(browser)
}
#[derive(Debug, Default)]
pub struct Launcher {
browser_type: Option<BrowserType>,
user_data_dir: Option<UserDataDir>,
headless: Option<bool>,
use_pipe: Option<bool>,
output: Option<bool>,
}
impl Launcher {
pub fn browser_type(&mut self, value: BrowserType) -> &mut Self {
self.browser_type = Some(value);
self
}
pub fn user_data_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
self.user_data_dir = Some(UserDataDir::Specified(path.as_ref().to_path_buf()));
self
}
pub fn user_data_dir_default(&mut self) -> &mut Self {
self.user_data_dir = Some(UserDataDir::Default);
self
}
pub fn headless(&mut self, value: bool) -> &mut Self {
self.headless = Some(value);
self
}
pub fn use_pipe(&mut self, value: bool) -> &mut Self {
self.use_pipe = Some(value);
self
}
pub fn output(&mut self, value: bool) -> &mut Self {
self.output = Some(value);
self
}
pub async fn launch(&mut self) -> Result<Browser> {
let now = SystemTime::now();
let user_data_dir = if let Some(dir) = &self.user_data_dir {
match dir {
UserDataDir::Specified(dir) => UserDataDir::Specified(dir.clone()),
UserDataDir::Default => UserDataDir::Default,
_ => unreachable!(),
}
} else {
UserDataDir::generated().await?
};
let headless = self.headless.unwrap_or(true);
let browser_type = self
.browser_type
.to_owned()
.unwrap_or(BrowserType::Chromium);
let mut command = if let Some(bin) = which_browser(&browser_type) {
ProcessBuilder::new(bin)
} else {
return Err(BrowserError::BrowserNotFound);
};
command.stdin(ProcessStdio::null());
if self.output.unwrap_or(false) {
command
.stdout(ProcessStdio::inherit())
.stderr(ProcessStdio::inherit());
} else {
command
.stdout(ProcessStdio::null())
.stderr(ProcessStdio::null());
}
if headless {
command.args(&["--headless", "--disable-gpu"]);
}
match &user_data_dir {
UserDataDir::Default => &mut command,
UserDataDir::Generated(p) => command.arg(&format!(
"--user-data-dir={}",
p.as_ref().to_string_lossy().to_string()
)),
UserDataDir::Specified(p) => command.arg(&format!(
"--user-data-dir={}",
p.to_string_lossy().to_string()
)),
};
command.args(&[
"--disable-background-networking",
"--enable-features=NetworkService,NetworkServiceInProcess",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-breakpad",
"--disable-client-side-phishing-detection",
"--disable-component-extensions-with-background-pages",
"--disable-default-apps",
"--disable-dev-shm-usage",
"--disable-extensions",
"--disable-features=Translate",
"--disable-hang-monitor",
"--disable-ipc-flooding-protection",
"--disable-popup-blocking",
"--disable-prompt-on-repost",
"--disable-renderer-backgrounding",
"--disable-sync",
"--force-color-profile=srgb",
"--metrics-recording-only",
"--no-first-run",
"--enable-automation",
"--password-store=basic",
"--use-mock-keychain",
]);
let (proc, remote_debugging) = if self.use_pipe.unwrap_or(true) {
command.arg("--remote-debugging-pipe");
log::debug!("browser spawned {:?}", command);
let (proc, ospipe) = command.spawn_with_pipe().await?;
(proc, RemoteDebugging::Pipe(Some(ospipe)))
} else {
command.arg("--remote-debugging-port=0");
log::debug!("browser spawned {:?}", command);
let proc = command.spawn()?;
(proc, RemoteDebugging::Ws)
};
Ok(Browser {
when: now,
proc: Some(proc),
browser_type,
user_data_dir: Some(user_data_dir),
remote_debugging,
})
}
}
#[derive(Debug)]
pub struct Browser {
when: SystemTime,
proc: Option<Process>,
browser_type: BrowserType,
user_data_dir: Option<UserDataDir>,
remote_debugging: RemoteDebugging,
}
impl Browser {
pub fn launcher() -> Launcher {
Default::default()
}
fn user_data_dir(&self) -> PathBuf {
match self.user_data_dir.as_ref().expect("already closed.") {
UserDataDir::Generated(path) => path.as_ref().to_path_buf(),
UserDataDir::Specified(path) => path.to_path_buf(),
UserDataDir::Default => {
todo!()
}
}
}
pub(crate) async fn cdp_url(&mut self) -> Result<Url> {
let f = self.user_data_dir().join("DevToolsActivePort");
let interval = Duration::from_millis(200);
for n in 0..50usize {
match File::open(&f).await {
Ok(f) => {
let metadata = f.metadata().await?;
if metadata.modified()? >= self.when {
let mut f = BufReader::new(f).lines();
let maybe_port = f.next_line().await?;
let maybe_path = f.next_line().await?;
let maybe_eof = f.next_line().await?;
if let (Some(port), Some(path), None) = (maybe_port, maybe_path, maybe_eof)
{
return Ok(Url::parse(&format!("ws://127.0.0.1:{}{}", port, path))?);
} else {
return Err(BrowserError::UnexpectedFormat);
}
}
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
if self.proc.as_mut().unwrap().try_wait()? {
return Err(io::Error::new(
io::ErrorKind::Other,
"process may be terminated.",
)
.into());
}
log::trace!("{}: {:?} not found. wait {}.", n, f, interval.as_millis());
}
Err(e) => return Err(e.into()),
}
sleep(interval).await;
}
Err(BrowserError::CannotDetectUrl)
}
pub async fn connect(mut self) -> super::Result<(super::CdpClient, super::Loop)> {
let maybe_channel = match &mut self.remote_debugging {
RemoteDebugging::Ws => None,
RemoteDebugging::Pipe(inner) => Some(inner.take().unwrap().into()),
};
match maybe_channel {
None => super::CdpClient::connect_ws(&self.cdp_url().await?, Some(self)).await,
Some(channel) => super::CdpClient::connect_pipe(self, channel).await,
}
}
pub async fn run_with<F, E, R, Fut>(self, fun: F) -> std::result::Result<R, E>
where
F: FnOnce(super::CdpClient) -> Fut,
E: From<super::Error>,
Fut: Future<Output = std::result::Result<R, E>>,
{
let (client, r#loop) = self.connect().await?;
let (_, result) = tokio::try_join!(r#loop.map_err(E::from), fun(client))?;
Ok(result)
}
}
impl Browser {
pub async fn close(&mut self) {
if let Some(proc) = self.proc.take() {
proc.kill().await;
}
self.user_data_dir.take();
}
pub async fn wait(&mut self) -> io::Result<()> {
if let Some(proc) = self.proc.as_mut() {
proc.wait().await?;
}
Ok(())
}
}
impl Drop for Browser {
fn drop(&mut self) {
if let Some(proc) = self.proc.take() {
proc.kill_sync();
}
self.user_data_dir.take();
}
}