diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 36e9d7e10b588..d4e3e5f0aa7e0 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,6 +2,14 @@ name = "selenium-manager" version = "1.0.0-M2" edition = "2021" +authors = ["Selenium &str { self.browser_name } @@ -97,30 +98,31 @@ impl BrowserManager for ChromeManager { ]) } - fn get_browser_version(&self, os: &str, browser_version: &str) -> Option { - match self.get_browser_path(os, browser_version) { + fn discover_browser_version(&self) -> Option { + match self.get_browser_path() { Some(browser_path) => { - let (shell, flag, args) = if WINDOWS.is(os) { - let mut commands = vec![ - format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES, browser_path), - format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES_X86, browser_path), - format_two_args(WMIC_COMMAND, ENV_LOCALAPPDATA, browser_path), - ]; - if !is_unstable(browser_version) { - commands.push(format_one_arg( - REG_QUERY, - r#"HKCU\Software\Google\Chrome\BLBeacon"#, - )); - } - ("cmd", "/C", commands) - } else { - ( - "sh", - "-c", - vec![format_one_arg(DASH_DASH_VERSION, browser_path)], - ) - }; - detect_browser_version(self.browser_name, shell, flag, args) + let (shell, flag, args) = + if WINDOWS.is(self.get_os()) { + let mut commands = vec![ + self.format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES, browser_path), + self.format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES_X86, browser_path), + self.format_two_args(WMIC_COMMAND, ENV_LOCALAPPDATA, browser_path), + ]; + if !self.is_browser_version_unstable() { + commands.push(self.format_one_arg( + REG_QUERY, + r#"HKCU\Software\Google\Chrome\BLBeacon"#, + )); + } + ("cmd", "/C", commands) + } else { + ( + "sh", + "-c", + vec![self.format_one_arg(DASH_DASH_VERSION, browser_path)], + ) + }; + self.detect_browser_version(shell, flag, args) } _ => None, } @@ -130,11 +132,8 @@ impl BrowserManager for ChromeManager { self.driver_name } - fn get_driver_version( - &self, - browser_version: &str, - _os: &str, - ) -> Result> { + fn request_driver_version(&self) -> Result> { + let browser_version = self.get_browser_version(); let mut metadata = get_metadata(); match get_driver_version_from_metadata(&metadata.drivers, self.driver_name, browser_version) @@ -168,19 +167,18 @@ impl BrowserManager for ChromeManager { } } - fn get_driver_url( - &self, - driver_version: &str, - os: &str, - arch: &str, - ) -> Result> { + fn get_driver_url(&self) -> Result> { + 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) { // As of chromedriver 106, the naming convention for macOS ARM64 releases changed. See: // https://groups.google.com/g/chromedriver-users/c/JRuQzH3qr2c - let major_driver_version = get_major_version(driver_version)? + let major_driver_version = self + .get_major_version(driver_version)? .parse::() .unwrap_or_default(); if major_driver_version < 106 { @@ -200,7 +198,10 @@ impl BrowserManager for ChromeManager { )) } - fn get_driver_path_in_cache(&self, driver_version: &str, os: &str, arch: &str) -> PathBuf { + fn get_driver_path_in_cache(&self) -> PathBuf { + let driver_version = self.get_driver_version(); + let os = self.get_os(); + let arch = self.get_arch(); let arch_folder = if WINDOWS.is(os) { "win32" } else if MACOS.is(os) { @@ -214,4 +215,12 @@ impl BrowserManager for ChromeManager { }; compose_driver_path_in_cache(self.driver_name, os, arch_folder, driver_version) } + + fn get_config(&self) -> &ManagerConfig { + &self.config + } + + fn set_config(&mut self, config: ManagerConfig) { + self.config = config; + } } diff --git a/rust/src/config.rs b/rust/src/config.rs new file mode 100644 index 0000000000000..04b5e08b33db5 --- /dev/null +++ b/rust/src/config.rs @@ -0,0 +1,102 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::config::OS::{LINUX, MACOS, WINDOWS}; +use std::env::consts::{ARCH, OS}; + +pub struct ManagerConfig { + pub browser_version: String, + pub driver_version: String, + pub os: String, + pub arch: String, +} + +impl ManagerConfig { + pub fn default() -> ManagerConfig { + ManagerConfig { + browser_version: "".to_string(), + driver_version: "".to_string(), + os: OS.to_string(), + arch: ARCH.to_string(), + } + } + + #[allow(clippy::should_implement_trait)] + pub fn clone(config: &ManagerConfig) -> ManagerConfig { + ManagerConfig { + browser_version: config.browser_version.as_str().to_string(), + driver_version: config.driver_version.as_str().to_string(), + os: config.os.as_str().to_string(), + arch: config.arch.as_str().to_string(), + } + } +} + +#[allow(dead_code)] +#[allow(clippy::upper_case_acronyms)] +#[derive(Hash, Eq, PartialEq, Debug)] +pub enum OS { + WINDOWS, + MACOS, + LINUX, +} + +impl OS { + pub fn to_str(&self) -> &str { + match self { + WINDOWS => "windows", + MACOS => "macos", + LINUX => "linux", + } + } + + pub fn is(&self, os: &str) -> bool { + self.to_str().eq_ignore_ascii_case(os) + } +} + +pub fn str_to_os(os: &str) -> OS { + if WINDOWS.is(os) { + WINDOWS + } else if MACOS.is(os) { + MACOS + } else { + LINUX + } +} + +#[allow(dead_code)] +#[allow(clippy::upper_case_acronyms)] +pub enum ARCH { + X32, + X64, + ARM64, +} + +impl ARCH { + pub fn to_str(&self) -> &str { + match self { + ARCH::X32 => "x86", + ARCH::X64 => "x86_64", + ARCH::ARM64 => "aarch64", + } + } + + pub fn is(&self, arch: &str) -> bool { + self.to_str().eq_ignore_ascii_case(arch) + } +} diff --git a/rust/src/edge.rs b/rust/src/edge.rs index 635db162431f6..919c9a1fed288 100644 --- a/rust/src/edge.rs +++ b/rust/src/edge.rs @@ -15,23 +15,22 @@ // specific language governing permissions and limitations // under the License. +use crate::config::ManagerConfig; use std::collections::HashMap; use std::error::Error; use std::path::PathBuf; +use crate::config::ARCH::{ARM64, X32}; +use crate::config::OS::{LINUX, MACOS, WINDOWS}; use crate::downloads::read_content_from_link; -use crate::files::compose_driver_path_in_cache; -use crate::is_unstable; -use crate::manager::ARCH::{ARM64, X32}; -use crate::manager::OS::{LINUX, MACOS, WINDOWS}; -use crate::manager::{ - detect_browser_version, format_one_arg, format_two_args, BrowserManager, BrowserPath, BETA, - DASH_DASH_VERSION, DEV, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, NIGHTLY, REG_QUERY, STABLE, - WMIC_COMMAND, -}; +use crate::files::{compose_driver_path_in_cache, BrowserPath}; use crate::metadata::{ create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata, }; +use crate::{ + SeleniumManager, BETA, DASH_DASH_VERSION, DEV, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, + NIGHTLY, REG_QUERY, STABLE, WMIC_COMMAND, +}; const BROWSER_NAME: &str = "edge"; const DRIVER_NAME: &str = "msedgedriver"; @@ -42,6 +41,7 @@ const LATEST_RELEASE: &str = "LATEST_RELEASE"; pub struct EdgeManager { pub browser_name: &'static str, pub driver_name: &'static str, + pub config: ManagerConfig, } impl EdgeManager { @@ -49,11 +49,12 @@ impl EdgeManager { Box::new(EdgeManager { browser_name: BROWSER_NAME, driver_name: DRIVER_NAME, + config: ManagerConfig::default(), }) } } -impl BrowserManager for EdgeManager { +impl SeleniumManager for EdgeManager { fn get_browser_name(&self) -> &str { self.browser_name } @@ -98,16 +99,16 @@ impl BrowserManager for EdgeManager { ]) } - fn get_browser_version(&self, os: &str, browser_version: &str) -> Option { - match self.get_browser_path(os, browser_version) { + fn discover_browser_version(&self) -> Option { + match self.get_browser_path() { Some(browser_path) => { - let (shell, flag, args) = if WINDOWS.is(os) { + let (shell, flag, args) = if WINDOWS.is(self.get_os()) { let mut commands = vec![ - format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES_X86, browser_path), - format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES, browser_path), + self.format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES_X86, browser_path), + self.format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES, browser_path), ]; - if !is_unstable(browser_version) { - commands.push(format_one_arg( + if !self.is_browser_version_unstable() { + commands.push(self.format_one_arg( REG_QUERY, r#"REG QUERY HKCU\Software\Microsoft\Edge\BLBeacon"#, )); @@ -117,10 +118,10 @@ impl BrowserManager for EdgeManager { ( "sh", "-c", - vec![format_one_arg(DASH_DASH_VERSION, browser_path)], + vec![self.format_one_arg(DASH_DASH_VERSION, browser_path)], ) }; - detect_browser_version(self.browser_name, shell, flag, args) + self.detect_browser_version(shell, flag, args) } _ => None, } @@ -130,11 +131,8 @@ impl BrowserManager for EdgeManager { self.driver_name } - fn get_driver_version( - &self, - browser_version: &str, - os: &str, - ) -> Result> { + fn request_driver_version(&self) -> Result> { + let browser_version = self.get_browser_version(); let mut metadata = get_metadata(); match get_driver_version_from_metadata(&metadata.drivers, self.driver_name, browser_version) @@ -155,7 +153,7 @@ impl BrowserManager for EdgeManager { DRIVER_URL, LATEST_RELEASE, browser_version, - os.to_uppercase() + self.get_os().to_uppercase() ) }; log::debug!("Reading {} version from {}", &self.driver_name, driver_url); @@ -175,12 +173,10 @@ impl BrowserManager for EdgeManager { } } - fn get_driver_url( - &self, - driver_version: &str, - os: &str, - arch: &str, - ) -> Result> { + fn get_driver_url(&self) -> Result> { + let driver_version = self.get_driver_version(); + let os = self.get_os(); + let arch = self.get_arch(); let driver_label = if WINDOWS.is(os) { if ARM64.is(arch) { "arm64" @@ -204,7 +200,10 @@ impl BrowserManager for EdgeManager { )) } - fn get_driver_path_in_cache(&self, driver_version: &str, os: &str, arch: &str) -> PathBuf { + fn get_driver_path_in_cache(&self) -> PathBuf { + let driver_version = self.get_driver_version(); + let os = self.get_os(); + let arch = self.get_arch(); let arch_folder = if WINDOWS.is(os) { if ARM64.is(arch) { "win-arm64" @@ -224,4 +223,12 @@ impl BrowserManager for EdgeManager { }; compose_driver_path_in_cache(self.driver_name, os, arch_folder, driver_version) } + + fn get_config(&self) -> &ManagerConfig { + &self.config + } + + fn set_config(&mut self, config: ManagerConfig) { + self.config = config; + } } diff --git a/rust/src/files.rs b/rust/src/files.rs index 84a3837253871..f30243e186426 100644 --- a/rust/src/files.rs +++ b/rust/src/files.rs @@ -22,30 +22,32 @@ use std::io; use std::path::MAIN_SEPARATOR; use std::path::{Path, PathBuf}; +use crate::config::OS; use directories::BaseDirs; use flate2::read::GzDecoder; use regex::Regex; use tar::Archive; use zip::ZipArchive; -use crate::manager::OS::WINDOWS; +use crate::config::OS::WINDOWS; const CACHE_FOLDER: &str = ".cache/selenium"; const ZIP: &str = "zip"; const GZ: &str = "gz"; const XML: &str = "xml"; -pub fn clear_cache() { - let cache_path = compose_cache_folder(); - if cache_path.exists() { - log::debug!("Clearing cache at: {}", cache_path.display()); - fs::remove_dir_all(&cache_path).unwrap_or_else(|err| { - log::warn!( - "The cache {} cannot be cleared: {}", - cache_path.display(), - err - ) - }); +#[derive(Hash, Eq, PartialEq, Debug)] +pub struct BrowserPath { + os: OS, + channel: String, +} + +impl BrowserPath { + pub fn new(os: OS, channel: &str) -> BrowserPath { + BrowserPath { + os, + channel: channel.to_string(), + } } } diff --git a/rust/src/firefox.rs b/rust/src/firefox.rs index 0293cc1e654d5..b2d0f611ad3f4 100644 --- a/rust/src/firefox.rs +++ b/rust/src/firefox.rs @@ -15,22 +15,22 @@ // specific language governing permissions and limitations // under the License. +use crate::config::ManagerConfig; use std::collections::HashMap; use std::error::Error; use std::path::PathBuf; +use crate::config::ARCH::{ARM64, X32}; +use crate::config::OS::{LINUX, MACOS, WINDOWS}; use crate::downloads::read_redirect_from_link; -use crate::files::compose_driver_path_in_cache; -use crate::manager::ARCH::{ARM64, X32}; -use crate::manager::OS::{LINUX, MACOS, WINDOWS}; -use crate::manager::{ - detect_browser_version, format_one_arg, format_two_args, get_minor_version, BrowserManager, - BrowserPath, BETA, DASH_VERSION, DEV, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, NIGHTLY, - STABLE, WMIC_COMMAND, -}; +use crate::files::{compose_driver_path_in_cache, BrowserPath}; use crate::metadata::{ create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata, }; +use crate::{ + SeleniumManager, BETA, DASH_VERSION, DEV, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, NIGHTLY, + STABLE, WMIC_COMMAND, +}; const BROWSER_NAME: &str = "firefox"; const DRIVER_NAME: &str = "geckodriver"; @@ -40,6 +40,7 @@ const LATEST_RELEASE: &str = "latest"; pub struct FirefoxManager { pub browser_name: &'static str, pub driver_name: &'static str, + pub config: ManagerConfig, } impl FirefoxManager { @@ -47,11 +48,12 @@ impl FirefoxManager { Box::new(FirefoxManager { browser_name: BROWSER_NAME, driver_name: DRIVER_NAME, + config: ManagerConfig::default(), }) } } -impl BrowserManager for FirefoxManager { +impl SeleniumManager for FirefoxManager { fn get_browser_name(&self) -> &str { self.browser_name } @@ -97,22 +99,26 @@ impl BrowserManager for FirefoxManager { ]) } - fn get_browser_version(&self, os: &str, browser_version: &str) -> Option { - match self.get_browser_path(os, browser_version) { + fn discover_browser_version(&self) -> Option { + match self.get_browser_path() { Some(browser_path) => { - let (shell, flag, args) = if WINDOWS.is(os) { + let (shell, flag, args) = if WINDOWS.is(self.get_os()) { ( "cmd", "/C", vec![ - format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES, browser_path), - format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES_X86, browser_path), + self.format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES, browser_path), + self.format_two_args(WMIC_COMMAND, ENV_PROGRAM_FILES_X86, browser_path), ], ) } else { - ("sh", "-c", vec![format_one_arg(DASH_VERSION, browser_path)]) + ( + "sh", + "-c", + vec![self.format_one_arg(DASH_VERSION, browser_path)], + ) }; - detect_browser_version(self.browser_name, shell, flag, args) + self.detect_browser_version(shell, flag, args) } _ => None, } @@ -122,11 +128,8 @@ impl BrowserManager for FirefoxManager { self.driver_name } - fn get_driver_version( - &self, - browser_version: &str, - _os: &str, - ) -> Result> { + fn request_driver_version(&self) -> Result> { + let browser_version = self.get_browser_version(); let mut metadata = get_metadata(); match get_driver_version_from_metadata(&metadata.drivers, self.driver_name, browser_version) @@ -156,15 +159,15 @@ impl BrowserManager for FirefoxManager { } } - fn get_driver_url( - &self, - driver_version: &str, - os: &str, - arch: &str, - ) -> Result> { + fn get_driver_url(&self) -> Result> { + let driver_version = self.get_driver_version(); + let os = self.get_os(); + let arch = self.get_arch(); + // As of 0.32.0, geckodriver ships aarch64 binaries for Linux and Windows // https://github.com/mozilla/geckodriver/releases/tag/v0.32.0 - let minor_driver_version = get_minor_version(driver_version)? + let minor_driver_version = self + .get_minor_version(driver_version)? .parse::() .unwrap_or_default(); let driver_label = if WINDOWS.is(os) { @@ -194,8 +197,12 @@ impl BrowserManager for FirefoxManager { )) } - fn get_driver_path_in_cache(&self, driver_version: &str, os: &str, arch: &str) -> PathBuf { - let minor_driver_version = get_minor_version(driver_version) + fn get_driver_path_in_cache(&self) -> PathBuf { + let driver_version = self.get_driver_version(); + let os = self.get_os(); + let arch = self.get_arch(); + let minor_driver_version = self + .get_minor_version(driver_version) .unwrap_or_default() .parse::() .unwrap_or_default(); @@ -222,6 +229,14 @@ impl BrowserManager for FirefoxManager { }; compose_driver_path_in_cache(self.driver_name, os, arch_folder, driver_version) } + + fn get_config(&self) -> &ManagerConfig { + &self.config + } + + fn set_config(&mut self, config: ManagerConfig) { + self.config = config; + } } #[cfg(test)] @@ -230,7 +245,7 @@ mod unit_tests { #[test] fn test_driver_url() { - let firefox_manager = FirefoxManager::new(); + let mut firefox_manager = FirefoxManager::new(); let data = vec!( vec!("0.32.0", "linux", "x86", "https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.32.0-linux32.tar.gz"), @@ -254,9 +269,10 @@ mod unit_tests { ); data.iter().for_each(|d| { - let driver_url = firefox_manager - .get_driver_url(d.first().unwrap(), d.get(1).unwrap(), d.get(2).unwrap()) - .unwrap(); + firefox_manager.set_driver_version(d.first().unwrap().to_string()); + firefox_manager.set_os(d.get(1).unwrap().to_string()); + firefox_manager.set_arch(d.get(2).unwrap().to_string()); + let driver_url = firefox_manager.get_driver_url().unwrap(); assert_eq!(d.get(3).unwrap().to_string(), driver_url); }); } diff --git a/rust/src/iexplorer.rs b/rust/src/iexplorer.rs index c1952e8ec5b0e..5f078352482fb 100644 --- a/rust/src/iexplorer.rs +++ b/rust/src/iexplorer.rs @@ -15,14 +15,15 @@ // specific language governing permissions and limitations // under the License. +use crate::config::ManagerConfig; use std::collections::HashMap; use std::error::Error; use std::path::PathBuf; use crate::downloads::read_redirect_from_link; -use crate::files::compose_driver_path_in_cache; +use crate::files::{compose_driver_path_in_cache, BrowserPath}; -use crate::manager::{get_minor_version, BrowserManager, BrowserPath}; +use crate::SeleniumManager; use crate::metadata::{ create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata, @@ -36,6 +37,7 @@ const LATEST_RELEASE: &str = "latest"; pub struct IExplorerManager { pub browser_name: &'static str, pub driver_name: &'static str, + pub config: ManagerConfig, } impl IExplorerManager { @@ -43,11 +45,12 @@ impl IExplorerManager { Box::new(IExplorerManager { browser_name: BROWSER_NAME, driver_name: DRIVER_NAME, + config: ManagerConfig::default(), }) } } -impl BrowserManager for IExplorerManager { +impl SeleniumManager for IExplorerManager { fn get_browser_name(&self) -> &str { self.browser_name } @@ -56,7 +59,7 @@ impl BrowserManager for IExplorerManager { HashMap::new() } - fn get_browser_version(&self, _os: &str, _browser_version: &str) -> Option { + fn discover_browser_version(&self) -> Option { None } @@ -64,11 +67,8 @@ impl BrowserManager for IExplorerManager { self.driver_name } - fn get_driver_version( - &self, - browser_version: &str, - _os: &str, - ) -> Result> { + fn request_driver_version(&self) -> Result> { + let browser_version = self.get_browser_version(); let mut metadata = get_metadata(); match get_driver_version_from_metadata(&metadata.drivers, self.driver_name, browser_version) @@ -98,23 +98,29 @@ impl BrowserManager for IExplorerManager { } } - fn get_driver_url( - &self, - driver_version: &str, - _os: &str, - _arch: &str, - ) -> Result> { + fn get_driver_url(&self) -> Result> { + let driver_version = self.get_driver_version(); Ok(format!( "{}download/selenium-{}/IEDriverServer_Win32_{}.zip", DRIVER_URL, driver_version, driver_version )) } - fn get_driver_path_in_cache(&self, driver_version: &str, os: &str, _arch: &str) -> PathBuf { - let _minor_driver_version = get_minor_version(driver_version) + fn get_driver_path_in_cache(&self) -> PathBuf { + let driver_version = self.get_driver_version(); + let _minor_driver_version = self + .get_minor_version(driver_version) .unwrap_or_default() .parse::() .unwrap_or_default(); - compose_driver_path_in_cache(self.driver_name, os, "win32", driver_version) + compose_driver_path_in_cache(self.driver_name, "Windows", "win32", driver_version) + } + + fn get_config(&self) -> &ManagerConfig { + &self.config + } + + fn set_config(&mut self, config: ManagerConfig) { + self.config = config; } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000000000..bebbeb5369d01 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,333 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::chrome::ChromeManager; +use crate::edge::EdgeManager; +use crate::files::compose_cache_folder; +use crate::firefox::FirefoxManager; +use crate::iexplorer::IExplorerManager; +use std::fs; + +use crate::config::{str_to_os, ManagerConfig}; +use std::collections::HashMap; +use std::error::Error; +use std::path::PathBuf; +use std::process::Command; + +use crate::downloads::download_driver_to_tmp_folder; +use crate::files::{parse_version, uncompress, BrowserPath}; +use crate::metadata::{ + create_browser_metadata, get_browser_version_from_metadata, get_metadata, write_metadata, +}; + +pub mod chrome; +pub mod config; +pub mod downloads; +pub mod edge; +pub mod files; +pub mod firefox; +pub mod iexplorer; +pub mod metadata; + +pub const STABLE: &str = "stable"; +pub const BETA: &str = "beta"; +pub const DEV: &str = "dev"; +pub const CANARY: &str = "canary"; +pub const NIGHTLY: &str = "nightly"; +pub const WMIC_COMMAND: &str = r#"wmic datafile where name='%{}:\=\\%{}' get Version /value"#; +pub const REG_QUERY: &str = r#"REG QUERY {} /v version"#; +pub const DASH_VERSION: &str = "{} -v"; +pub const DASH_DASH_VERSION: &str = "{} --version"; +pub const ENV_PROGRAM_FILES: &str = "PROGRAMFILES"; +pub const ENV_PROGRAM_FILES_X86: &str = "PROGRAMFILES(X86)"; +pub const ENV_LOCALAPPDATA: &str = "LOCALAPPDATA"; + +pub trait SeleniumManager { + // ---------------------------------------------------------- + // Browser-specific functions + // ---------------------------------------------------------- + + fn get_browser_name(&self) -> &str; + + fn get_browser_path_map(&self) -> HashMap; + + fn discover_browser_version(&self) -> Option; + + fn get_driver_name(&self) -> &str; + + fn request_driver_version(&self) -> Result>; + + fn get_driver_url(&self) -> Result>; + + fn get_driver_path_in_cache(&self) -> PathBuf; + + fn get_config(&self) -> &ManagerConfig; + + fn set_config(&mut self, config: ManagerConfig); + + // ---------------------------------------------------------- + // Shared functions + // ---------------------------------------------------------- + + fn download_driver(&self) -> Result<(), Box> { + let driver_url = Self::get_driver_url(self)?; + let (_tmp_folder, driver_zip_file) = download_driver_to_tmp_folder(driver_url)?; + let driver_path_in_cache = Self::get_driver_path_in_cache(self); + uncompress(&driver_zip_file, driver_path_in_cache) + } + + fn get_browser_path(&self) -> Option<&str> { + let mut browser_version = self.get_browser_version(); + if browser_version.eq_ignore_ascii_case(CANARY) { + browser_version = NIGHTLY; + } else if browser_version.is_empty() { + browser_version = STABLE; + } + self.get_browser_path_map() + .get(&BrowserPath::new(str_to_os(self.get_os()), browser_version)) + .cloned() + } + + fn detect_browser_version(&self, shell: &str, flag: &str, args: Vec) -> Option { + let mut metadata = get_metadata(); + let browser_name = &self.get_browser_name(); + + match get_browser_version_from_metadata(&metadata.browsers, browser_name) { + Some(version) => { + log::trace!( + "Browser with valid TTL. Getting {} version from metadata", + browser_name + ); + Some(version) + } + _ => { + log::debug!("Using shell command to find out {} version", browser_name); + let mut browser_version = "".to_string(); + for arg in args.iter() { + let output = match self.run_shell_command(shell, flag, arg.to_string()) { + Ok(out) => out, + Err(_e) => continue, + }; + let full_browser_version = parse_version(output).unwrap_or_default(); + if full_browser_version.is_empty() { + continue; + } + log::debug!( + "The version of {} is {}", + browser_name, + full_browser_version + ); + match self.get_major_version(&full_browser_version) { + Ok(v) => browser_version = v, + Err(_) => return None, + } + break; + } + + metadata + .browsers + .push(create_browser_metadata(browser_name, &browser_version)); + write_metadata(&metadata); + Some(browser_version) + } + } + } + + fn discover_driver_version(&mut self) -> String { + if self.get_browser_version().is_empty() || self.is_browser_version_unstable() { + match self.discover_browser_version() { + Some(version) => { + log::debug!("Detected browser: {} {}", self.get_browser_name(), version); + self.set_browser_version(version); + } + None => { + log::debug!( + "The version of {} cannot be detected. Trying with latest driver version", + self.get_browser_name() + ); + } + } + } + let driver_version = self + .request_driver_version() + .unwrap_or_else(|err| err.to_string()); + log::debug!( + "Required driver: {} {}", + self.get_driver_name(), + driver_version + ); + driver_version + } + + fn is_browser_version_unstable(&self) -> bool { + let browser_version = self.get_browser_version(); + browser_version.eq_ignore_ascii_case(BETA) + || browser_version.eq_ignore_ascii_case(DEV) + || browser_version.eq_ignore_ascii_case(NIGHTLY) + || browser_version.eq_ignore_ascii_case(CANARY) + } + + fn resolve_driver(&mut self) -> Result> { + if self.get_driver_version().is_empty() { + let driver_version = self.discover_driver_version(); + self.set_driver_version(driver_version); + } + + let driver_path = self.get_driver_path_in_cache(); + if driver_path.exists() { + log::debug!( + "{} {} already in the cache", + self.get_driver_name(), + self.get_driver_version() + ); + } else { + self.download_driver()?; + } + Ok(driver_path) + } + + fn run_shell_command( + &self, + command: &str, + flag: &str, + args: String, + ) -> Result> { + log::debug!("Running {} command: {:?}", command, args); + let output = Command::new(command).args([flag, args.as_str()]).output()?; + log::debug!("{:?}", output); + + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } + + fn get_major_version(&self, full_version: &str) -> Result> { + get_index_version(full_version, 0) + } + + fn get_minor_version(&self, full_version: &str) -> Result> { + get_index_version(full_version, 1) + } + + fn format_one_arg(&self, string: &str, arg1: &str) -> String { + string.replacen("{}", arg1, 1) + } + + fn format_two_args(&self, string: &str, arg1: &str, arg2: &str) -> String { + string.replacen("{}", arg1, 1).replacen("{}", arg2, 2) + } + + // ---------------------------------------------------------- + // Getters and setters for configuration parameters + // ---------------------------------------------------------- + + fn get_os(&self) -> &str { + self.get_config().os.as_str() + } + + fn set_os(&mut self, os: String) { + let mut config = ManagerConfig::clone(self.get_config()); + config.os = os; + self.set_config(config); + } + + fn get_arch(&self) -> &str { + self.get_config().arch.as_str() + } + + fn set_arch(&mut self, arch: String) { + let mut config = ManagerConfig::clone(self.get_config()); + config.arch = arch; + self.set_config(config); + } + + fn get_browser_version(&self) -> &str { + self.get_config().browser_version.as_str() + } + + fn set_browser_version(&mut self, browser_version: String) { + let mut config = ManagerConfig::clone(self.get_config()); + config.browser_version = browser_version; + self.set_config(config); + } + + fn get_driver_version(&self) -> &str { + self.get_config().driver_version.as_str() + } + + fn set_driver_version(&mut self, driver_version: String) { + let mut config = ManagerConfig::clone(self.get_config()); + config.driver_version = driver_version; + self.set_config(config); + } +} + +// ---------------------------------------------------------- +// Public functions +// ---------------------------------------------------------- + +pub fn get_manager_by_browser(browser_name: String) -> Result, String> { + if browser_name.eq_ignore_ascii_case("chrome") { + Ok(ChromeManager::new()) + } else if browser_name.eq_ignore_ascii_case("firefox") { + Ok(FirefoxManager::new()) + } else if browser_name.eq_ignore_ascii_case("edge") { + Ok(EdgeManager::new()) + } else if browser_name.eq_ignore_ascii_case("iexplorer") { + Ok(IExplorerManager::new()) + } else { + Err(format!("Invalid browser name: {browser_name}")) + } +} + +pub fn get_manager_by_driver(driver_name: String) -> Result, String> { + if driver_name.eq_ignore_ascii_case("chromedriver") { + Ok(ChromeManager::new()) + } else if driver_name.eq_ignore_ascii_case("geckodriver") { + Ok(FirefoxManager::new()) + } else if driver_name.eq_ignore_ascii_case("msedgedriver") { + Ok(EdgeManager::new()) + } else if driver_name.eq_ignore_ascii_case("iedriverserver") { + Ok(IExplorerManager::new()) + } else { + Err(format!("Invalid driver name: {driver_name}")) + } +} + +pub fn clear_cache() { + let cache_path = compose_cache_folder(); + if cache_path.exists() { + log::debug!("Clearing cache at: {}", cache_path.display()); + fs::remove_dir_all(&cache_path).unwrap_or_else(|err| { + log::warn!( + "The cache {} cannot be cleared: {}", + cache_path.display(), + err + ) + }); + } +} + +// ---------------------------------------------------------- +// Private functions +// ---------------------------------------------------------- + +fn get_index_version(full_version: &str, index: usize) -> Result> { + let version_vec: Vec<&str> = full_version.split('.').collect(); + Ok(version_vec + .get(index) + .ok_or(format!("Wrong version: {}", full_version))? + .to_string()) +} diff --git a/rust/src/main.rs b/rust/src/main.rs index ed3c5b07ba77f..88c54b2fe73ac 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -use std::env::consts::{ARCH, OS}; use std::error::Error; use std::io::Write; use std::process::exit; @@ -23,25 +22,15 @@ use std::process::exit; use clap::Parser; use env_logger::fmt::Color; use env_logger::Target::Stdout; +use exitcode::DATAERR; +use Color::{Blue, Cyan, Green, Red, Yellow}; use log::Level; use log::LevelFilter::{Debug, Info, Trace}; -use crate::chrome::ChromeManager; -use crate::edge::EdgeManager; -use crate::files::clear_cache; -use crate::firefox::FirefoxManager; -use crate::iexplorer::IExplorerManager; -use crate::manager::{is_unstable, BrowserManager}; - -mod chrome; -mod downloads; -mod edge; -mod files; -mod firefox; -mod iexplorer; -mod manager; -mod metadata; +use selenium_manager::{ + clear_cache, get_manager_by_browser, get_manager_by_driver, SeleniumManager, +}; /// Automated driver management for Selenium #[derive(Parser, Debug)] @@ -83,83 +72,39 @@ struct Cli { fn main() -> Result<(), Box> { let cli = Cli::parse(); setup_logging(&cli); - let browser_name: String = cli.browser; - let driver_name: String = cli.driver; - let os = OS; - let arch = ARCH; - let mut driver_version = cli.driver_version; - let mut browser_version = cli.browser_version; - - let browser_manager: Box = if browser_name.eq_ignore_ascii_case("chrome") - || driver_name.eq_ignore_ascii_case("chromedriver") - { - ChromeManager::new() - } else if browser_name.eq_ignore_ascii_case("firefox") - || driver_name.eq_ignore_ascii_case("geckodriver") - { - FirefoxManager::new() - } else if browser_name.eq_ignore_ascii_case("edge") - || driver_name.eq_ignore_ascii_case("msedgedriver") - { - EdgeManager::new() - } else if browser_name.eq_ignore_ascii_case("iexplorer") - || driver_name.eq_ignore_ascii_case("iedriverserver") - { - IExplorerManager::new() - } else { - exit_with_error("Invalid browser/driver name".to_string()); - exit(exitcode::DATAERR); - }; if cli.clear_cache { clear_cache(); } - if !driver_version.is_empty() && !browser_version.is_empty() { - log::warn!("Ignoring --browser-version (since --driver-version is also used)"); - } + let browser_name: String = cli.browser; + let driver_name: String = cli.driver; - if driver_version.is_empty() { - if browser_version.is_empty() || is_unstable(&browser_version) { - match browser_manager.get_browser_version(os, &browser_version) { - Some(version) => { - browser_version = version; - log::debug!("Detected browser: {} {}", browser_name, browser_version); - } - None => { - log::debug!( - "The version of {} cannot be detected. Trying with latest driver version", - browser_name - ); - } - } - } - match browser_manager.get_driver_version(&browser_version, os) { - Ok(d) => { - driver_version = d; - } - Err(err) => { - exit_with_error(err.to_string()); - } - } - log::debug!( - "Required driver: {} {}", - browser_manager.get_driver_name(), - driver_version - ); - } + let mut selenium_manager: Box = if !browser_name.is_empty() { + get_manager_by_browser(browser_name).unwrap_or_else(|err| { + log::error!("{}", err); + exit(DATAERR); + }) + } else if !driver_name.is_empty() { + get_manager_by_driver(driver_name).unwrap_or_else(|err| { + log::error!("{}", err); + exit(DATAERR); + }) + } else { + log::error!("You need to specify a browser or driver"); + exit(DATAERR); + }; - let driver_path = browser_manager.get_driver_path_in_cache(&driver_version, os, arch); - if driver_path.exists() { - log::debug!( - "{} {} already in the cache", - browser_manager.get_driver_name(), - driver_version - ); - } else if let Err(err) = browser_manager.download_driver(&driver_version, os, arch) { - exit_with_error(err.to_string()); - } - log::info!("{}", driver_path.display()); + selenium_manager.set_browser_version(cli.browser_version); + selenium_manager.set_driver_version(cli.driver_version); + + match selenium_manager.resolve_driver() { + Ok(driver_path) => log::info!("{}", driver_path.display()), + Err(err) => { + log::error!("{}", err); + exit(DATAERR); + } + }; Ok(()) } @@ -179,11 +124,11 @@ fn setup_logging(cli: &Cli) { .format(|buf, record| { let mut level_style = buf.style(); match record.level() { - Level::Trace => level_style.set_color(Color::Cyan), - Level::Debug => level_style.set_color(Color::Blue), - Level::Info => level_style.set_color(Color::Green), - Level::Warn => level_style.set_color(Color::Yellow), - Level::Error => level_style.set_color(Color::Red).set_bold(true), + Level::Trace => level_style.set_color(Cyan), + Level::Debug => level_style.set_color(Blue), + Level::Info => level_style.set_color(Green), + Level::Warn => level_style.set_color(Yellow), + Level::Error => level_style.set_color(Red).set_bold(true), }; writeln!( buf, @@ -194,8 +139,3 @@ fn setup_logging(cli: &Cli) { }) .init(); } - -fn exit_with_error(err: String) { - log::error!("{}", err); - exit(exitcode::DATAERR); -} diff --git a/rust/src/manager.rs b/rust/src/manager.rs deleted file mode 100644 index 5f2a366891620..0000000000000 --- a/rust/src/manager.rs +++ /dev/null @@ -1,249 +0,0 @@ -// Licensed to the Software Freedom Conservancy (SFC) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The SFC licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::collections::HashMap; -use std::error::Error; -use std::path::PathBuf; -use std::process::Command; - -use crate::downloads::download_driver_to_tmp_folder; -use crate::files::{parse_version, uncompress}; -use crate::manager::OS::{LINUX, MACOS, WINDOWS}; - -use crate::metadata::{ - create_browser_metadata, get_browser_version_from_metadata, get_metadata, write_metadata, -}; - -pub const STABLE: &str = "stable"; -pub const BETA: &str = "beta"; -pub const DEV: &str = "dev"; -pub const CANARY: &str = "canary"; -pub const NIGHTLY: &str = "nightly"; -pub const WMIC_COMMAND: &str = r#"wmic datafile where name='%{}:\=\\%{}' get Version /value"#; -pub const REG_QUERY: &str = r#"REG QUERY {} /v version"#; -pub const DASH_VERSION: &str = "{} -v"; -pub const DASH_DASH_VERSION: &str = "{} --version"; -pub const ENV_PROGRAM_FILES: &str = "PROGRAMFILES"; -pub const ENV_PROGRAM_FILES_X86: &str = "PROGRAMFILES(X86)"; -pub const ENV_LOCALAPPDATA: &str = "LOCALAPPDATA"; - -pub trait BrowserManager { - fn get_browser_name(&self) -> &str; - - fn get_browser_path_map(&self) -> HashMap; - - fn get_browser_version(&self, os: &str, browser_version: &str) -> Option; - - fn get_driver_name(&self) -> &str; - - fn get_driver_version(&self, browser_version: &str, os: &str) - -> Result>; - - fn get_driver_url( - &self, - driver_version: &str, - os: &str, - arch: &str, - ) -> Result>; - - fn get_driver_path_in_cache(&self, driver_version: &str, os: &str, arch: &str) -> PathBuf; - - fn download_driver( - &self, - driver_version: &str, - os: &str, - arch: &str, - ) -> Result<(), Box> { - let driver_url = Self::get_driver_url(self, driver_version, os, arch)?; - let (_tmp_folder, driver_zip_file) = download_driver_to_tmp_folder(driver_url)?; - let driver_path_in_cache = Self::get_driver_path_in_cache(self, driver_version, os, arch); - uncompress(&driver_zip_file, driver_path_in_cache) - } - - fn get_browser_path(&self, os: &str, mut browser_version: &str) -> Option<&str> { - if browser_version.eq_ignore_ascii_case(CANARY) { - browser_version = NIGHTLY; - } else if browser_version.is_empty() { - browser_version = STABLE; - } - self.get_browser_path_map() - .get(&BrowserPath::new(str_to_os(os), browser_version)) - .cloned() - } -} - -#[allow(dead_code)] -#[allow(clippy::upper_case_acronyms)] -#[derive(Hash, Eq, PartialEq, Debug)] -pub enum OS { - WINDOWS, - MACOS, - LINUX, -} - -impl OS { - pub fn to_str(&self) -> &str { - match self { - WINDOWS => "windows", - MACOS => "macos", - LINUX => "linux", - } - } - - pub fn is(&self, os: &str) -> bool { - self.to_str().eq_ignore_ascii_case(os) - } -} - -fn str_to_os(os: &str) -> OS { - if WINDOWS.is(os) { - WINDOWS - } else if MACOS.is(os) { - MACOS - } else { - LINUX - } -} - -#[allow(dead_code)] -#[allow(clippy::upper_case_acronyms)] -pub enum ARCH { - X32, - X64, - ARM64, -} - -impl ARCH { - pub fn to_str(&self) -> &str { - match self { - ARCH::X32 => "x86", - ARCH::X64 => "x86_64", - ARCH::ARM64 => "aarch64", - } - } - - pub fn is(&self, arch: &str) -> bool { - self.to_str().eq_ignore_ascii_case(arch) - } -} - -#[derive(Hash, Eq, PartialEq, Debug)] -pub struct BrowserPath { - os: OS, - channel: String, -} - -impl BrowserPath { - pub fn new(os: OS, channel: &str) -> BrowserPath { - BrowserPath { - os, - channel: channel.to_string(), - } - } -} - -pub fn run_shell_command( - command: &str, - flag: &str, - args: String, -) -> Result> { - log::debug!("Running {} command: {:?}", command, args); - let output = Command::new(command).args([flag, args.as_str()]).output()?; - log::debug!("{:?}", output); - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - -pub fn detect_browser_version( - browser_name: &str, - shell: &str, - flag: &str, - args: Vec, -) -> Option { - let mut metadata = get_metadata(); - - match get_browser_version_from_metadata(&metadata.browsers, browser_name) { - Some(version) => { - log::trace!( - "Browser with valid TTL. Getting {} version from metadata", - browser_name - ); - Some(version) - } - _ => { - log::debug!("Using shell command to find out {} version", browser_name); - let mut browser_version = "".to_string(); - for arg in args.iter() { - let output = match run_shell_command(shell, flag, arg.to_string()) { - Ok(out) => out, - Err(_e) => continue, - }; - let full_browser_version = parse_version(output).unwrap_or_default(); - if full_browser_version.is_empty() { - continue; - } - log::debug!( - "The version of {} is {}", - browser_name, - full_browser_version - ); - match get_major_version(&full_browser_version) { - Ok(v) => browser_version = v, - Err(_) => return None, - } - break; - } - - metadata - .browsers - .push(create_browser_metadata(browser_name, &browser_version)); - write_metadata(&metadata); - Some(browser_version) - } - } -} - -pub fn get_major_version(full_version: &str) -> Result> { - get_index_version(full_version, 0) -} - -pub fn get_minor_version(full_version: &str) -> Result> { - get_index_version(full_version, 1) -} - -fn get_index_version(full_version: &str, index: usize) -> Result> { - let version_vec: Vec<&str> = full_version.split('.').collect(); - Ok(version_vec - .get(index) - .ok_or(format!("Wrong version: {}", full_version))? - .to_string()) -} - -pub fn format_one_arg(string: &str, arg1: &str) -> String { - string.replacen("{}", arg1, 1) -} - -pub fn format_two_args(string: &str, arg1: &str, arg2: &str) -> String { - string.replacen("{}", arg1, 1).replacen("{}", arg2, 2) -} - -pub fn is_unstable(browser_version: &str) -> bool { - browser_version.eq_ignore_ascii_case(BETA) - || browser_version.eq_ignore_ascii_case(DEV) - || browser_version.eq_ignore_ascii_case(NIGHTLY) - || browser_version.eq_ignore_ascii_case(CANARY) -}