use crate::config::ManagerConfig;
use crate::config::ARCH::{ARM64, X32};
use crate::config::OS::{LINUX, MACOS, WINDOWS};
use crate::downloads::{parse_json_from_url, read_version_from_link};
use crate::files::{compose_driver_path_in_cache, BrowserPath};
use crate::logger::Logger;
use crate::metadata::{
create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata,
};
use crate::{
create_http_client, format_three_args, SeleniumManager, BETA, DASH_DASH_VERSION, DEV, NIGHTLY,
OFFLINE_REQUEST_ERR_MSG, REG_VERSION_ARG, STABLE,
UNAVAILABLE_DOWNLOAD_WITH_MIN_VERSION_ERR_MSG,
};
use anyhow::anyhow;
use anyhow::Error;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::option::Option;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
pub const CHROME_NAME: &str = "chrome";
pub const CHROMEDRIVER_NAME: &str = "chromedriver";
const DRIVER_URL: &str = "https://chromedriver.storage.googleapis.com/";
const LATEST_RELEASE: &str = "LATEST_RELEASE";
const CFT_URL: &str = "https://googlechromelabs.github.io/chrome-for-testing/";
const GOOD_VERSIONS_ENDPOINT: &str = "known-good-versions-with-downloads.json";
const LATEST_VERSIONS_ENDPOINT: &str = "last-known-good-versions-with-downloads.json";
const CFT_MACOS_APP_NAME: &str =
"Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing";
const MIN_CHROME_VERSION_CFT: i32 = 113;
const MIN_CHROMEDRIVER_VERSION_CFT: i32 = 115;
const CHROMIUM_SNAP_LINK: &str = "/snap/bin/chromium";
const CHROMIUM_SNAP_BINARY: &str = "/snap/chromium/current/usr/lib/chromium-browser/chrome";
pub struct ChromeManager {
pub browser_name: &'static str,
pub driver_name: &'static str,
pub config: ManagerConfig,
pub http_client: Client,
pub log: Logger,
pub tx: Sender<String>,
pub rx: Receiver<String>,
pub download_browser: bool,
pub driver_url: Option<String>,
pub browser_url: Option<String>,
}
impl ChromeManager {
pub fn new() -> Result<Box<Self>, Error> {
let browser_name = CHROME_NAME;
let driver_name = CHROMEDRIVER_NAME;
let config = ManagerConfig::default(browser_name, driver_name);
let default_timeout = config.timeout.to_owned();
let default_proxy = &config.proxy;
let (tx, rx): (Sender<String>, Receiver<String>) = mpsc::channel();
Ok(Box::new(ChromeManager {
browser_name,
driver_name,
http_client: create_http_client(default_timeout, default_proxy)?,
config,
log: Logger::new(),
tx,
rx,
download_browser: false,
driver_url: None,
browser_url: None,
}))
}
fn create_latest_release_url(&self) -> String {
format!(
"{}{}",
self.get_driver_mirror_url_or_default(DRIVER_URL),
LATEST_RELEASE
)
}
fn create_latest_release_with_version_url(&self) -> String {
format!(
"{}{}_{}",
self.get_driver_mirror_url_or_default(DRIVER_URL),
LATEST_RELEASE,
self.get_major_browser_version()
)
}
fn create_cft_url(&self, base_url: &str, endpoint: &str) -> String {
format!("{}{}", base_url, endpoint)
}
fn create_cft_url_for_browsers(&self, endpoint: &str) -> String {
self.create_cft_url(&self.get_browser_mirror_url_or_default(CFT_URL), endpoint)
}
fn create_cft_url_for_drivers(&self, endpoint: &str) -> String {
self.create_cft_url(&self.get_driver_mirror_url_or_default(CFT_URL), endpoint)
}
fn request_driver_version_from_latest(&self, driver_url: &str) -> Result<String, Error> {
self.log.debug(format!(
"Reading {} version from {}",
&self.driver_name, driver_url
));
read_version_from_link(self.get_http_client(), driver_url, self.get_logger())
}
fn request_versions_from_online<T>(&self, driver_url: &str) -> Result<T, Error>
where
T: Serialize + for<'a> Deserialize<'a>,
{
self.log
.debug(format!("Discovering versions from {}", driver_url));
parse_json_from_url::<T>(self.get_http_client(), driver_url)
}
fn request_latest_driver_version_from_online(&mut self) -> Result<String, Error> {
let driver_name = self.driver_name;
self.get_logger().trace(format!(
"Using Chrome for Testing (CfT) endpoints to find out latest stable {} version",
driver_name
));
let latest_versions_url = self.create_cft_url_for_drivers(LATEST_VERSIONS_ENDPOINT);
let versions_with_downloads =
self.request_versions_from_online::<LatestVersionsWithDownloads>(&latest_versions_url)?;
let stable_channel = versions_with_downloads.channels.stable;
let chromedriver = stable_channel.downloads.chromedriver;
if chromedriver.is_none() {
self.log.warn(format!(
"Latest stable version of {} not found using CfT endpoints. Trying with {}",
&self.driver_name, LATEST_RELEASE
));
return self.request_driver_version_from_latest(&self.create_latest_release_url());
}
let platform_url: Vec<&PlatformUrl> = chromedriver
.as_ref()
.unwrap()
.iter()
.filter(|p| p.platform.eq_ignore_ascii_case(self.get_platform_label()))
.collect();
self.log.trace(format!(
"CfT URLs for downloading {}: {:?}",
self.get_driver_name(),
platform_url
));
self.driver_url = Some(platform_url.first().unwrap().url.to_string());
Ok(stable_channel.version)
}
fn request_good_driver_version_from_online(&mut self) -> Result<String, Error> {
let browser_or_driver_version = if self.get_driver_version().is_empty() {
self.get_browser_version()
} else {
self.get_driver_version()
};
let version_for_filtering = self.get_major_version(browser_or_driver_version)?;
self.log.trace(format!(
"Driver version used to request CfT: {version_for_filtering}"
));
let good_versions_url = self.create_cft_url_for_drivers(GOOD_VERSIONS_ENDPOINT);
let all_versions =
self.request_versions_from_online::<VersionsWithDownloads>(&good_versions_url)?;
let filtered_versions: Vec<Version> = all_versions
.versions
.into_iter()
.filter(|r| r.version.starts_with(version_for_filtering.as_str()))
.collect();
if filtered_versions.is_empty() {
return Err(anyhow!(format_three_args(
UNAVAILABLE_DOWNLOAD_WITH_MIN_VERSION_ERR_MSG,
self.get_driver_name(),
&version_for_filtering,
&MIN_CHROMEDRIVER_VERSION_CFT.to_string(),
)));
}
let driver_version = filtered_versions.last().unwrap();
let url: Vec<&PlatformUrl> = driver_version
.downloads
.chromedriver
.as_ref()
.unwrap()
.iter()
.filter(|p| p.platform.eq_ignore_ascii_case(self.get_platform_label()))
.collect();
self.log.trace(format!("URLs for CfT: {:?}", url));
self.driver_url = Some(url.first().unwrap().url.to_string());
Ok(driver_version.version.to_string())
}
}
impl SeleniumManager for ChromeManager {
fn get_browser_name(&self) -> &str {
self.browser_name
}
fn get_browser_names_in_path(&self) -> Vec<&str> {
vec![self.get_browser_name(), "chromium-browser", "chromium"]
}
fn get_http_client(&self) -> &Client {
&self.http_client
}
fn set_http_client(&mut self, http_client: Client) {
self.http_client = http_client;
}
fn get_browser_path_map(&self) -> HashMap<BrowserPath, &str> {
HashMap::from([
(
BrowserPath::new(WINDOWS, STABLE),
r"Google\Chrome\Application\chrome.exe",
),
(
BrowserPath::new(WINDOWS, BETA),
r"Google\Chrome Beta\Application\chrome.exe",
),
(
BrowserPath::new(WINDOWS, DEV),
r"Google\Chrome Dev\Application\chrome.exe",
),
(
BrowserPath::new(WINDOWS, NIGHTLY),
r"Google\Chrome SxS\Application\chrome.exe",
),
(
BrowserPath::new(MACOS, STABLE),
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
),
(
BrowserPath::new(MACOS, BETA),
"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
),
(
BrowserPath::new(MACOS, DEV),
"/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
),
(
BrowserPath::new(MACOS, NIGHTLY),
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
),
(BrowserPath::new(LINUX, STABLE), "/usr/bin/google-chrome"),
(BrowserPath::new(LINUX, BETA), "/usr/bin/google-chrome-beta"),
(
BrowserPath::new(LINUX, DEV),
"/usr/bin/google-chrome-unstable",
),
])
}
fn discover_browser_version(&mut self) -> Result<Option<String>, Error> {
self.general_discover_browser_version(
r"HKCU\Software\Google\Chrome\BLBeacon",
REG_VERSION_ARG,
DASH_DASH_VERSION,
)
}
fn get_driver_name(&self) -> &str {
self.driver_name
}
fn request_driver_version(&mut self) -> Result<String, Error> {
let major_browser_version_binding = self.get_major_browser_version();
let major_browser_version = major_browser_version_binding.as_str();
let cache_path = self.get_cache_path()?;
let mut metadata = get_metadata(self.get_logger(), &cache_path);
match get_driver_version_from_metadata(
&metadata.drivers,
self.driver_name,
major_browser_version,
) {
Some(driver_version) => {
self.log.trace(format!(
"Driver TTL is valid. Getting {} version from metadata",
&self.driver_name
));
Ok(driver_version)
}
_ => {
self.assert_online_or_err(OFFLINE_REQUEST_ERR_MSG)?;
let major_browser_version_int =
major_browser_version.parse::<i32>().unwrap_or_default();
let driver_version = if self.is_browser_version_stable()
|| major_browser_version.is_empty()
|| self.is_browser_version_unstable()
{
self.request_latest_driver_version_from_online()?
} else if !major_browser_version.is_empty()
&& major_browser_version_int < MIN_CHROMEDRIVER_VERSION_CFT
{
self.request_driver_version_from_latest(
&self.create_latest_release_with_version_url(),
)?
} else {
self.request_good_driver_version_from_online()?
};
let driver_ttl = self.get_ttl();
if driver_ttl > 0 && !major_browser_version.is_empty() && !driver_version.is_empty()
{
metadata.drivers.push(create_driver_metadata(
major_browser_version,
self.driver_name,
&driver_version,
driver_ttl,
));
write_metadata(&metadata, self.get_logger(), cache_path);
}
Ok(driver_version)
}
}
}
fn request_browser_version(&mut self) -> Result<Option<String>, Error> {
self.general_request_browser_version(self.browser_name)
}
fn get_driver_url(&mut self) -> Result<String, Error> {
let major_driver_version = self
.get_major_driver_version()
.parse::<i32>()
.unwrap_or_default();
if major_driver_version >= MIN_CHROMEDRIVER_VERSION_CFT && self.driver_url.is_none() {
self.request_good_driver_version_from_online()?;
}
if self.driver_url.is_some() {
return Ok(self.driver_url.as_ref().unwrap().to_string());
}
let driver_version = self.get_driver_version();
let os = self.get_os();
let arch = self.get_arch();
let driver_label = if WINDOWS.is(os) {
"win32"
} else if MACOS.is(os) {
if ARM64.is(arch) {
if major_driver_version < 106 {
"mac64_m1"
} else {
"mac_arm64"
}
} else {
"mac64"
}
} else {
"linux64"
};
Ok(format!(
"{}{}/{}_{}.zip",
self.get_driver_mirror_url_or_default(DRIVER_URL),
driver_version,
self.driver_name,
driver_label
))
}
fn get_driver_path_in_cache(&self) -> Result<PathBuf, Error> {
Ok(compose_driver_path_in_cache(
self.get_cache_path()?.unwrap_or_default(),
self.driver_name,
self.get_os(),
self.get_platform_label(),
self.get_driver_version(),
))
}
fn get_config(&self) -> &ManagerConfig {
&self.config
}
fn get_config_mut(&mut self) -> &mut ManagerConfig {
&mut self.config
}
fn set_config(&mut self, config: ManagerConfig) {
self.config = config;
}
fn get_logger(&self) -> &Logger {
&self.log
}
fn set_logger(&mut self, log: Logger) {
self.log = log;
}
fn get_sender(&self) -> &Sender<String> {
&self.tx
}
fn get_receiver(&self) -> &Receiver<String> {
&self.rx
}
fn get_platform_label(&self) -> &str {
let os = self.get_os();
let arch = self.get_arch();
if WINDOWS.is(os) {
if X32.is(arch) {
"win32"
} else {
"win64"
}
} else if MACOS.is(os) {
if ARM64.is(arch) {
"mac-arm64"
} else {
"mac-x64"
}
} else {
"linux64"
}
}
fn request_latest_browser_version_from_online(
&mut self,
_browser_version: &str,
) -> Result<String, Error> {
let browser_name = self.browser_name;
self.get_logger().trace(format!(
"Using Chrome for Testing (CfT) endpoints to find out latest stable {} version",
browser_name
));
let latest_versions_url = self.create_cft_url_for_browsers(LATEST_VERSIONS_ENDPOINT);
let versions_with_downloads =
self.request_versions_from_online::<LatestVersionsWithDownloads>(&latest_versions_url)?;
let stable_channel = versions_with_downloads.channels.stable;
let chrome = stable_channel.downloads.chrome;
let platform_url: Vec<&PlatformUrl> = chrome
.iter()
.filter(|p| p.platform.eq_ignore_ascii_case(self.get_platform_label()))
.collect();
self.log.trace(format!(
"CfT URLs for downloading {}: {:?}",
self.get_browser_name(),
platform_url
));
let browser_version = stable_channel.version;
self.browser_url = Some(platform_url.first().unwrap().url.to_string());
Ok(browser_version)
}
fn request_fixed_browser_version_from_online(
&mut self,
_browser_version: &str,
) -> Result<String, Error> {
let browser_name = self.browser_name;
let mut browser_version = self.get_browser_version().to_string();
let major_browser_version = self.get_major_browser_version();
self.get_logger().trace(format!(
"Using Chrome for Testing (CfT) endpoints to find out {} {}",
browser_name, major_browser_version
));
if self.is_browser_version_unstable() {
let latest_versions_url = self.create_cft_url_for_browsers(LATEST_VERSIONS_ENDPOINT);
let versions_with_downloads = self
.request_versions_from_online::<LatestVersionsWithDownloads>(
&latest_versions_url,
)?;
let channel = if browser_version.eq_ignore_ascii_case(BETA) {
versions_with_downloads.channels.beta
} else if browser_version.eq_ignore_ascii_case(DEV) {
versions_with_downloads.channels.dev
} else {
versions_with_downloads.channels.canary
};
browser_version = channel.version;
let platform_url: Vec<&PlatformUrl> = channel
.downloads
.chrome
.iter()
.filter(|p| p.platform.eq_ignore_ascii_case(self.get_platform_label()))
.collect();
self.browser_url = Some(platform_url.first().unwrap().url.to_string());
Ok(browser_version)
} else {
let good_versions_url = self.create_cft_url_for_browsers(GOOD_VERSIONS_ENDPOINT);
let all_versions =
self.request_versions_from_online::<VersionsWithDownloads>(&good_versions_url)?;
let iter_versions = all_versions.versions.into_iter();
let filtered_versions: Vec<Version> = if self.is_browser_version_specific() {
iter_versions
.filter(|r| r.version.eq(browser_version.as_str()))
.collect()
} else {
iter_versions
.filter(|r| r.version.starts_with(major_browser_version.as_str()))
.collect()
};
if filtered_versions.is_empty() {
return self.unavailable_download();
}
let last_browser = filtered_versions.last().unwrap();
let platform_url: Vec<&PlatformUrl> = last_browser
.downloads
.chrome
.iter()
.filter(|p| p.platform.eq_ignore_ascii_case(self.get_platform_label()))
.collect();
self.browser_url = Some(platform_url.first().unwrap().url.to_string());
Ok(last_browser.version.to_string())
}
}
fn get_min_browser_version_for_download(&self) -> Result<i32, Error> {
Ok(MIN_CHROME_VERSION_CFT)
}
fn get_browser_binary_path(&mut self, _browser_version: &str) -> Result<PathBuf, Error> {
let browser_in_cache = self.get_browser_path_in_cache()?;
if MACOS.is(self.get_os()) {
Ok(browser_in_cache.join(CFT_MACOS_APP_NAME))
} else {
Ok(browser_in_cache.join(self.get_browser_name_with_extension()))
}
}
fn get_browser_url_for_download(&mut self, browser_version: &str) -> Result<String, Error> {
if let Some(browser_url) = self.browser_url.clone() {
Ok(browser_url)
} else {
if self.is_browser_version_stable() || self.is_browser_version_empty() {
self.request_latest_browser_version_from_online(browser_version)?;
} else {
self.request_fixed_browser_version_from_online(browser_version)?;
}
Ok(self.browser_url.clone().unwrap())
}
}
fn get_browser_label_for_download(
&self,
_browser_version: &str,
) -> Result<Option<&str>, Error> {
Ok(None)
}
fn is_download_browser(&self) -> bool {
self.download_browser
}
fn set_download_browser(&mut self, download_browser: bool) {
self.download_browser = download_browser;
}
fn is_snap(&self, browser_path: &str) -> bool {
LINUX.is(self.get_os())
&& (browser_path.eq(CHROMIUM_SNAP_LINK) || browser_path.eq(CHROMIUM_SNAP_BINARY))
}
fn get_snap_path(&self) -> Option<PathBuf> {
Some(Path::new(CHROMIUM_SNAP_BINARY).to_path_buf())
}
}
#[derive(Serialize, Deserialize)]
pub struct LatestVersionsWithDownloads {
pub timestamp: String,
pub channels: Channels,
}
#[derive(Serialize, Deserialize)]
pub struct Channels {
#[serde(rename = "Stable")]
pub stable: Channel,
#[serde(rename = "Beta")]
pub beta: Channel,
#[serde(rename = "Dev")]
pub dev: Channel,
#[serde(rename = "Canary")]
pub canary: Channel,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Channel {
pub channel: String,
pub version: String,
pub revision: String,
pub downloads: Downloads,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct VersionsWithDownloads {
pub timestamp: String,
pub versions: Vec<Version>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Version {
pub version: String,
pub revision: String,
pub downloads: Downloads,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Downloads {
pub chrome: Vec<PlatformUrl>,
pub chromedriver: Option<Vec<PlatformUrl>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PlatformUrl {
pub platform: String,
pub url: String,
}