blob: 539c9bca7eaa864114591908aa42f3132e291351 [file] [edit]
// 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 anyhow::Error;
use anyhow::anyhow;
use clap::{Parser, Subcommand};
use exitcode::{DATAERR, OK};
use selenium_manager::config::{BooleanKey, CACHE_PATH_KEY, StringKey};
use selenium_manager::grid::GridManager;
use selenium_manager::jre::{JavaRuntime, ensure_jre};
use selenium_manager::logger::Logger;
use selenium_manager::metadata::clear_metadata;
use selenium_manager::{
REQUEST_TIMEOUT_SEC, SM_BETA_LABEL, SeleniumManager, TTL_SEC, clear_cache,
get_manager_by_browser, get_manager_by_driver,
};
use serde::Serialize;
use std::process::exit;
#[derive(Parser, Debug)]
#[command(
name = "sel",
version,
about = "Selenium CLI for browsers and grid installation"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Install browsers, drivers, and Selenium Grid artifacts
Install(InstallArgs),
/// Print sel version
Version,
}
#[derive(Parser, Debug)]
struct InstallArgs {
/// Browser name (repeat to install multiple)
#[arg(long, value_parser)]
browser: Vec<String>,
/// Driver name (repeat to install multiple)
#[arg(long, value_parser)]
driver: Vec<String>,
/// Selenium Grid. If version is omitted, latest is downloaded
#[arg(long, value_parser, num_args = 0..=1, default_missing_value = "", value_name = "GRID_VERSION")]
grid: Option<String>,
/// Browser version requested for browser install
#[arg(long, value_parser)]
browser_version: Option<String>,
/// Driver version requested for driver install
#[arg(long, value_parser)]
driver_version: Option<String>,
/// Output machine-readable summary as JSON
#[arg(long)]
json: bool,
/// Output type passed to internal manager logs
#[arg(long, value_parser)]
output: Option<String>,
/// Operating system (windows, linux, or macos)
#[arg(long, value_parser)]
os: Option<String>,
/// System architecture (x32, x64, or arm64)
#[arg(long, value_parser)]
arch: Option<String>,
/// HTTP proxy for network connections
#[arg(long, value_parser)]
proxy: Option<String>,
/// Timeout for network requests in seconds
#[arg(long, value_parser, default_value_t = REQUEST_TIMEOUT_SEC)]
timeout: u64,
/// TTL for discovered browser/driver versions
#[arg(long, value_parser, default_value_t = TTL_SEC)]
ttl: u64,
/// Local cache path for downloaded assets
#[arg(long, value_parser)]
cache_path: Option<String>,
/// Clear cache folder
#[arg(long)]
clear_cache: bool,
/// Clear metadata file
#[arg(long)]
clear_metadata: bool,
/// Display DEBUG messages
#[arg(long)]
debug: bool,
/// Display TRACE messages
#[arg(long)]
trace: bool,
/// Level for output messages (info, debug, trace, warn, error)
#[arg(long)]
log_level: Option<String>,
/// Disable network requests and downloads
#[arg(long)]
offline: bool,
/// Avoid sending usage statistics
#[arg(long)]
avoid_stats: bool,
/// Force browser download even when a browser exists in PATH
#[arg(long)]
force_browser_download: bool,
/// Avoid browser download even when browser-version is provided
#[arg(long)]
avoid_browser_download: bool,
/// Avoid drivers found in PATH
#[arg(long)]
skip_driver_in_path: bool,
/// Avoid browsers found in PATH
#[arg(long)]
skip_browser_in_path: bool,
/// Browser path override for version discovery
#[arg(long, value_parser)]
browser_path: Option<String>,
/// Mirror for driver repositories
#[arg(long, value_parser)]
driver_mirror_url: Option<String>,
/// Mirror for browser repositories
#[arg(long, value_parser)]
browser_mirror_url: Option<String>,
/// Selenium language binding invoking the install command
#[arg(long, value_parser)]
language_binding: Option<String>,
}
#[derive(Debug, Serialize)]
struct InstallArtifact {
name: String,
version: String,
path: String,
}
#[derive(Debug, Serialize, Default)]
struct InstallSummary {
browsers: Vec<InstallArtifact>,
drivers: Vec<InstallArtifact>,
grid: Option<InstallArtifact>,
jre: Option<InstallArtifact>,
}
fn main() {
let cli = Cli::parse();
let code = match cli.command {
Commands::Version => {
println!("{}", clap::crate_version!());
OK
}
Commands::Install(args) => match run_install(args) {
Ok(()) => OK,
Err(err) => {
eprintln!("{}", err);
DATAERR
}
},
};
exit(code);
}
fn run_install(args: InstallArgs) -> Result<(), Error> {
if args.browser.is_empty() && args.driver.is_empty() && args.grid.is_none() {
return Err(anyhow!(
"No install target provided. Use --browser, --driver, and/or --grid"
));
}
let cache_path = StringKey(
vec![CACHE_PATH_KEY],
&args.cache_path.clone().unwrap_or_default(),
)
.get_value();
let output = args
.output
.clone()
.unwrap_or_else(|| if args.json { "MIXED" } else { "LOGGER" }.to_string());
let debug = args.debug || BooleanKey("debug", false).get_value();
let trace = args.trace || BooleanKey("trace", false).get_value();
let log_level = StringKey(
vec!["log-level"],
&args.log_level.clone().unwrap_or_default(),
)
.get_value();
let log = Logger::create(&output, debug, trace, &log_level);
if args.clear_cache || BooleanKey("clear-cache", false).get_value() {
clear_cache(&log, &cache_path);
}
if args.clear_metadata || BooleanKey("clear-metadata", false).get_value() {
clear_metadata(&log, &cache_path);
}
let mut summary = InstallSummary::default();
for browser in &args.browser {
let mut manager = get_manager_by_browser(browser.to_string())?;
setup_manager(
manager.as_mut(),
&args,
&cache_path,
&output,
debug,
trace,
&log_level,
)?;
let driver_path = manager.setup()?;
let browser_path = manager.get_browser_path_or_latest_from_cache();
summary.browsers.push(InstallArtifact {
name: browser.to_string(),
version: manager.get_browser_version().to_string(),
path: browser_path,
});
summary.drivers.push(InstallArtifact {
name: manager.get_driver_name().to_string(),
version: manager.get_driver_version().to_string(),
path: driver_path.display().to_string(),
});
}
for driver in &args.driver {
let mut manager = get_manager_by_driver(driver.to_string())?;
setup_manager(
manager.as_mut(),
&args,
&cache_path,
&output,
debug,
trace,
&log_level,
)?;
let driver_path = manager.setup()?;
summary.drivers.push(InstallArtifact {
name: driver.to_string(),
version: manager.get_driver_version().to_string(),
path: driver_path.display().to_string(),
});
}
if let Some(grid_version) = &args.grid {
let mut manager = GridManager::new(grid_version.to_string())?;
setup_manager(
manager.as_mut(),
&args,
&cache_path,
&output,
debug,
trace,
&log_level,
)?;
let grid_path = manager.setup()?;
summary.grid = Some(InstallArtifact {
name: "selenium-server".to_string(),
version: manager.get_driver_version().to_string(),
path: grid_path.display().to_string(),
});
let runtime = ensure_jre(
Some(cache_path.as_str()),
args.timeout,
args.proxy.as_deref(),
&log,
)?;
summary.jre = Some(java_runtime_to_artifact(runtime));
}
if args.json {
println!("{}", serde_json::to_string_pretty(&summary)?);
} else {
print_human_summary(&summary);
}
Ok(())
}
fn setup_manager(
manager: &mut dyn SeleniumManager,
args: &InstallArgs,
cache_path: &str,
output: &str,
debug: bool,
trace: bool,
log_level: &str,
) -> Result<(), Error> {
let logger = Logger::create(output, debug, trace, log_level);
manager.set_logger(logger);
manager.set_browser_version(args.browser_version.clone().unwrap_or_default());
manager.set_driver_version(args.driver_version.clone().unwrap_or_default());
manager.set_browser_path(args.browser_path.clone().unwrap_or_default());
manager.set_driver_mirror_url(args.driver_mirror_url.clone().unwrap_or_default());
manager.set_browser_mirror_url(args.browser_mirror_url.clone().unwrap_or_default());
manager.set_os(args.os.clone().unwrap_or_default());
manager.set_arch(args.arch.clone().unwrap_or_default());
manager.set_ttl(args.ttl);
manager.set_force_browser_download(args.force_browser_download);
manager.set_avoid_browser_download(args.avoid_browser_download);
manager.set_cache_path(cache_path.to_string());
manager.set_offline(args.offline);
manager.set_language_binding(args.language_binding.clone().unwrap_or_default());
manager.set_avoid_stats(args.avoid_stats);
manager.set_skip_driver_in_path(args.skip_driver_in_path);
manager.set_skip_browser_in_path(args.skip_browser_in_path);
let sm_version = clap::crate_version!();
let selenium_version = sm_version.strip_prefix(SM_BETA_LABEL).unwrap_or(sm_version);
manager.set_selenium_version(selenium_version.to_string());
manager
.set_timeout(args.timeout)
.and_then(|_| manager.set_proxy(args.proxy.clone().unwrap_or_default()))
.and_then(|_| manager.stats())
}
fn java_runtime_to_artifact(runtime: JavaRuntime) -> InstallArtifact {
InstallArtifact {
name: runtime.source,
version: runtime.version,
path: runtime.java_path.display().to_string(),
}
}
fn print_human_summary(summary: &InstallSummary) {
for browser in &summary.browsers {
println!(
"Browser: {} {} -> {}",
browser.name, browser.version, browser.path
);
}
for driver in &summary.drivers {
println!(
"Driver: {} {} -> {}",
driver.name, driver.version, driver.path
);
}
if let Some(grid) = &summary.grid {
println!("Grid: {} {} -> {}", grid.name, grid.version, grid.path);
}
if let Some(jre) = &summary.jre {
println!("Java: {} {} -> {}", jre.name, jre.version, jre.path);
}
}