diff --git a/rust/Cargo.Bazel.lock b/rust/Cargo.Bazel.lock index 1bcfec94e2f28..da69ac1bc2c03 100644 --- a/rust/Cargo.Bazel.lock +++ b/rust/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "e324d731a7a745741843d13e869e9d16f097a4e797b827d05d06dcbd0a5cb7b0", + "checksum": "0a20f0fe60fda64d9bb35f8a482f95239754a053b83e53e92d2a259eab911369", "crates": { "addr2line 0.19.0": { "name": "addr2line", @@ -7075,9 +7075,9 @@ }, "license": "Apache-2.0/ISC/MIT" }, - "selenium-manager 1.0.0-M3": { + "selenium-manager 1.0.0-M4": { "name": "selenium-manager", - "version": "1.0.0-M3", + "version": "1.0.0-M4", "repository": null, "targets": [ { @@ -7182,7 +7182,7 @@ "selects": {} }, "edition": "2021", - "version": "1.0.0-M3" + "version": "1.0.0-M4" }, "license": "Apache-2.0" }, @@ -11822,7 +11822,7 @@ }, "binary_crates": [], "workspace_members": { - "selenium-manager 1.0.0-M3": "rust" + "selenium-manager 1.0.0-M4": "rust" }, "conditions": { "aarch64-apple-darwin": [ diff --git a/rust/Cargo.lock b/rust/Cargo.lock index cb8463149da17..8f3786303d02e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1315,7 +1315,7 @@ dependencies = [ [[package]] name = "selenium-manager" -version = "1.0.0-M3" +version = "1.0.0-M4" dependencies = [ "assert_cmd", "clap", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2218bce92a472..d6d1dcd695425 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "selenium-manager" -version = "1.0.0-M3" +version = "1.0.0-M4" edition = "2021" authors = ["Selenium Major browser version (e.g., 105, 106, etc. Also: beta, dev, canary -or nightly- is accepted) --browser-path - Browser path (absolute) for browser version detection (e.g., /usr/bin/google-chrome, "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome", "C:\Program Files\Google\Chrome\Application\chrome.exe") + Browser path (absolute) for browser version detection (e.g., /usr/bin/google-chrome, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "C:\Program Files\Google\Chrome\Application\chrome.exe") --output Output type: LOGGER (using INFO, WARN, etc.), JSON (custom JSON notation), or SHELL (Unix-like) [default: LOGGER] --proxy HTTP proxy for network connection (e.g., https://myproxy.net:8080) --timeout - Timeout for network requests (in seconds) [default: 180] + Timeout for network requests (in seconds) [default: 300] --driver-ttl Driver TTL (time-to-live) [default: 3600] --browser-ttl - Browser TTL (time-to-live) [default: 0] + Browser TTL (time-to-live) [default: 3600] --clear-cache Clear cache folder (~/.cache/selenium) --clear-metadata @@ -53,6 +53,8 @@ Options: Display TRACE messages --offline Offline mode (i.e., disabling network requests and downloads) + --force-browser-download + Force to download browser. Currently Chrome for Testing (CfT) is supported -h, --help Print help -V, --version diff --git a/rust/src/chrome.rs b/rust/src/chrome.rs index a00569871b38a..84b479b3f527d 100644 --- a/rust/src/chrome.rs +++ b/rust/src/chrome.rs @@ -26,15 +26,18 @@ use std::path::PathBuf; 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::files::{ + compose_driver_path_in_cache, get_cache_folder, path_buf_to_string, 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_one_arg, format_three_args, SeleniumManager, BETA, - DASH_DASH_VERSION, DEV, ENV_LOCALAPPDATA, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, NIGHTLY, - OFFLINE_REQUEST_ERR_MSG, REG_QUERY, REMOVE_X86, STABLE, WMIC_COMMAND, WMIC_COMMAND_ENV, + create_browser_metadata, create_http_client, download_to_tmp_folder, format_one_arg, + format_three_args, get_browser_version_from_metadata, uncompress, SeleniumManager, BETA, + DASH_DASH_VERSION, DEV, DOUBLE_QUOTE, NIGHTLY, OFFLINE_REQUEST_ERR_MSG, REG_QUERY, + SINGLE_QUOTE, STABLE, WMIC_COMMAND, }; pub const CHROME_NAME: &str = "chrome"; @@ -44,6 +47,8 @@ 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"; pub struct ChromeManager { pub browser_name: &'static str, @@ -51,7 +56,7 @@ pub struct ChromeManager { pub config: ManagerConfig, pub http_client: Client, pub log: Logger, - pub driver_url: Option, + pub download_url: Option, } impl ChromeManager { @@ -67,7 +72,7 @@ impl ChromeManager { http_client: create_http_client(default_timeout, default_proxy)?, config, log: Logger::default(), - driver_url: None, + download_url: None, })) } @@ -107,19 +112,51 @@ impl ChromeManager { where T: Serialize + for<'a> Deserialize<'a>, { - self.log.debug(format!( - "Reading {} metadata from {}", - &self.driver_name, driver_url - )); + self.log + .debug(format!("Reading metadata from {}", driver_url)); parse_json_from_url::(self.get_http_client(), driver_url) } - fn request_latest_driver_version_from_cft(&mut self) -> Result> { + fn request_latest_browser_version_from_cft(&mut self) -> Result> { + 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 versions_with_downloads = self .request_versions_from_cft::( self.create_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.download_url = Some(platform_url.first().unwrap().url.to_string()); + Ok(browser_version) + } + + fn request_latest_driver_version_from_cft(&mut self) -> Result> { + 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 versions_with_downloads = self + .request_versions_from_cft::( + self.create_latest_versions_url(), + )?; let stable_channel = versions_with_downloads.channels.stable; let chromedriver = stable_channel.downloads.chromedriver; if chromedriver.is_none() { @@ -131,14 +168,18 @@ impl ChromeManager { return self.request_driver_version_from_latest(self.create_latest_release_url()); } - let url: Vec<&PlatformUrl> = chromedriver + 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!("URLs for CfT: {:?}", url)); - self.driver_url = Some(url.first().unwrap().url.to_string()); + self.log.trace(format!( + "CfT URLs for downloading {}: {:?}", + self.get_driver_name(), + platform_url + )); + self.download_url = Some(platform_url.first().unwrap().url.to_string()); Ok(stable_channel.version) } @@ -180,7 +221,7 @@ impl ChromeManager { .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()); + self.download_url = Some(url.first().unwrap().url.to_string()); Ok(driver_version.version.to_string()) } @@ -204,6 +245,13 @@ impl ChromeManager { "linux64" } } + + fn get_browser_path_in_cache(&self) -> PathBuf { + get_cache_folder() + .join(self.get_browser_name()) + .join(self.get_platform_label()) + .join(self.get_browser_version()) + } } impl SeleniumManager for ChromeManager { @@ -223,65 +271,55 @@ impl SeleniumManager for ChromeManager { HashMap::from([ ( BrowserPath::new(WINDOWS, STABLE), - r#"\\Google\\Chrome\\Application\\chrome.exe"#, + r#"Google\Chrome\Application\chrome.exe"#, ), ( BrowserPath::new(WINDOWS, BETA), - r#"\\Google\\Chrome Beta\\Application\\chrome.exe"#, + r#"Google\Chrome Beta\Application\chrome.exe"#, ), ( BrowserPath::new(WINDOWS, DEV), - r#"\\Google\\Chrome Dev\\Application\\chrome.exe"#, + r#"Google\Chrome Dev\Application\chrome.exe"#, ), ( BrowserPath::new(WINDOWS, NIGHTLY), - r#"\\Google\\Chrome SxS\\Application\\chrome.exe"#, + r#"Google\Chrome SxS\Application\chrome.exe"#, ), ( BrowserPath::new(MACOS, STABLE), - r#"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"#, + r#"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"#, ), ( BrowserPath::new(MACOS, BETA), - r#"/Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta"#, + r#"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta"#, ), ( BrowserPath::new(MACOS, DEV), - r#"/Applications/Google\ Chrome\ Dev.app/Contents/MacOS/Google\ Chrome\ Dev"#, + r#"/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev"#, ), ( BrowserPath::new(MACOS, NIGHTLY), - r#"/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"#, + r#"/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", ), - (BrowserPath::new(LINUX, STABLE), "google-chrome"), - (BrowserPath::new(LINUX, BETA), "google-chrome-beta"), - (BrowserPath::new(LINUX, DEV), "google-chrome-unstable"), ]) } - fn discover_browser_version(&self) -> Option { + fn discover_browser_version(&mut self) -> Option { let mut commands; - let escaped_browser_path = self.get_escaped_browser_path(); - let mut browser_path = escaped_browser_path.as_str(); + let mut browser_path = self.get_browser_path().to_string(); + let escaped_browser_path; if browser_path.is_empty() { match self.detect_browser_path() { Some(path) => { - browser_path = path; - commands = vec![ - format_three_args( - WMIC_COMMAND_ENV, - ENV_PROGRAM_FILES, - REMOVE_X86, - browser_path, - ), - format_three_args( - WMIC_COMMAND_ENV, - ENV_PROGRAM_FILES_X86, - "", - browser_path, - ), - format_three_args(WMIC_COMMAND_ENV, ENV_LOCALAPPDATA, "", browser_path), - ]; + browser_path = path_buf_to_string(path); + escaped_browser_path = self.get_escaped_path(browser_path.to_string()); + commands = vec![format_one_arg(WMIC_COMMAND, &escaped_browser_path)]; if !self.is_browser_version_unstable() { commands.push(format_one_arg( REG_QUERY, @@ -292,10 +330,16 @@ impl SeleniumManager for ChromeManager { _ => return None, } } else { - commands = vec![format_one_arg(WMIC_COMMAND, browser_path)]; + escaped_browser_path = self.get_escaped_path(browser_path.to_string()); + commands = vec![format_one_arg(WMIC_COMMAND, &escaped_browser_path)]; } if !WINDOWS.is(self.get_os()) { - commands = vec![format_one_arg(DASH_DASH_VERSION, browser_path)] + commands = vec![ + format_three_args(DASH_DASH_VERSION, "", &escaped_browser_path, ""), + format_three_args(DASH_DASH_VERSION, DOUBLE_QUOTE, &browser_path, DOUBLE_QUOTE), + format_three_args(DASH_DASH_VERSION, SINGLE_QUOTE, &browser_path, SINGLE_QUOTE), + format_three_args(DASH_DASH_VERSION, "", &browser_path, ""), + ] } self.detect_browser_version(commands) } @@ -359,14 +403,14 @@ impl SeleniumManager for ChromeManager { .parse::() .unwrap_or_default(); - if major_driver_version >= 115 && self.driver_url.is_none() { + if major_driver_version >= 115 && self.download_url.is_none() { // This case happens when driver_version is set (e.g. using CLI flag) self.request_good_version_from_cft()?; } // As of Chrome 115+, the driver URL is already gathered thanks to the CfT endpoints - if self.driver_url.is_some() { - return Ok(self.driver_url.as_ref().unwrap().to_string()); + if self.download_url.is_some() { + return Ok(self.download_url.as_ref().unwrap().to_string()); } let driver_version = self.get_driver_version(); @@ -421,6 +465,83 @@ impl SeleniumManager for ChromeManager { fn set_logger(&mut self, log: Logger) { self.log = log; } + + fn download_browser(&mut self) -> Result, Box> { + let browser_name = self.browser_name; + + // Checking latest version of Chrome for Testing (CfT) + let mut metadata = get_metadata(self.get_logger()); + let browser_version; + match get_browser_version_from_metadata(&metadata.browsers, browser_name) { + Some(version) => { + self.get_logger().trace(format!( + "Browser with valid TTL. Getting {} version from metadata", + browser_name + )); + browser_version = version; + self.set_browser_version(browser_version.clone()); + } + _ => { + browser_version = self.request_latest_browser_version_from_cft()?; + self.set_browser_version(browser_version.clone()); + let browser_ttl = self.get_browser_ttl(); + if browser_ttl > 0 && !browser_version.is_empty() { + metadata.browsers.push(create_browser_metadata( + browser_name, + &browser_version, + browser_ttl, + )); + write_metadata(&metadata, self.get_logger()); + } + } + } + self.get_logger().debug(format!( + "Required browser: {} {}", + browser_name, browser_version + )); + + // Checking if that browser version is in the cache + let browser_path_in_cache = Self::get_browser_path_in_cache(self); + let browser_path = if MACOS.is(self.get_os()) { + Some(browser_path_in_cache.join(CFT_MACOS_APP_NAME)) + } else { + Some(browser_path_in_cache.join(self.get_browser_name_with_extension())) + }; + if browser_path.clone().unwrap().exists() { + self.get_logger().debug(format!( + "{} {} already in the cache", + browser_name, browser_version + )); + } else { + // If browser is not in the cache, download it + let download_url = if let Some(url) = self.download_url.clone() { + url + } else { + self.request_latest_browser_version_from_cft()?; + self.download_url.clone().unwrap() + }; + self.get_logger().debug(format!( + "Downloading {} {} from {}", + self.get_browser_name(), + self.get_browser_version(), + download_url + )); + let (_tmp_folder, driver_zip_file) = + download_to_tmp_folder(self.get_http_client(), download_url, self.get_logger())?; + + uncompress( + &driver_zip_file, + &browser_path_in_cache, + self.get_logger(), + None, + )?; + } + if browser_path.is_some() { + self.set_browser_path(path_buf_to_string(browser_path.clone().unwrap())); + } + + Ok(browser_path) + } } #[derive(Serialize, Deserialize)] diff --git a/rust/src/config.rs b/rust/src/config.rs index 7f27d4617a587..bb1662a912dea 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -17,7 +17,7 @@ use crate::config::OS::{LINUX, MACOS, WINDOWS}; use crate::files::get_cache_folder; -use crate::{format_one_arg, run_shell_command, REQUEST_TIMEOUT_SEC, UNAME_COMMAND}; +use crate::{format_one_arg, run_shell_command_by_os, REQUEST_TIMEOUT_SEC, UNAME_COMMAND}; use crate::{ARCH_AMD64, ARCH_ARM64, ARCH_X86, TTL_BROWSERS_SEC, TTL_DRIVERS_SEC, WMIC_COMMAND_OS}; use std::env; use std::env::consts::OS; @@ -42,6 +42,7 @@ pub struct ManagerConfig { pub browser_ttl: u64, pub driver_ttl: u64, pub offline: bool, + pub force_browser_download: bool, } impl ManagerConfig { @@ -49,7 +50,7 @@ impl ManagerConfig { let self_os = OS; let self_arch = if WINDOWS.is(self_os) { let wmic_output = - run_shell_command(self_os, WMIC_COMMAND_OS.to_string()).unwrap_or_default(); + run_shell_command_by_os(self_os, WMIC_COMMAND_OS.to_string()).unwrap_or_default(); if wmic_output.contains("32") { ARCH_X86.to_string() } else if wmic_output.contains("ARM") { @@ -59,7 +60,7 @@ impl ManagerConfig { } } else { let uname_a = format_one_arg(UNAME_COMMAND, "a"); - if run_shell_command(self_os, uname_a) + if run_shell_command_by_os(self_os, uname_a) .unwrap_or_default() .to_ascii_lowercase() .contains(ARCH_ARM64) @@ -67,7 +68,7 @@ impl ManagerConfig { ARCH_ARM64.to_string() } else { let uname_m = format_one_arg(UNAME_COMMAND, "m"); - run_shell_command(self_os, uname_m).unwrap_or_default() + run_shell_command_by_os(self_os, uname_m).unwrap_or_default() } }; @@ -89,6 +90,7 @@ impl ManagerConfig { browser_ttl: IntegerKey("browser-ttl", TTL_BROWSERS_SEC).get_value(), driver_ttl: IntegerKey("driver-ttl", TTL_DRIVERS_SEC).get_value(), offline: BooleanKey("offline", false).get_value(), + force_browser_download: BooleanKey("force_browser_download", false).get_value(), } } } diff --git a/rust/src/downloads.rs b/rust/src/downloads.rs index 50d37c625a3e0..d21b8cf2025e0 100644 --- a/rust/src/downloads.rs +++ b/rust/src/downloads.rs @@ -28,7 +28,7 @@ use crate::files::parse_version; use crate::Logger; #[tokio::main] -pub async fn download_driver_to_tmp_folder( +pub async fn download_to_tmp_folder( http_client: &Client, url: String, log: &Logger, diff --git a/rust/src/edge.rs b/rust/src/edge.rs index 5daa3920ab474..98c8c307c7777 100644 --- a/rust/src/edge.rs +++ b/rust/src/edge.rs @@ -24,14 +24,14 @@ use std::path::PathBuf; use crate::config::ARCH::{ARM64, X32}; use crate::config::OS::{LINUX, MACOS, WINDOWS}; use crate::downloads::read_version_from_link; -use crate::files::{compose_driver_path_in_cache, BrowserPath}; +use crate::files::{compose_driver_path_in_cache, path_buf_to_string, BrowserPath}; use crate::metadata::{ create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata, }; use crate::{ create_http_client, format_one_arg, format_three_args, Logger, SeleniumManager, BETA, - DASH_DASH_VERSION, DEV, ENV_LOCALAPPDATA, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, NIGHTLY, - OFFLINE_REQUEST_ERR_MSG, REG_QUERY, REMOVE_X86, STABLE, WMIC_COMMAND, WMIC_COMMAND_ENV, + DASH_DASH_VERSION, DEV, DOUBLE_QUOTE, NIGHTLY, OFFLINE_REQUEST_ERR_MSG, REG_QUERY, + SINGLE_QUOTE, STABLE, WMIC_COMMAND, }; pub const EDGE_NAMES: &[&str] = &["edge", "msedge", "microsoftedge"]; @@ -82,65 +82,55 @@ impl SeleniumManager for EdgeManager { HashMap::from([ ( BrowserPath::new(WINDOWS, STABLE), - r#"\\Microsoft\\Edge\\Application\\msedge.exe"#, + r#"Microsoft\Edge\Application\msedge.exe"#, ), ( BrowserPath::new(WINDOWS, BETA), - r#"\\Microsoft\\Edge Beta\\Application\\msedge.exe"#, + r#"Microsoft\Edge Beta\Application\msedge.exe"#, ), ( BrowserPath::new(WINDOWS, DEV), - r#"\\Microsoft\\Edge Dev\\Application\\msedge.exe"#, + r#"Microsoft\Edge Dev\Application\msedge.exe"#, ), ( BrowserPath::new(WINDOWS, NIGHTLY), - r#"\\Microsoft\\Edge SxS\\Application\\msedge.exe"#, + r#"Microsoft\Edge SxS\Application\msedge.exe"#, ), ( BrowserPath::new(MACOS, STABLE), - r#"/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge"#, + r#"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"#, ), ( BrowserPath::new(MACOS, BETA), - r#"/Applications/Microsoft\ Edge\ Beta.app/Contents/MacOS/Microsoft\ Edge\ Beta"#, + r#"/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta"#, ), ( BrowserPath::new(MACOS, DEV), - r#"/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev"#, + r#"/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev"#, ), ( BrowserPath::new(MACOS, NIGHTLY), - r#"/Applications/Microsoft\ Edge\ Canary.app/Contents/MacOS/Microsoft\ Edge\ Canary"#, + r#"/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary"#, ), - (BrowserPath::new(LINUX, STABLE), "microsoft-edge"), - (BrowserPath::new(LINUX, BETA), "microsoft-edge-beta"), - (BrowserPath::new(LINUX, DEV), "microsoft-edge-dev"), + (BrowserPath::new(LINUX, STABLE), "/usr/bin/microsoft-edge"), + ( + BrowserPath::new(LINUX, BETA), + "/usr/bin/microsoft-edge-beta", + ), + (BrowserPath::new(LINUX, DEV), "/usr/bin/microsoft-edge-dev"), ]) } - fn discover_browser_version(&self) -> Option { + fn discover_browser_version(&mut self) -> Option { let mut commands; - let escaped_browser_path = self.get_escaped_browser_path(); - let mut browser_path = escaped_browser_path.as_str(); + let mut browser_path = self.get_browser_path().to_string(); + let escaped_browser_path; if browser_path.is_empty() { match self.detect_browser_path() { Some(path) => { - browser_path = path; - commands = vec![ - format_three_args( - WMIC_COMMAND_ENV, - ENV_PROGRAM_FILES_X86, - "", - browser_path, - ), - format_three_args( - WMIC_COMMAND_ENV, - ENV_PROGRAM_FILES, - REMOVE_X86, - browser_path, - ), - format_three_args(WMIC_COMMAND_ENV, ENV_LOCALAPPDATA, "", browser_path), - ]; + browser_path = path_buf_to_string(path); + escaped_browser_path = self.get_escaped_path(browser_path.to_string()); + commands = vec![format_one_arg(WMIC_COMMAND, &escaped_browser_path)]; if !self.is_browser_version_unstable() { commands.push(format_one_arg( REG_QUERY, @@ -151,10 +141,16 @@ impl SeleniumManager for EdgeManager { _ => return None, } } else { - commands = vec![format_one_arg(WMIC_COMMAND, browser_path)]; + escaped_browser_path = self.get_escaped_path(browser_path.to_string()); + commands = vec![format_one_arg(WMIC_COMMAND, &escaped_browser_path)]; } if !WINDOWS.is(self.get_os()) { - commands = vec![format_one_arg(DASH_DASH_VERSION, browser_path)] + commands = vec![ + format_three_args(DASH_DASH_VERSION, "", &escaped_browser_path, ""), + format_three_args(DASH_DASH_VERSION, DOUBLE_QUOTE, &browser_path, DOUBLE_QUOTE), + format_three_args(DASH_DASH_VERSION, SINGLE_QUOTE, &browser_path, SINGLE_QUOTE), + format_three_args(DASH_DASH_VERSION, "", &browser_path, ""), + ] } self.detect_browser_version(commands) } @@ -299,4 +295,8 @@ impl SeleniumManager for EdgeManager { fn set_logger(&mut self, log: Logger) { self.log = log; } + + fn download_browser(&mut self) -> Result, Box> { + Ok(None) + } } diff --git a/rust/src/files.rs b/rust/src/files.rs index 6d62d7bc924e8..28a359af6f865 100644 --- a/rust/src/files.rs +++ b/rust/src/files.rs @@ -70,6 +70,7 @@ pub fn uncompress( compressed_file: &str, target: &Path, log: &Logger, + single_file: Option, ) -> Result<(), Box> { let file = File::open(compressed_file)?; let kind = infer::get_from_path(compressed_file)? @@ -81,7 +82,7 @@ pub fn uncompress( )); if extension.eq_ignore_ascii_case(ZIP) { - unzip(file, target, log)? + unzip(file, target, log, single_file)? } else if extension.eq_ignore_ascii_case(GZ) { untargz(file, target, log)? } else if extension.eq_ignore_ascii_case(XML) || extension.eq_ignore_ascii_case(HTML) { @@ -113,39 +114,66 @@ pub fn untargz(file: File, target: &Path, log: &Logger) -> Result<(), Box Result<(), Box> { +pub fn unzip( + file: File, + target: &Path, + log: &Logger, + single_file: Option, +) -> Result<(), Box> { log.trace(format!("Unzipping file to {}", target.display())); + let mut out_path = target.to_path_buf(); let mut archive = ZipArchive::new(file)?; + let mut unzipped_files = 0; for i in 0..archive.len() { let mut file = archive.by_index(i)?; - if target.exists() { - continue; + let path: PathBuf = match file.enclosed_name() { + Some(p) => p.to_owned().iter().skip(1).collect(), + None => continue, + }; + if single_file.is_none() { + create_path_if_not_exists(target); + out_path = target.join(path); } - let target_file_name = target.file_name().unwrap().to_str().unwrap(); - if target_file_name.eq(get_raw_file_name(file.name())) { - log.debug(format!( + + if single_file.is_none() && file.name().ends_with('/') { + log.trace(format!("File {} extracted to {}", i, out_path.display())); + fs::create_dir_all(&out_path)?; + } else if single_file.is_none() + || (single_file.is_some() + && get_raw_file_name(file.name()).eq(&single_file.clone().unwrap())) + { + log.trace(format!( "File extracted to {} ({} bytes)", - target.display(), + out_path.display(), file.size() )); - create_parent_path_if_not_exists(target); - if !target.exists() { - let mut outfile = File::create(target)?; + create_parent_path_if_not_exists(out_path.as_path()); - // Set permissions in Unix-like systems - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; + let mut outfile = File::create(&out_path)?; + io::copy(&mut file, &mut outfile)?; + unzipped_files += 1; - fs::set_permissions(&target, fs::Permissions::from_mode(0o755))?; - } + // Set permissions in Unix-like systems + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; - io::copy(&mut file, &mut outfile)?; + if single_file.is_some() { + fs::set_permissions(&out_path, fs::Permissions::from_mode(0o755))?; + } else if let Some(mode) = file.unix_mode() { + fs::set_permissions(&out_path, fs::Permissions::from_mode(mode)).unwrap(); + } } - break; } } + if unzipped_files == 0 || (single_file.is_some() && unzipped_files != 1) { + return Err(format!( + "Problem uncompressing zip ({} files extracted)", + unzipped_files + ) + .into()); + } Ok(()) } @@ -216,3 +244,7 @@ pub fn parse_version(version_text: String, log: &Logger) -> Result String { + path_buf.into_os_string().into_string().unwrap_or_default() +} diff --git a/rust/src/firefox.rs b/rust/src/firefox.rs index a758ea8b3c520..319c111bacf08 100644 --- a/rust/src/firefox.rs +++ b/rust/src/firefox.rs @@ -24,14 +24,14 @@ 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, BrowserPath}; +use crate::files::{compose_driver_path_in_cache, path_buf_to_string, BrowserPath}; use crate::metadata::{ create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata, }; use crate::{ create_http_client, format_one_arg, format_three_args, format_two_args, Logger, - SeleniumManager, BETA, DASH_VERSION, DEV, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, NIGHTLY, - OFFLINE_REQUEST_ERR_MSG, REG_QUERY_FIND, REMOVE_X86, STABLE, WMIC_COMMAND, WMIC_COMMAND_ENV, + SeleniumManager, BETA, DASH_VERSION, DEV, DOUBLE_QUOTE, NIGHTLY, OFFLINE_REQUEST_ERR_MSG, + REG_QUERY_FIND, SINGLE_QUOTE, STABLE, WMIC_COMMAND, }; pub const FIREFOX_NAME: &str = "firefox"; @@ -81,19 +81,19 @@ impl SeleniumManager for FirefoxManager { HashMap::from([ ( BrowserPath::new(WINDOWS, STABLE), - r#"\\Mozilla Firefox\\firefox.exe"#, + r#"Mozilla Firefox\firefox.exe"#, ), ( BrowserPath::new(WINDOWS, BETA), - r#"\\Mozilla Firefox\\firefox.exe"#, + r#"Mozilla Firefox\firefox.exe"#, ), ( BrowserPath::new(WINDOWS, DEV), - r#"\\Firefox Developer Edition\\firefox.exe"#, + r#"Firefox Developer Edition\firefox.exe"#, ), ( BrowserPath::new(WINDOWS, NIGHTLY), - r#"\\Firefox Nightly\\firefox.exe"#, + r#"Firefox Nightly\firefox.exe"#, ), ( BrowserPath::new(MACOS, STABLE), @@ -105,41 +105,29 @@ impl SeleniumManager for FirefoxManager { ), ( BrowserPath::new(MACOS, DEV), - r#"/Applications/Firefox\ Developer\ Edition.app/Contents/MacOS/firefox"#, + r#"/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox"#, ), ( BrowserPath::new(MACOS, NIGHTLY), - r#"/Applications/Firefox\ Nightly.app/Contents/MacOS/firefox"#, + r#"/Applications/Firefox Nightly.app/Contents/MacOS/firefox"#, ), - (BrowserPath::new(LINUX, STABLE), "firefox"), - (BrowserPath::new(LINUX, BETA), "firefox"), - (BrowserPath::new(LINUX, DEV), "firefox"), - (BrowserPath::new(LINUX, NIGHTLY), "firefox-trunk"), + (BrowserPath::new(LINUX, STABLE), "/usr/bin/firefox"), + (BrowserPath::new(LINUX, BETA), "/usr/bin/firefox"), + (BrowserPath::new(LINUX, DEV), "/usr/bin/firefox"), + (BrowserPath::new(LINUX, NIGHTLY), "/usr/bin/firefox-trunk"), ]) } - fn discover_browser_version(&self) -> Option { + fn discover_browser_version(&mut self) -> Option { let mut commands; - let escaped_browser_path = self.get_escaped_browser_path(); - let mut browser_path = escaped_browser_path.as_str(); + let mut browser_path = self.get_browser_path().to_string(); + let escaped_browser_path; if browser_path.is_empty() { match self.detect_browser_path() { Some(path) => { - browser_path = path; - commands = vec![ - format_three_args( - WMIC_COMMAND_ENV, - ENV_PROGRAM_FILES, - REMOVE_X86, - browser_path, - ), - format_three_args( - WMIC_COMMAND_ENV, - ENV_PROGRAM_FILES_X86, - "", - browser_path, - ), - ]; + browser_path = path_buf_to_string(path); + escaped_browser_path = self.get_escaped_path(browser_path.to_string()); + commands = vec![format_one_arg(WMIC_COMMAND, &escaped_browser_path)]; if !self.is_browser_version_unstable() { commands.push(format_two_args( REG_QUERY_FIND, @@ -151,10 +139,16 @@ impl SeleniumManager for FirefoxManager { _ => return None, } } else { - commands = vec![format_one_arg(WMIC_COMMAND, browser_path)]; + escaped_browser_path = self.get_escaped_path(browser_path.to_string()); + commands = vec![format_one_arg(WMIC_COMMAND, &escaped_browser_path)]; } if !WINDOWS.is(self.get_os()) { - commands = vec![format_one_arg(DASH_VERSION, browser_path)] + commands = vec![ + format_three_args(DASH_VERSION, "", &escaped_browser_path, ""), + format_three_args(DASH_VERSION, DOUBLE_QUOTE, &browser_path, DOUBLE_QUOTE), + format_three_args(DASH_VERSION, SINGLE_QUOTE, &browser_path, SINGLE_QUOTE), + format_three_args(DASH_VERSION, "", &browser_path, ""), + ] } self.detect_browser_version(commands) } @@ -290,6 +284,10 @@ impl SeleniumManager for FirefoxManager { fn set_logger(&mut self, log: Logger) { self.log = log; } + + fn download_browser(&mut self) -> Result, Box> { + Ok(None) + } } #[cfg(test)] diff --git a/rust/src/grid.rs b/rust/src/grid.rs index 20604b82906cc..83a3e478d46f1 100644 --- a/rust/src/grid.rs +++ b/rust/src/grid.rs @@ -86,7 +86,7 @@ impl SeleniumManager for GridManager { HashMap::new() } - fn discover_browser_version(&self) -> Option { + fn discover_browser_version(&mut self) -> Option { None } @@ -209,4 +209,8 @@ impl SeleniumManager for GridManager { fn set_logger(&mut self, log: Logger) { self.log = log; } + + fn download_browser(&mut self) -> Result, Box> { + Ok(None) + } } diff --git a/rust/src/iexplorer.rs b/rust/src/iexplorer.rs index 508c9ec4b803a..ebaab73ad8007 100644 --- a/rust/src/iexplorer.rs +++ b/rust/src/iexplorer.rs @@ -24,7 +24,9 @@ use std::path::PathBuf; use crate::files::{compose_driver_path_in_cache, BrowserPath}; use crate::downloads::parse_json_from_url; -use crate::{create_http_client, parse_version, Logger, SeleniumManager, OFFLINE_REQUEST_ERR_MSG}; +use crate::{ + create_http_client, parse_version, Logger, SeleniumManager, OFFLINE_REQUEST_ERR_MSG, WINDOWS, +}; use crate::metadata::{ create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata, @@ -56,9 +58,10 @@ impl IExplorerManager { pub fn new() -> Result, Box> { let browser_name = IE_NAMES[0]; let driver_name = IEDRIVER_NAME; - let config = ManagerConfig::default(browser_name, driver_name); + let mut config = ManagerConfig::default(browser_name, driver_name); let default_timeout = config.timeout.to_owned(); let default_proxy = &config.proxy; + config.os = WINDOWS.to_str().to_string(); Ok(Box::new(IExplorerManager { browser_name, driver_name, @@ -87,7 +90,7 @@ impl SeleniumManager for IExplorerManager { HashMap::new() } - fn discover_browser_version(&self) -> Option { + fn discover_browser_version(&mut self) -> Option { None } @@ -205,4 +208,8 @@ impl SeleniumManager for IExplorerManager { fn set_logger(&mut self, log: Logger) { self.log = log; } + + fn download_browser(&mut self) -> Result, Box> { + Ok(None) + } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9ca4eaae18462..152f8cae4529c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -17,11 +17,14 @@ use crate::chrome::{ChromeManager, CHROMEDRIVER_NAME, CHROME_NAME}; use crate::edge::{EdgeManager, EDGEDRIVER_NAME, EDGE_NAMES}; -use crate::files::{compose_cache_folder, create_parent_path_if_not_exists}; +use crate::files::{ + compose_cache_folder, create_parent_path_if_not_exists, get_binary_extension, + path_buf_to_string, +}; use crate::firefox::{FirefoxManager, FIREFOX_NAME, GECKODRIVER_NAME}; use crate::iexplorer::{IExplorerManager, IEDRIVER_NAME, IE_NAMES}; use crate::safari::{SafariManager, SAFARIDRIVER_NAME, SAFARI_NAME}; -use std::fs; +use std::{env, fs}; use crate::config::OS::WINDOWS; use crate::config::{str_to_os, ManagerConfig}; @@ -33,13 +36,11 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; -use crate::downloads::download_driver_to_tmp_folder; +use crate::downloads::download_to_tmp_folder; use crate::files::{parse_version, uncompress, BrowserPath}; use crate::grid::GRID_NAME; use crate::logger::Logger; -use crate::metadata::{ - create_browser_metadata, get_browser_version_from_metadata, get_metadata, write_metadata, -}; +use crate::metadata::{create_browser_metadata, get_browser_version_from_metadata}; use crate::safaritp::{SafariTPManager, SAFARITP_NAMES}; pub mod chrome; @@ -56,35 +57,36 @@ pub mod mirror; pub mod safari; pub mod safaritp; -pub const REQUEST_TIMEOUT_SEC: u64 = 180; // The timeout is applied from when the request starts connecting until the response body has finished +pub const REQUEST_TIMEOUT_SEC: u64 = 300; // The timeout is applied from when the request starts connecting until the response body has finished 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 WMIC_COMMAND_ENV: &str = - r#"set PFILES=%{}{}%&& wmic datafile where name='!PFILES:\=\\!{}' get Version /value"#; pub const WMIC_COMMAND_OS: &str = r#"wmic os get osarchitecture"#; pub const REG_QUERY: &str = r#"REG QUERY {} /v version"#; pub const REG_QUERY_FIND: &str = r#"REG QUERY {} /f {}"#; pub const PLIST_COMMAND: &str = r#"/usr/libexec/PlistBuddy -c "print :CFBundleShortVersionString" {}/Contents/Info.plist"#; -pub const DASH_VERSION: &str = "{} -v"; -pub const DASH_DASH_VERSION: &str = "{} --version"; +pub const DASH_VERSION: &str = "{}{}{} -v"; +pub const DASH_DASH_VERSION: &str = "{}{}{} --version"; +pub const DOUBLE_QUOTE: &str = "\""; +pub const SINGLE_QUOTE: &str = "'"; pub const ENV_PROGRAM_FILES: &str = "PROGRAMFILES"; pub const ENV_PROGRAM_FILES_X86: &str = "PROGRAMFILES(X86)"; pub const ENV_LOCALAPPDATA: &str = "LOCALAPPDATA"; -pub const REMOVE_X86: &str = ": (x86)="; +pub const ENV_X86: &str = " (x86)"; pub const ARCH_X86: &str = "x86"; pub const ARCH_AMD64: &str = "amd64"; pub const ARCH_ARM64: &str = "arm64"; pub const ENV_PROCESSOR_ARCHITECTURE: &str = "PROCESSOR_ARCHITECTURE"; pub const WHERE_COMMAND: &str = "where {}"; pub const WHICH_COMMAND: &str = "which {}"; -pub const TTL_BROWSERS_SEC: u64 = 0; +pub const TTL_BROWSERS_SEC: u64 = 3600; pub const TTL_DRIVERS_SEC: u64 = 3600; pub const UNAME_COMMAND: &str = "uname -{}"; +pub const ESCAPE_COMMAND: &str = "printf %q \"{}\""; pub const CRLF: &str = "\r\n"; pub const LF: &str = "\n"; pub const SNAPSHOT: &str = "SNAPSHOT"; @@ -104,7 +106,7 @@ pub trait SeleniumManager { fn get_browser_path_map(&self) -> HashMap; - fn discover_browser_version(&self) -> Option; + fn discover_browser_version(&mut self) -> Option; fn get_driver_name(&self) -> &str; @@ -124,6 +126,8 @@ pub trait SeleniumManager { fn set_logger(&mut self, log: Logger); + fn download_browser(&mut self) -> Result, Box>; + // ---------------------------------------------------------- // Shared functions // ---------------------------------------------------------- @@ -133,7 +137,7 @@ pub trait SeleniumManager { self.get_logger() .debug(format!("Driver URL: {}", driver_url)); let (_tmp_folder, driver_zip_file) = - download_driver_to_tmp_folder(self.get_http_client(), driver_url, self.get_logger())?; + download_to_tmp_folder(self.get_http_client(), driver_url, self.get_logger())?; if self.is_grid() { let driver_path_in_cache = Self::get_driver_path_in_cache(self); @@ -141,74 +145,110 @@ pub trait SeleniumManager { Ok(fs::rename(driver_zip_file, driver_path_in_cache)?) } else { let driver_path_in_cache = Self::get_driver_path_in_cache(self); - uncompress(&driver_zip_file, &driver_path_in_cache, self.get_logger()) + let driver_name_with_extension = self.get_driver_name_with_extension(); + uncompress( + &driver_zip_file, + &driver_path_in_cache, + self.get_logger(), + Some(driver_name_with_extension), + ) } } - fn detect_browser_path(&self) -> Option<&str> { + fn detect_browser_path(&mut self) -> Option { 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() + let browser_path = self + .get_browser_path_map() .get(&BrowserPath::new(str_to_os(self.get_os()), browser_version)) .cloned() + .unwrap_or_default(); + + let mut full_browser_path = Path::new(browser_path).to_path_buf(); + if WINDOWS.is(self.get_os()) { + let envs = vec![ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, ENV_LOCALAPPDATA]; + + for env in envs { + let mut env_value = env::var(env).unwrap_or_default(); + if env.eq(ENV_PROGRAM_FILES) && env_value.contains(ENV_X86) { + // This special case is required to keep compliance between x32 and x64 + // architectures (since the selenium-manager in Windows is compiled as x32 binary) + env_value = env_value.replace(ENV_X86, ""); + } + let parent_path = Path::new(&env_value); + full_browser_path = parent_path.join(browser_path); + if full_browser_path.exists() { + break; + } + } + } + + if full_browser_path.exists() { + self.get_logger().debug(format!( + "{} detected at {}", + self.get_browser_name(), + full_browser_path.display() + )); + self.set_browser_path(path_buf_to_string(full_browser_path.clone())); + + Some(full_browser_path) + } else { + // Check browser in PATH + let browser_name = self.get_browser_name(); + self.get_logger() + .debug(format!("Checking {} in PATH", browser_name)); + let browser_in_path = self.find_browser_in_path(); + if let Some(path) = &browser_in_path { + self.get_logger().debug(format!( + "Found {} in PATH: {}", + browser_name, + path.display() + )); + } else { + self.get_logger() + .debug(format!("{} not found in PATH", browser_name)); + } + browser_in_path + } } fn detect_browser_version(&self, commands: Vec) -> Option { - let mut metadata = get_metadata(self.get_logger()); let browser_name = &self.get_browser_name(); - match get_browser_version_from_metadata(&metadata.browsers, browser_name) { - Some(version) => { - self.get_logger().trace(format!( - "Browser with valid TTL. Getting {} version from metadata", - browser_name - )); - Some(version) + self.get_logger().debug(format!( + "Using shell command to find out {} version", + browser_name + )); + let mut browser_version: Option = None; + for command in commands.iter() { + let output = match self.run_shell_command_with_log(command.to_string()) { + Ok(out) => out, + Err(_e) => continue, + }; + let full_browser_version = parse_version(output, self.get_logger()).unwrap_or_default(); + if full_browser_version.is_empty() { + continue; } - _ => { - self.get_logger().debug(format!( - "Using shell command to find out {} version", - browser_name - )); - let mut browser_version: Option = None; - for command in commands.iter() { - let output = match self.run_shell_command_with_log(command.to_string()) { - Ok(out) => out, - Err(_e) => continue, - }; - let full_browser_version = - parse_version(output, self.get_logger()).unwrap_or_default(); - if full_browser_version.is_empty() { - continue; - } - self.get_logger().trace(format!( - "The version of {} is {}", - browser_name, full_browser_version - )); - - browser_version = Some(full_browser_version); - break; - } + self.get_logger().trace(format!( + "The version of {} is {}", + browser_name, full_browser_version + )); - let browser_ttl = self.get_browser_ttl(); - if browser_ttl > 0 && browser_version.is_some() && !self.is_safari() { - metadata.browsers.push(create_browser_metadata( - browser_name, - browser_version.as_ref().unwrap(), - browser_ttl, - )); - write_metadata(&metadata, self.get_logger()); - } - browser_version - } + browser_version = Some(full_browser_version); + break; } + + browser_version } - fn discover_driver_version(&mut self) -> Result { + fn discover_driver_version(&mut self) -> Result> { + if self.is_force_browser_download() { + self.download_browser()?; + } let browser_version = self.get_major_browser_version(); if browser_version.is_empty() || self.is_browser_version_unstable() { match self.discover_browser_version() { @@ -223,8 +263,20 @@ pub trait SeleniumManager { } } None => { - if self.is_browser_version_unstable() { - return Err(format!("Browser version '{browser_version}' not found")); + self.get_logger().debug(format!( + "{} has not been discovered in the system", + self.get_browser_name() + )); + let browser_path = self.download_browser()?; + if browser_path.is_some() { + self.get_logger().debug(format!( + "{} {} has been downloaded at {}", + self.get_browser_name(), + self.get_browser_version(), + browser_path.unwrap().display() + )); + } else if self.is_browser_version_unstable() { + return Err(format!("Browser version '{browser_version}' not found").into()); } else if !self.is_iexplorer() && !self.is_grid() { self.get_logger().warn(format!( "The version of {} cannot be detected. Trying with latest driver version", @@ -234,62 +286,42 @@ pub trait SeleniumManager { } } } - let driver_version = match self.request_driver_version() { - Ok(version) => { - if version.is_empty() { - return Err(format!( - "The {} version cannot be discovered", - self.get_driver_name() - )); - } - version - } - Err(err) => { - return Err(err.to_string()); - } - }; - self.get_logger().debug(format!( - "Required driver: {} {}", - self.get_driver_name(), - driver_version - )); - Ok(driver_version) + let driver_version = self.request_driver_version()?; + if driver_version.is_empty() { + Err(format!( + "The {} version cannot be discovered", + self.get_driver_name() + ) + .into()) + } else { + self.get_logger().debug(format!( + "Required driver: {} {}", + self.get_driver_name(), + driver_version + )); + Ok(driver_version) + } + } + + fn find_browser_in_path(&self) -> Option { + let browser_path = self.execute_which_in_shell(self.get_browser_name()); + if let Some(path) = browser_path { + return Some(Path::new(&path).to_path_buf()); + } + None } fn find_driver_in_path(&self) -> (Option, Option) { - match self - .run_shell_command_with_log(format_one_arg(DASH_DASH_VERSION, self.get_driver_name())) - { + match self.run_shell_command_with_log(format_three_args( + DASH_DASH_VERSION, + self.get_driver_name(), + "", + "", + )) { Ok(output) => { let parsed_version = parse_version(output, self.get_logger()).unwrap_or_default(); if !parsed_version.is_empty() { - let which_command = if WINDOWS.is(self.get_os()) { - WHERE_COMMAND - } else { - WHICH_COMMAND - }; - let driver_path = match self.run_shell_command_with_log(format_one_arg( - which_command, - self.get_driver_name(), - )) { - Ok(path) => { - let path_vector = split_lines(path.as_str()); - if path_vector.len() == 1 { - Some(path_vector.first().unwrap().to_string()) - } else { - let exec_paths: Vec<&str> = path_vector - .into_iter() - .filter(|p| Path::new(p).is_executable()) - .collect(); - if exec_paths.is_empty() { - None - } else { - Some(exec_paths.first().unwrap().to_string()) - } - } - } - Err(_) => None, - }; + let driver_path = self.execute_which_in_shell(self.get_driver_name()); return (Some(parsed_version), driver_path); } (None, None) @@ -298,6 +330,46 @@ pub trait SeleniumManager { } } + fn execute_which_in_shell(&self, command: &str) -> Option { + let which_command = if WINDOWS.is(self.get_os()) { + WHERE_COMMAND + } else { + WHICH_COMMAND + }; + let path = match self.run_shell_command_with_log(format_one_arg(which_command, command)) { + Ok(path) => { + let path_vector = split_lines(path.as_str()); + if path_vector.len() == 1 { + self.get_first_in_vector(path_vector) + } else { + let exec_paths: Vec<&str> = path_vector + .into_iter() + .filter(|p| Path::new(p).is_executable()) + .collect(); + if exec_paths.is_empty() { + None + } else { + self.get_first_in_vector(exec_paths) + } + } + } + Err(_) => None, + }; + path + } + + fn get_first_in_vector(&self, vector: Vec<&str>) -> Option { + if vector.is_empty() { + return None; + } + let first = vector.first().unwrap().to_string(); + if first.is_empty() { + None + } else { + Some(first) + } + } + fn is_safari(&self) -> bool { self.get_browser_name().contains(SAFARI_NAME) } @@ -324,6 +396,8 @@ pub trait SeleniumManager { // Try to find driver in PATH if !self.is_safari() && !self.is_grid() { + self.get_logger() + .debug(format!("Checking {} in PATH", self.get_driver_name())); (driver_in_path_version, driver_in_path) = self.find_driver_in_path(); if let (Some(version), Some(path)) = (&driver_in_path_version, &driver_in_path) { self.get_logger().debug(format!( @@ -332,6 +406,9 @@ pub trait SeleniumManager { version, path )); + } else { + self.get_logger() + .debug(format!("{} not found in PATH", self.get_driver_name())); } } @@ -349,7 +426,7 @@ pub trait SeleniumManager { err )); } else { - return Err(err.into()); + return Err(err); } } } @@ -396,8 +473,8 @@ pub trait SeleniumManager { fn run_shell_command_with_log(&self, command: String) -> Result> { self.get_logger() - .debug(format!("Running command: {:?}", command)); - let output = run_shell_command(self.get_os(), command)?; + .debug(format!("Running command: {}", command)); + let output = run_shell_command_by_os(self.get_os(), command)?; self.get_logger().debug(format!("Output: {:?}", output)); Ok(output) } @@ -438,6 +515,22 @@ pub trait SeleniumManager { Ok(()) } + fn get_driver_name_with_extension(&self) -> String { + format!( + "{}{}", + self.get_driver_name(), + get_binary_extension(self.get_os()) + ) + } + + fn get_browser_name_with_extension(&self) -> String { + format!( + "{}{}", + self.get_browser_name(), + get_binary_extension(self.get_os()) + ) + } + // ---------------------------------------------------------- // Getters and setters for configuration parameters // ---------------------------------------------------------- @@ -492,26 +585,42 @@ pub trait SeleniumManager { self.get_config().browser_path.as_str() } - fn get_escaped_browser_path(&self) -> String { - let mut browser_path = self.get_browser_path().to_string(); - let path = Path::new(&browser_path); - if path.exists() && WINDOWS.is(self.get_os()) { - browser_path = Path::new(path) + fn set_browser_path(&mut self, browser_path: String) { + if !browser_path.is_empty() { + self.get_config_mut().browser_path = browser_path; + } + } + + fn get_escaped_path(&self, string_path: String) -> String { + let original_path = string_path.clone(); + let mut escaped_path = string_path; + let path = Path::new(&original_path); + if path.exists() { + escaped_path = Path::new(path) .canonicalize() .unwrap() .to_str() .unwrap() - .to_string() - .replace("\\\\?\\", "") - .replace('\\', "\\\\"); - } - browser_path - } - - fn set_browser_path(&mut self, browser_path: String) { - if !browser_path.is_empty() { - self.get_config_mut().browser_path = browser_path; + .to_string(); + if WINDOWS.is(self.get_os()) { + escaped_path = escaped_path.replace("\\\\?\\", "").replace('\\', "\\\\"); + } else { + escaped_path = run_shell_command( + "bash", + "-c", + format_one_arg(ESCAPE_COMMAND, escaped_path.as_str()), + ) + .unwrap_or_default(); + if escaped_path.is_empty() { + escaped_path = original_path.clone(); + } + } } + self.get_logger().trace(format!( + "Original path: {} - Escaped path: {}", + original_path, escaped_path + )); + escaped_path } fn get_proxy(&self) -> &str { @@ -574,6 +683,16 @@ pub trait SeleniumManager { self.get_config_mut().offline = true; } } + + fn is_force_browser_download(&self) -> bool { + self.get_config().force_browser_download + } + + fn set_force_browser_download(&mut self, force_browser_download: bool) { + if force_browser_download { + self.get_config_mut().force_browser_download = true; + } + } } // ---------------------------------------------------------- @@ -650,12 +769,11 @@ pub fn create_http_client(timeout: u64, proxy: &str) -> Result Result> { - let (shell, flag) = if WINDOWS.is(os) { - ("cmd", "/v/c") - } else { - ("sh", "-c") - }; +pub fn run_shell_command( + shell: &str, + flag: &str, + command: String, +) -> Result> { let output = Command::new(shell) .args([flag, command.as_str()]) .output()?; @@ -665,6 +783,15 @@ pub fn run_shell_command(os: &str, command: String) -> Result Result> { + let (shell, flag) = if WINDOWS.is(os) { + ("cmd", "/c") + } else { + ("sh", "-c") + }; + run_shell_command(shell, flag, command) +} + pub fn format_one_arg(string: &str, arg1: &str) -> String { string.replacen("{}", arg1, 1) } diff --git a/rust/src/logger.rs b/rust/src/logger.rs index 8ffb877300481..df19517be7518 100644 --- a/rust/src/logger.rs +++ b/rust/src/logger.rs @@ -29,6 +29,9 @@ use std::io::Write; use std::ops::Deref; use Color::{Blue, Cyan, Green, Red, Yellow}; +pub const DRIVER_PATH: &str = "Driver path: "; +pub const BROWSER_PATH: &str = "Browser path: "; + #[derive(Default)] enum OutputType { #[default] @@ -56,6 +59,8 @@ pub struct Logs { pub struct Result { pub code: i32, pub message: String, + pub driver_path: String, + pub browser_path: String, } #[derive(Default, Serialize, Deserialize)] @@ -125,6 +130,8 @@ impl Logger { result: Result { code: 0, message: "".to_string(), + driver_path: "".to_string(), + browser_path: "".to_string(), }, }), } @@ -163,14 +170,23 @@ impl Logger { .push(self.create_json_log(message.to_string(), level)); } if level == Level::Info || level <= Level::Error { - self.json.borrow_mut().result.message = message; + if message.starts_with(DRIVER_PATH) { + let driver_path = message.replace(DRIVER_PATH, ""); + self.json.borrow_mut().result.driver_path = driver_path.to_owned(); + self.json.borrow_mut().result.message = driver_path; + } else if message.starts_with(BROWSER_PATH) { + let browser_path = message.replace(BROWSER_PATH, ""); + self.json.borrow_mut().result.browser_path = browser_path; + } else { + self.json.borrow_mut().result.message = message; + } } } OutputType::Shell => { if level == Level::Info { - print!("{}", message); + println!("{}", message); } else if level == Level::Error { - eprint!("{}", message); + eprintln!("{}", message); } } _ => { diff --git a/rust/src/main.rs b/rust/src/main.rs index a51496dd162ac..fd47523e3a01b 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -20,12 +20,11 @@ use std::process::exit; use clap::Parser; use exitcode::DATAERR; -use exitcode::UNAVAILABLE; - use exitcode::OK; +use exitcode::UNAVAILABLE; use selenium_manager::config::BooleanKey; use selenium_manager::grid::GridManager; -use selenium_manager::logger::Logger; +use selenium_manager::logger::{Logger, BROWSER_PATH, DRIVER_PATH}; use selenium_manager::REQUEST_TIMEOUT_SEC; use selenium_manager::TTL_BROWSERS_SEC; use selenium_manager::TTL_DRIVERS_SEC; @@ -64,7 +63,7 @@ struct Cli { browser_version: Option, /// Browser path (absolute) for browser version detection (e.g., /usr/bin/google-chrome, - /// "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome", + /// "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", /// "C:\Program Files\Google\Chrome\Application\chrome.exe") #[clap(long, value_parser)] browser_path: Option, @@ -108,6 +107,10 @@ struct Cli { /// Offline mode (i.e., disabling network requests and downloads) #[clap(long)] offline: bool, + + /// Force to download browser. Currently Chrome for Testing (CfT) is supported + #[clap(long)] + force_browser_download: bool, } fn main() { @@ -155,20 +158,25 @@ fn main() { selenium_manager.set_driver_ttl(cli.driver_ttl); selenium_manager.set_browser_ttl(cli.browser_ttl); selenium_manager.set_offline(cli.offline); + selenium_manager.set_force_browser_download(cli.force_browser_download); selenium_manager .set_timeout(cli.timeout) .and_then(|_| selenium_manager.set_proxy(cli.proxy.unwrap_or_default())) .and_then(|_| selenium_manager.resolve_driver()) - .map(|path| { + .map(|driver_path| { let log = selenium_manager.get_logger(); - if path.exists() { - log.info(path.display()); - flush_and_exit(OK, log); + if driver_path.exists() { + log.info(format!("{}{}", DRIVER_PATH, driver_path.display())); } else { - log.error("Driver unavailable in the cache".to_string()); + log.error(format!("Driver unavailable: {}", DRIVER_PATH)); flush_and_exit(UNAVAILABLE, log); } + let browser_path = selenium_manager.get_browser_path(); + if !browser_path.is_empty() { + log.info(format!("{}{}", BROWSER_PATH, browser_path)); + } + flush_and_exit(OK, log); }) .unwrap_or_else(|err| { let log = selenium_manager.get_logger(); diff --git a/rust/src/safari.rs b/rust/src/safari.rs index 1ef13264381a0..7620af0c0588b 100644 --- a/rust/src/safari.rs +++ b/rust/src/safari.rs @@ -22,7 +22,7 @@ use std::error::Error; use std::path::PathBuf; use std::string::ToString; -use crate::files::BrowserPath; +use crate::files::{path_buf_to_string, BrowserPath}; use crate::config::OS::MACOS; use crate::{create_http_client, format_one_arg, Logger, SeleniumManager, PLIST_COMMAND, STABLE}; @@ -75,19 +75,18 @@ impl SeleniumManager for SafariManager { )]) } - fn discover_browser_version(&self) -> Option { - let escaped_browser_path = self.get_escaped_browser_path(); - let mut browser_path = escaped_browser_path.as_str(); + fn discover_browser_version(&mut self) -> Option { + let mut browser_path = self.get_browser_path().to_string(); if browser_path.is_empty() { match self.detect_browser_path() { Some(path) => { - browser_path = path; + browser_path = self.get_escaped_path(path_buf_to_string(path)); } _ => return None, } } let command = if MACOS.is(self.get_os()) { - vec![format_one_arg(PLIST_COMMAND, browser_path)] + vec![format_one_arg(PLIST_COMMAND, &browser_path)] } else { return None; }; @@ -129,4 +128,8 @@ impl SeleniumManager for SafariManager { fn set_logger(&mut self, log: Logger) { self.log = log; } + + fn download_browser(&mut self) -> Result, Box> { + Ok(None) + } } diff --git a/rust/src/safaritp.rs b/rust/src/safaritp.rs index 6a4b40eea17cb..3b118a5993b07 100644 --- a/rust/src/safaritp.rs +++ b/rust/src/safaritp.rs @@ -22,7 +22,7 @@ use std::error::Error; use std::path::PathBuf; use std::string::ToString; -use crate::files::BrowserPath; +use crate::files::{path_buf_to_string, BrowserPath}; use crate::config::OS::MACOS; use crate::{create_http_client, format_one_arg, Logger, SeleniumManager, PLIST_COMMAND, STABLE}; @@ -80,19 +80,18 @@ impl SeleniumManager for SafariTPManager { )]) } - fn discover_browser_version(&self) -> Option { - let escaped_browser_path = self.get_escaped_browser_path(); - let mut browser_path = escaped_browser_path.as_str(); + fn discover_browser_version(&mut self) -> Option { + let mut browser_path = self.get_browser_path().to_string(); if browser_path.is_empty() { match self.detect_browser_path() { Some(path) => { - browser_path = path; + browser_path = self.get_escaped_path(path_buf_to_string(path)); } _ => return None, } } let command = if MACOS.is(self.get_os()) { - vec![format_one_arg(PLIST_COMMAND, browser_path)] + vec![format_one_arg(PLIST_COMMAND, &browser_path)] } else { return None; }; @@ -134,4 +133,8 @@ impl SeleniumManager for SafariTPManager { fn set_logger(&mut self, log: Logger) { self.log = log; } + + fn download_browser(&mut self) -> Result, Box> { + Ok(None) + } } diff --git a/rust/tests/chrome_download_tests.rs b/rust/tests/chrome_download_tests.rs new file mode 100644 index 0000000000000..3355f422d46da --- /dev/null +++ b/rust/tests/chrome_download_tests.rs @@ -0,0 +1,51 @@ +// 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 assert_cmd::Command; +use std::path::Path; + +use is_executable::is_executable; +use selenium_manager::logger::JsonOutput; +use std::str; + +#[test] +fn chrome_download_test() { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_selenium-manager")); + cmd.args([ + "--browser", + "chrome", + "--force-browser-download", + "--output", + "json", + ]) + .assert() + .success() + .code(0); + + let stdout = &cmd.unwrap().stdout; + let output = str::from_utf8(stdout).unwrap(); + println!("{}", output); + + let json: JsonOutput = serde_json::from_str(output).unwrap(); + let driver_path = Path::new(&json.result.driver_path); + assert!(driver_path.exists()); + assert!(is_executable(driver_path)); + + let browser_path = Path::new(&json.result.browser_path); + assert!(browser_path.exists()); + assert!(is_executable(browser_path)); +} diff --git a/rust/tests/cli_tests.rs b/rust/tests/cli_tests.rs index 26307db8c689e..a0d1385deecec 100644 --- a/rust/tests/cli_tests.rs +++ b/rust/tests/cli_tests.rs @@ -64,7 +64,6 @@ fn ok_test( if !browser_version.is_empty() && output.contains("cache") { assert!(output.contains(&driver_version)); } - assert!(!output.contains("Trying with latest driver version")); } #[rstest] @@ -148,6 +147,11 @@ fn beta_test(#[case] browser: String, #[case] driver_name: String) { "chrome", r#"/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"# )] +#[case( + "macos", + "chrome", + r#"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"# +)] fn path_test(#[case] os: String, #[case] browser: String, #[case] browser_path: String) { println!( "Path test browser={} -- browser_path={}", @@ -163,7 +167,7 @@ fn path_test(#[case] os: String, #[case] browser: String, #[case] browser_path: if OS.eq(&os) { let stdout = &cmd.unwrap().stdout; let output = str::from_utf8(stdout).unwrap(); - println!("output {:?}", output); + println!("{}", output); assert!(!output.contains("WARN")); } } diff --git a/rust/tests/output_tests.rs b/rust/tests/output_tests.rs index 5e0b847dc0ce2..762fccd3abd38 100644 --- a/rust/tests/output_tests.rs +++ b/rust/tests/output_tests.rs @@ -18,7 +18,7 @@ use assert_cmd::Command; use std::path::Path; -use selenium_manager::logger::JsonOutput; +use selenium_manager::logger::{JsonOutput, DRIVER_PATH}; use std::str; #[test] @@ -54,7 +54,5 @@ fn shell_output_test() { let stdout = &cmd.unwrap().stdout; let output = str::from_utf8(stdout).unwrap(); println!("{}", output); - - let driver = Path::new(output); - assert!(driver.exists()); + assert!(output.contains(DRIVER_PATH)); }