use eyre::{eyre, Result};
use regex::Regex;
use tracing::debug;
use url::Url;
use std::process::{Command, Stdio};
use crate::DriverFetcher;
#[cfg(target_os = "windows")]
use crate::run_powershell_cmd;
use std::path::PathBuf;
pub struct Chromedriver;
impl DriverFetcher for Chromedriver {
const BASE_URL: &'static str = "https://chromedriver.storage.googleapis.com";
fn latest_version(&self) -> Result<String> {
let latest_release_url = format!(
"{}/LATEST_RELEASE_{}",
Self::BASE_URL,
Version::find()?.build_version()
);
debug!("latest_release_url: {}", latest_release_url);
let resp = reqwest::blocking::get(&latest_release_url)?;
Ok(resp.text()?)
}
fn direct_download_url(&self, version: &str) -> Result<Url> {
Ok(Url::parse(&format!(
"{}/{version}/chromedriver_{platform}",
Self::BASE_URL,
version = version,
platform = Self::platform()?
))?)
}
}
impl Chromedriver {
pub fn new() -> Self {
Self {}
}
fn platform() -> Result<String> {
match sys_info::os_type()?.as_str() {
"Linux" => Ok(String::from("linux64.zip")),
"Darwin" => Ok(String::from("mac64.zip")),
"Windows" => Ok(String::from("win32.zip")),
other => Err(eyre!(
"webdriver-install doesn't support '{}' currently",
other
)),
}
}
}
#[derive(Debug, PartialEq)]
pub struct Version {
major: i16,
minor: i16,
build: i16,
patch: i16,
}
struct Location {}
#[cfg(target_os = "linux")]
static LINUX_CHROME_DIRS: &[&'static str] = &[
"/usr/local/sbin",
"/usr/local/bin",
"/usr/sbin",
"/usr/bin",
"/sbin",
"/bin",
"/opt/google/chrome",
];
#[cfg(target_os = "linux")]
static LINUX_CHROME_FILES: &[&'static str] =
&["google-chrome", "chrome", "chromium", "chromium-browser"];
#[cfg(target_os = "windows")]
static WIN_CHROME_DIRS: &[&'static str] = &["Google\\Chrome\\Application", "Chromium\\Application"];
#[cfg(target_os = "macos")]
static MAC_CHROME_DIRS: &[&'static str] = &[
"/Applications/Chromium.app",
"/Applications/Google Chrome.app",
];
#[cfg(target_os = "macos")]
static MAC_CHROME_FILES: &[&'static str] =
&["Contents/MacOS/Chromium", "Contents/MacOS/Google Chrome"];
impl Version {
pub fn find() -> Result<Self> {
#[cfg(target_os = "linux")]
return Self::linux_version();
#[cfg(target_os = "windows")]
return Self::windows_version();
#[cfg(target_os = "macos")]
return Self::mac_version();
}
#[allow(dead_code)]
pub fn full_version(&self) -> String {
format!(
"{}.{}.{}.{}",
self.major, self.minor, self.build, self.patch
)
}
pub fn build_version(&self) -> String {
format!("{}.{}.{}", self.major, self.minor, self.build)
}
#[cfg(target_os = "linux")]
fn linux_version() -> Result<Self> {
let output = Command::new(Location::location()?)
.arg("--version")
.stdout(Stdio::piped())
.output()?
.stdout;
let output = String::from_utf8(output)?;
debug!("Chrome --version output: {}", output);
Ok(Self::version_from_output(&output)?)
}
#[cfg(target_os = "windows")]
fn windows_version() -> Result<Self> {
let output = run_powershell_cmd(&format!(
"(Get-ItemProperty '{}').VersionInfo.ProductVersion",
Location::location()?.display()
));
let stdout = String::from_utf8(output.stdout)?;
debug!("chrome version: {}", stdout);
Ok(Self::version_from_output(&stdout)?)
}
#[cfg(target_os = "macos")]
fn mac_version() -> Result<Self> {
let output = Command::new(Location::location()?)
.arg("--version")
.stdout(Stdio::piped())
.output()?
.stdout;
let output = String::from_utf8(output)?;
debug!("Chrome --version output: {}", output);
Ok(Self::version_from_output(&output)?)
}
fn version_from_output(output: &str) -> Result<Self> {
let version_pattern = Regex::new(r"\d+\.\d+\.\d+\.\d+")?;
let version = version_pattern
.captures(&output)
.ok_or(eyre!(
"regex: Could not find 4-part Chrome version string in '{}'",
output
))?
.get(0)
.map_or("", |m| m.as_str());
let parts: Vec<i16> = version
.split(".")
.map(|i| i.parse::<i16>().unwrap())
.collect();
Ok(Self {
major: parts[0],
minor: parts[1],
build: parts[2],
patch: parts[3],
})
}
}
impl Location {
pub fn location() -> Result<PathBuf> {
#[cfg(target_os = "linux")]
return Self::linux_location();
#[cfg(target_os = "windows")]
return Self::windows_location();
#[cfg(target_os = "macos")]
return Self::mac_location();
}
#[cfg(target_os = "linux")]
fn linux_location() -> Result<PathBuf> {
for dir in LINUX_CHROME_DIRS.into_iter().map(PathBuf::from) {
for file in LINUX_CHROME_FILES {
let path = dir.join(file);
if path.exists() {
return Ok(path);
}
}
}
Err(eyre!("Unable to find chrome executable"))
}
#[cfg(target_os = "windows")]
fn windows_location() -> Result<PathBuf> {
use dirs_sys::known_folder;
let roots = vec![
known_folder(&winapi::um::knownfolders::FOLDERID_ProgramFiles),
known_folder(&winapi::um::knownfolders::FOLDERID_ProgramFilesX86),
known_folder(&winapi::um::knownfolders::FOLDERID_ProgramFilesX64),
]
.into_iter()
.flatten()
.collect::<Vec<PathBuf>>();
for dir in WIN_CHROME_DIRS.into_iter().map(PathBuf::from) {
for root in &roots {
let path = root.join(&dir).join("chrome.exe");
debug!("root: {}", root.display());
debug!("checking path {}", &path.display());
if path.exists() {
return Ok(path);
}
}
}
Err(eyre!("Unable to find chrome executable"))
}
#[cfg(target_os = "macos")]
fn mac_location() -> Result<PathBuf> {
for dir in MAC_CHROME_DIRS.into_iter().map(PathBuf::from) {
for file in MAC_CHROME_FILES {
let path = dir.join(file);
if path.exists() {
return Ok(path);
}
}
}
Err(eyre!("Unable to find chrome executable"))
}
}
#[test]
fn version_from_output_test() {
assert_eq!(
Version::version_from_output("Chromium 87.0.4280.141 snap").unwrap(),
Version {
major: 87,
minor: 0,
build: 4280,
patch: 141
}
);
assert_eq!(
Version::version_from_output("127.0.0.1").unwrap(),
Version {
major: 127,
minor: 0,
build: 0,
patch: 1
}
);
}
#[test]
#[should_panic(expected = "Could not find 4-part Chrome version string in 'a.0.0.1'")]
fn version_from_output_panic_test() {
Version::version_from_output("a.0.0.1").unwrap();
}
#[test]
#[should_panic(expected = "Could not find 4-part Chrome version string in 'abc 1.0.1 def'")]
fn version_from_output_panic_not_4_parts_test() {
Version::version_from_output("abc 1.0.1 def").unwrap();
}
#[test]
fn direct_download_url_test() {
#[cfg(target_os = "linux")]
assert_eq!(
"https://chromedriver.storage.googleapis.com/v1/chromedriver_linux64.zip",
Chromedriver::new()
.direct_download_url("v1")
.unwrap()
.to_string()
);
#[cfg(target_os = "macos")]
assert_eq!(
"https://chromedriver.storage.googleapis.com/v1/chromedriver_mac64.zip",
Chromedriver::new()
.direct_download_url("v1")
.unwrap()
.to_string()
);
#[cfg(target_os = "windows")]
assert_eq!(
"https://chromedriver.storage.googleapis.com/v1/chromedriver_win32.zip",
Chromedriver::new()
.direct_download_url("v1")
.unwrap()
.to_string()
);
}