wasm_pack/test/webdriver/
chromedriver.rs

1use super::get_and_notify;
2use crate::install::InstallMode;
3use crate::stamps;
4use crate::target;
5use anyhow::{bail, Context, Result};
6use binary_install::Cache;
7use chrono::DateTime;
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11// Keep it up to date with each `wasm-pack` release.
12// https://chromedriver.storage.googleapis.com/LATEST_RELEASE
13const DEFAULT_CHROMEDRIVER_VERSION: &str = "114.0.5735.90";
14
15const CHROMEDRIVER_LAST_UPDATED_STAMP: &str = "chromedriver_last_updated";
16const CHROMEDRIVER_VERSION_STAMP: &str = "chromedriver_version";
17
18/// Get the path to an existing `chromedriver`, or install it if no existing
19/// binary is found or if there is a new binary version.
20pub fn get_or_install_chromedriver(cache: &Cache, mode: InstallMode) -> Result<PathBuf> {
21    if let Ok(path) = which::which("chromedriver") {
22        return Ok(path);
23    }
24    install_chromedriver(cache, mode.install_permitted())
25}
26
27/// Download and install a pre-built `chromedriver` binary.
28pub fn install_chromedriver(cache: &Cache, installation_allowed: bool) -> Result<PathBuf> {
29    let target = if target::LINUX && target::x86_64 {
30        "linux64"
31    } else if target::MACOS && target::x86_64 {
32        "mac-x64"
33    } else if target::MACOS && target::aarch64 {
34        "mac-arm64"
35    } else if target::WINDOWS {
36        "win32"
37    } else {
38        bail!("chromedriver binaries are unavailable for this target")
39    };
40
41    let url = get_chromedriver_url(target);
42
43    match get_and_notify(cache, installation_allowed, "chromedriver", &url)? {
44        Some(path) => Ok(path),
45        None => bail!(
46            "No cached `chromedriver` binary found, and could not find a global \
47             `chromedriver` on the `$PATH`. Not installing `chromedriver` because of noinstall \
48             mode."
49        ),
50    }
51}
52
53/// Get `chromedriver` download URL.
54///
55/// _Algorithm_:
56/// 1. Try to open `*.stamps` file and deserialize its content to JSON object.
57/// 2. Try to compare current time with the saved one.
58/// 3. If the saved time is older than 1 day or something failed
59///    => fetch a new version and save version & time.
60/// 4. If everything failed, use the default version.
61/// 5. Return URL.
62///
63/// _Notes:_
64///
65/// It returns the latest one without checking the installed `Chrome` version
66/// because it's not easy to find out `Chrome` version on `Windows` -
67/// https://bugs.chromium.org/p/chromium/issues/detail?id=158372
68///
69/// The official algorithm for `chromedriver` version selection:
70/// https://chromedriver.chromium.org/downloads/version-selection
71fn get_chromedriver_url(target: &str) -> String {
72    let fetch_and_save_version =
73        || fetch_chromedriver_version().and_then(save_chromedriver_version);
74
75    let chromedriver_version = match stamps::read_stamps_file_to_json() {
76        Ok(json) => {
77            if should_load_chromedriver_version_from_stamp(&json) {
78                stamps::get_stamp_value(CHROMEDRIVER_VERSION_STAMP, &json)
79            } else {
80                fetch_and_save_version()
81            }
82        }
83        Err(_) => fetch_and_save_version(),
84    }
85    .unwrap_or_else(|error| {
86        log::warn!(
87            "Cannot load or fetch chromedriver's latest version data, \
88             the default version {} will be used. Error: {}",
89            DEFAULT_CHROMEDRIVER_VERSION,
90            error
91        );
92        DEFAULT_CHROMEDRIVER_VERSION.to_owned()
93    });
94    assemble_chromedriver_url(&chromedriver_version, target)
95}
96
97// ------ `get_chromedriver_url` helpers ------
98
99fn save_chromedriver_version(version: String) -> Result<String> {
100    stamps::save_stamp_value(CHROMEDRIVER_VERSION_STAMP, &version)?;
101
102    let current_time = chrono::offset::Local::now().to_rfc3339();
103    stamps::save_stamp_value(CHROMEDRIVER_LAST_UPDATED_STAMP, current_time)?;
104
105    Ok(version)
106}
107
108fn should_load_chromedriver_version_from_stamp(json: &serde_json::Value) -> bool {
109    let last_updated = stamps::get_stamp_value(CHROMEDRIVER_LAST_UPDATED_STAMP, json)
110        .ok()
111        .and_then(|last_updated| DateTime::parse_from_rfc3339(&last_updated).ok());
112
113    match last_updated {
114        None => false,
115        Some(last_updated) => {
116            let current_time = chrono::offset::Local::now();
117            current_time.signed_duration_since(last_updated).num_hours() < 24
118        }
119    }
120}
121
122/// Channel information from the chromedriver version endpoint.
123#[derive(Deserialize)]
124struct ChannelInfo {
125    version: String,
126}
127
128/// The response from the chromedriver version endpoint.
129#[derive(Deserialize)]
130struct GoodLatestVersions {
131    channels: HashMap<String, ChannelInfo>,
132}
133
134/// Retrieve the latest version of chromedriver from the json endpoints.
135/// See: <https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints>
136fn fetch_chromedriver_version() -> Result<String> {
137    let info: GoodLatestVersions = ureq::builder()
138        .try_proxy_from_env(true)
139        .build()
140        .get("https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json")
141        .call()
142        .context("fetching of chromedriver's LATEST_RELEASE failed")?
143        .into_json()
144        .context("converting chromedriver version response to GoodLatestVersions failed")?;
145
146    let version = info
147        .channels
148        .get("Stable")
149        .ok_or_else(|| anyhow::anyhow!("no Stable channel found in chromedriver version response"))?
150        .version
151        .clone();
152
153    println!("chromedriver version: {}", version);
154
155    Ok(version)
156}
157
158fn assemble_chromedriver_url(chromedriver_version: &str, target: &str) -> String {
159    format!(
160        "https://storage.googleapis.com/chrome-for-testing-public/{version}/{target}/chromedriver-{target}.zip",
161        version = chromedriver_version,
162        target = target,
163    )
164}