From b2748ff4e06077b49e16f9593f29e057210b464f Mon Sep 17 00:00:00 2001 From: Boni Garcia Date: Mon, 19 May 2025 15:57:22 +0200 Subject: [PATCH] [rust] Electron support in Selenium-Manager (#13954) --- rust/src/electron.rs | 275 +++++++++++++++++++++++++++++++++++ rust/src/firefox.rs | 4 +- rust/src/lib.rs | 14 +- rust/src/main.rs | 2 +- rust/tests/electron_tests.rs | 39 +++++ 5 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 rust/src/electron.rs create mode 100644 rust/tests/electron_tests.rs diff --git a/rust/src/electron.rs b/rust/src/electron.rs new file mode 100644 index 0000000000000..868e087111b54 --- /dev/null +++ b/rust/src/electron.rs @@ -0,0 +1,275 @@ +// 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::CHROMEDRIVER_NAME; +use crate::config::ManagerConfig; +use crate::config::ARCH::{ARM64, X32}; +use crate::config::OS::MACOS; +use crate::downloads::read_redirect_from_link; +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::{ + create_http_client, Logger, SeleniumManager, LATEST_RELEASE, OFFLINE_REQUEST_ERR_MSG, WINDOWS, +}; +use anyhow::Error; +use reqwest::Client; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, Sender}; + +pub const ELECTRON_NAME: &str = "electron"; +const DRIVER_URL: &str = "https://github.com/electron/electron/releases/"; + +pub struct ElectronManager { + pub browser_name: &'static str, + pub driver_name: &'static str, + pub config: ManagerConfig, + pub http_client: Client, + pub log: Logger, + pub tx: Sender, + pub rx: Receiver, + pub download_browser: bool, + pub driver_url: Option, +} + +impl ElectronManager { + pub fn new() -> Result, Error> { + let browser_name = ELECTRON_NAME; + let driver_name = CHROMEDRIVER_NAME; + let config = ManagerConfig::default(browser_name, driver_name); + let default_timeout = config.timeout.to_owned(); + let default_proxy = &config.proxy; + let (tx, rx): (Sender, Receiver) = mpsc::channel(); + Ok(Box::new(ElectronManager { + browser_name, + driver_name, + http_client: create_http_client(default_timeout, default_proxy)?, + config, + log: Logger::new(), + tx, + rx, + download_browser: false, + driver_url: None, + })) + } +} + +impl SeleniumManager for ElectronManager { + fn get_browser_name(&self) -> &str { + self.browser_name + } + + fn get_browser_names_in_path(&self) -> Vec<&str> { + vec![self.get_browser_name()] + } + + fn get_http_client(&self) -> &Client { + &self.http_client + } + + fn set_http_client(&mut self, http_client: Client) { + self.http_client = http_client; + } + + fn get_browser_path_map(&self) -> HashMap { + HashMap::new() + } + + fn discover_browser_version(&mut self) -> Result, Error> { + Ok(None) + } + + fn get_driver_name(&self) -> &str { + self.driver_name + } + + fn request_driver_version(&mut self) -> Result { + let major_browser_version_binding = self.get_major_browser_version(); + let major_browser_version = major_browser_version_binding.as_str(); + let cache_path = self.get_cache_path()?; + let mut metadata = get_metadata(self.get_logger(), &cache_path); + + match get_driver_version_from_metadata( + &metadata.drivers, + self.driver_name, + major_browser_version, + ) { + Some(driver_version) => { + self.log.trace(format!( + "Driver TTL is valid. Getting {} version from metadata", + &self.driver_name + )); + Ok(driver_version) + } + _ => { + self.assert_online_or_err(OFFLINE_REQUEST_ERR_MSG)?; + + let latest_url = format!( + "{}{}", + self.get_driver_mirror_url_or_default(DRIVER_URL), + LATEST_RELEASE + ); + let driver_version = + read_redirect_from_link(self.get_http_client(), latest_url, self.get_logger())?; + let driver_ttl = self.get_ttl(); + if driver_ttl > 0 { + metadata.drivers.push(create_driver_metadata( + major_browser_version, + self.driver_name, + &driver_version, + driver_ttl, + )); + write_metadata(&metadata, self.get_logger(), cache_path); + } + Ok(driver_version) + } + } + } + + fn request_browser_version(&mut self) -> Result, Error> { + Ok(None) + } + + fn get_driver_url(&mut self) -> Result { + if self.driver_url.is_some() { + return Ok(self.driver_url.as_ref().unwrap().to_string()); + } + + Ok(format!( + "{}download/v{}/{}-v{}-{}.zip", + self.get_driver_mirror_url_or_default(DRIVER_URL), + self.get_driver_version(), + CHROMEDRIVER_NAME, + self.get_driver_version(), + self.get_platform_label() + )) + } + + fn get_driver_path_in_cache(&self) -> Result { + Ok(compose_driver_path_in_cache( + self.get_cache_path()?.unwrap_or_default(), + self.driver_name, + self.get_os(), + self.get_platform_label(), + self.get_driver_version(), + )) + } + + fn get_config(&self) -> &ManagerConfig { + &self.config + } + + fn get_config_mut(&mut self) -> &mut ManagerConfig { + &mut self.config + } + + fn set_config(&mut self, config: ManagerConfig) { + self.config = config; + } + + fn get_logger(&self) -> &Logger { + &self.log + } + + fn set_logger(&mut self, log: Logger) { + self.log = log; + } + + fn get_sender(&self) -> &Sender { + &self.tx + } + + fn get_receiver(&self) -> &Receiver { + &self.rx + } + + fn get_platform_label(&self) -> &str { + let os = self.get_os(); + let arch = self.get_arch(); + if WINDOWS.is(os) { + if X32.is(arch) { + "win32-ia32" + } else if ARM64.is(arch) { + "win32-arm64-x64" + } else { + "win32-x64" + } + } else if MACOS.is(os) { + if ARM64.is(arch) { + "mas-arm64" + } else { + "mas-x64" + } + } else if ARM64.is(arch) { + "linux-arm64" + } else { + "linux-x64" + } + } + + fn request_latest_browser_version_from_online( + &mut self, + _browser_version: &str, + ) -> Result { + self.unavailable_download() + } + + fn request_fixed_browser_version_from_online( + &mut self, + _browser_version: &str, + ) -> Result { + self.unavailable_download() + } + + fn get_min_browser_version_for_download(&self) -> Result { + self.unavailable_download() + } + + fn get_browser_binary_path(&mut self, _browser_version: &str) -> Result { + self.unavailable_download() + } + + fn get_browser_url_for_download(&mut self, _browser_version: &str) -> Result { + self.unavailable_download() + } + + fn get_browser_label_for_download( + &self, + _browser_version: &str, + ) -> Result, Error> { + self.unavailable_download() + } + + fn is_download_browser(&self) -> bool { + self.download_browser + } + + fn set_download_browser(&mut self, download_browser: bool) { + self.download_browser = download_browser; + } + + fn is_snap(&self, _browser_path: &str) -> bool { + false + } + + fn get_snap_path(&self) -> Option { + None + } +} diff --git a/rust/src/firefox.rs b/rust/src/firefox.rs index 75ca6b41453d0..17b95933f5ba5 100644 --- a/rust/src/firefox.rs +++ b/rust/src/firefox.rs @@ -25,7 +25,8 @@ use crate::metadata::{ }; use crate::{ create_http_client, format_three_args, format_two_args, Logger, SeleniumManager, BETA, - DASH_VERSION, DEV, ESR, NIGHTLY, OFFLINE_REQUEST_ERR_MSG, REG_CURRENT_VERSION_ARG, STABLE, + DASH_VERSION, DEV, ESR, LATEST_RELEASE, NIGHTLY, OFFLINE_REQUEST_ERR_MSG, + REG_CURRENT_VERSION_ARG, STABLE, }; use anyhow::anyhow; use anyhow::Error; @@ -41,7 +42,6 @@ use std::sync::mpsc::{Receiver, Sender}; pub const FIREFOX_NAME: &str = "firefox"; pub const GECKODRIVER_NAME: &str = "geckodriver"; const DRIVER_URL: &str = "https://github.com/mozilla/geckodriver/releases/"; -const LATEST_RELEASE: &str = "latest"; const DRIVER_VERSIONS_URL: &str = "https://raw.githubusercontent.com/SeleniumHQ/selenium/trunk/common/geckodriver/geckodriver-support.json"; const BROWSER_URL: &str = "https://ftp.mozilla.org/pub/firefox/releases/"; const FIREFOX_DEFAULT_LANG: &str = "en-US"; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 318b0ca0e96af..28a4351df87a4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -20,6 +20,7 @@ use crate::config::OS::{MACOS, WINDOWS}; use crate::config::{str_to_os, ManagerConfig}; use crate::downloads::download_to_tmp_folder; use crate::edge::{EdgeManager, EDGEDRIVER_NAME, EDGE_NAMES, WEBVIEW2_NAME}; +use crate::electron::{ElectronManager, ELECTRON_NAME}; use crate::files::get_win_file_version; use crate::files::{ capitalize, collect_files_from_cache, create_path_if_not_exists, default_cache_folder, @@ -56,6 +57,7 @@ pub mod chrome; pub mod config; pub mod downloads; pub mod edge; +pub mod electron; pub mod files; pub mod firefox; pub mod grid; @@ -112,6 +114,7 @@ pub const NOT_ADMIN_FOR_EDGE_INSTALLER_ERR_MSG: &str = pub const ONLINE_DISCOVERY_ERROR_MESSAGE: &str = "Unable to discover {}{} in online repository"; pub const UNC_PREFIX: &str = r"\\?\"; pub const SM_BETA_LABEL: &str = "0."; +pub const LATEST_RELEASE: &str = "latest"; pub trait SeleniumManager { // ---------------------------------------------------------- @@ -473,7 +476,7 @@ pub trait SeleniumManager { fn discover_local_browser(&mut self) -> Result<(), Error> { let mut download_browser = self.is_force_browser_download(); - if !download_browser { + if !download_browser && !self.is_electron() { let major_browser_version = self.get_major_browser_version(); match self.discover_browser_version()? { Some(discovered_version) => { @@ -563,6 +566,7 @@ pub trait SeleniumManager { && !self.is_grid() && !self.is_safari() && !self.is_webview2() + && !self.is_electron() { let browser_path = self.download_browser(original_browser_version)?; if browser_path.is_some() { @@ -691,6 +695,10 @@ pub trait SeleniumManager { self.get_browser_name().eq(GRID_NAME) } + fn is_electron(&self) -> bool { + self.get_browser_name().eq_ignore_ascii_case(ELECTRON_NAME) + } + fn is_firefox(&self) -> bool { self.get_browser_name().contains(FIREFOX_NAME) } @@ -778,7 +786,7 @@ pub trait SeleniumManager { let original_browser_version = self.get_config().browser_version.clone(); // Try to find driver in PATH - if !self.is_safari() && !self.is_grid() { + if !self.is_safari() && !self.is_grid() && !self.is_electron() { self.get_logger() .trace(format!("Checking {} in PATH", self.get_driver_name())); (driver_in_path_version, driver_in_path) = self.find_driver_in_path(); @@ -1619,6 +1627,8 @@ pub fn get_manager_by_browser(browser_name: String) -> Result, diff --git a/rust/tests/electron_tests.rs b/rust/tests/electron_tests.rs new file mode 100644 index 0000000000000..c0e6e48cf92e0 --- /dev/null +++ b/rust/tests/electron_tests.rs @@ -0,0 +1,39 @@ +// 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::common::get_selenium_manager; + +use rstest::rstest; + +mod common; + +#[test] +fn electron_latest_test() { + let mut cmd = get_selenium_manager(); + let cmd_assert = cmd.args(["--browser", "electron"]).assert(); + cmd_assert.success(); +} + +#[rstest] +#[case("36.2.1")] +fn electron_version_test(#[case] driver_version: String) { + let mut cmd = get_selenium_manager(); + let cmd_assert = cmd + .args(["--browser", "electron", "--driver-version", &driver_version]) + .assert(); + cmd_assert.success(); +}