Skip to content

Commit c3b226c

Browse files
authored
[rust] Use CfT endpoints to discover chromedriver 115+ (#12208)
1 parent 265e2f4 commit c3b226c

11 files changed

Lines changed: 289 additions & 108 deletions

File tree

rust/src/chrome.rs

Lines changed: 234 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,41 @@
1717

1818
use crate::config::ManagerConfig;
1919
use reqwest::Client;
20+
use serde::{Deserialize, Serialize};
2021
use std::collections::HashMap;
2122
use std::error::Error;
23+
use std::option::Option;
2224
use std::path::PathBuf;
2325

24-
use crate::config::ARCH::ARM64;
26+
use crate::config::ARCH::{ARM64, X32};
2527
use crate::config::OS::{LINUX, MACOS, WINDOWS};
26-
use crate::downloads::read_version_from_link;
27-
use crate::files::{compose_driver_path_in_cache, BrowserPath, PARSE_ERROR};
28+
use crate::downloads::{parse_json_from_url, read_version_from_link};
29+
use crate::files::{compose_driver_path_in_cache, BrowserPath};
2830
use crate::logger::Logger;
2931
use crate::metadata::{
3032
create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata,
3133
};
3234
use crate::{
3335
create_http_client, format_one_arg, format_three_args, SeleniumManager, BETA,
34-
DASH_DASH_VERSION, DEV, ENV_LOCALAPPDATA, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86,
35-
FALLBACK_RETRIES, NIGHTLY, REG_QUERY, REMOVE_X86, STABLE, WMIC_COMMAND, WMIC_COMMAND_ENV,
36+
DASH_DASH_VERSION, DEV, ENV_LOCALAPPDATA, ENV_PROGRAM_FILES, ENV_PROGRAM_FILES_X86, NIGHTLY,
37+
REG_QUERY, REMOVE_X86, STABLE, WMIC_COMMAND, WMIC_COMMAND_ENV,
3638
};
3739

3840
pub const CHROME_NAME: &str = "chrome";
3941
pub const CHROMEDRIVER_NAME: &str = "chromedriver";
4042
const DRIVER_URL: &str = "https://chromedriver.storage.googleapis.com/";
4143
const LATEST_RELEASE: &str = "LATEST_RELEASE";
44+
const CFT_URL: &str = "https://googlechromelabs.github.io/chrome-for-testing/";
45+
const GOOD_VERSIONS_ENDPOINT: &str = "known-good-versions-with-downloads.json";
46+
const LATEST_VERSIONS_ENDPOINT: &str = "last-known-good-versions-with-downloads.json";
4247

4348
pub struct ChromeManager {
4449
pub browser_name: &'static str,
4550
pub driver_name: &'static str,
4651
pub config: ManagerConfig,
4752
pub http_client: Client,
4853
pub log: Logger,
54+
pub driver_url: Option<String>,
4955
}
5056

5157
impl ChromeManager {
@@ -61,8 +67,143 @@ impl ChromeManager {
6167
http_client: create_http_client(default_timeout, default_proxy)?,
6268
config,
6369
log: Logger::default(),
70+
driver_url: None,
6471
}))
6572
}
73+
74+
fn create_latest_release_url(&self) -> String {
75+
format!("{}{}", DRIVER_URL, LATEST_RELEASE)
76+
}
77+
78+
fn create_latest_release_with_version_url(&self) -> String {
79+
format!(
80+
"{}{}_{}",
81+
DRIVER_URL,
82+
LATEST_RELEASE,
83+
self.get_major_browser_version()
84+
)
85+
}
86+
87+
fn create_good_versions_url(&self) -> String {
88+
format!("{}{}", CFT_URL, GOOD_VERSIONS_ENDPOINT)
89+
}
90+
91+
fn create_latest_versions_url(&self) -> String {
92+
format!("{}{}", CFT_URL, LATEST_VERSIONS_ENDPOINT)
93+
}
94+
95+
fn request_driver_version_from_latest(
96+
&self,
97+
driver_url: String,
98+
) -> Result<String, Box<dyn Error>> {
99+
self.log.debug(format!(
100+
"Reading {} version from {}",
101+
&self.driver_name, driver_url
102+
));
103+
read_version_from_link(self.get_http_client(), driver_url, self.get_logger())
104+
}
105+
106+
fn request_versions_from_cft<T>(&self, driver_url: String) -> Result<T, Box<dyn Error>>
107+
where
108+
T: Serialize + for<'a> Deserialize<'a>,
109+
{
110+
self.log.debug(format!(
111+
"Reading {} metadata from {}",
112+
&self.driver_name, driver_url
113+
));
114+
parse_json_from_url::<T>(self.get_http_client(), driver_url)
115+
}
116+
117+
fn request_latest_driver_version_from_cft(&mut self) -> Result<String, Box<dyn Error>> {
118+
let versions_with_downloads = self
119+
.request_versions_from_cft::<LatestVersionsWithDownloads>(
120+
self.create_latest_versions_url(),
121+
)?;
122+
123+
let stable_channel = versions_with_downloads.channels.stable;
124+
let chromedriver = stable_channel.downloads.chromedriver;
125+
if chromedriver.is_none() {
126+
// This should be temporal, since currently the stable channel has no chromedriver download
127+
self.log.warn(format!(
128+
"Latest stable version of {} not found using CfT endpoints. Trying with {}",
129+
&self.driver_name, LATEST_RELEASE
130+
));
131+
return self.request_driver_version_from_latest(self.create_latest_release_url());
132+
}
133+
134+
let url: Vec<&PlatformUrl> = chromedriver
135+
.as_ref()
136+
.unwrap()
137+
.iter()
138+
.filter(|p| p.platform.eq_ignore_ascii_case(self.get_platform_label()))
139+
.collect();
140+
self.log.trace(format!("URLs for CfT: {:?}", url));
141+
self.driver_url = Some(url.first().unwrap().url.to_string());
142+
143+
Ok(stable_channel.version)
144+
}
145+
146+
fn request_good_version_from_cft(&mut self) -> Result<String, Box<dyn Error>> {
147+
let browser_or_driver_version = if self.get_driver_version().is_empty() {
148+
self.get_browser_version()
149+
} else {
150+
self.get_driver_version()
151+
};
152+
let version_for_filtering = self.get_major_version(browser_or_driver_version)?;
153+
self.log.trace(format!(
154+
"Driver version used to request CfT: {version_for_filtering}"
155+
));
156+
157+
let all_versions = self
158+
.request_versions_from_cft::<VersionsWithDownloads>(self.create_good_versions_url())?;
159+
let filtered_versions: Vec<Version> = all_versions
160+
.versions
161+
.into_iter()
162+
.filter(|r| r.version.starts_with(version_for_filtering.as_str()))
163+
.collect();
164+
if filtered_versions.is_empty() {
165+
return Err(format!(
166+
"{} {} not available",
167+
self.get_driver_name(),
168+
version_for_filtering
169+
)
170+
.into());
171+
}
172+
173+
let driver_version = filtered_versions.last().unwrap();
174+
let url: Vec<&PlatformUrl> = driver_version
175+
.downloads
176+
.chromedriver
177+
.as_ref()
178+
.unwrap()
179+
.iter()
180+
.filter(|p| p.platform.eq_ignore_ascii_case(self.get_platform_label()))
181+
.collect();
182+
self.log.trace(format!("URLs for CfT: {:?}", url));
183+
self.driver_url = Some(url.first().unwrap().url.to_string());
184+
185+
Ok(driver_version.version.to_string())
186+
}
187+
188+
fn get_platform_label(&self) -> &str {
189+
let os = self.get_os();
190+
let arch = self.get_arch();
191+
if WINDOWS.is(os) {
192+
if X32.is(arch) {
193+
"win32"
194+
} else {
195+
"win64"
196+
}
197+
} else if MACOS.is(os) {
198+
if ARM64.is(arch) {
199+
"mac-arm64"
200+
} else {
201+
"mac-x64"
202+
}
203+
} else {
204+
"linux64"
205+
}
206+
}
66207
}
67208

68209
impl SeleniumManager for ChromeManager {
@@ -162,8 +303,9 @@ impl SeleniumManager for ChromeManager {
162303
self.driver_name
163304
}
164305

165-
fn request_driver_version(&self) -> Result<String, Box<dyn Error>> {
166-
let browser_version = self.get_browser_version();
306+
fn request_driver_version(&mut self) -> Result<String, Box<dyn Error>> {
307+
let browser_version_binding = self.get_major_browser_version();
308+
let browser_version = browser_version_binding.as_str();
167309
let mut metadata = get_metadata(self.get_logger());
168310
let driver_ttl = self.get_config().driver_ttl;
169311

@@ -177,46 +319,23 @@ impl SeleniumManager for ChromeManager {
177319
Ok(driver_version)
178320
}
179321
_ => {
180-
let mut driver_version = "".to_string();
181-
let mut browser_version_int = browser_version.parse::<i32>().unwrap_or_default();
182-
for i in 0..FALLBACK_RETRIES {
183-
let driver_url = if browser_version.is_empty() {
184-
format!("{}{}", DRIVER_URL, LATEST_RELEASE)
185-
} else {
186-
format!("{}{}_{}", DRIVER_URL, LATEST_RELEASE, browser_version_int)
187-
};
188-
if !browser_version.is_empty() && browser_version_int <= 0 {
189-
break;
190-
}
191-
self.log.debug(format!(
192-
"Reading {} version from {}",
193-
&self.driver_name, driver_url
194-
));
195-
match read_version_from_link(
196-
self.get_http_client(),
197-
driver_url,
198-
self.get_logger(),
199-
) {
200-
Ok(version) => {
201-
driver_version = version;
202-
break;
203-
}
204-
Err(err) => {
205-
if !err.to_string().eq(PARSE_ERROR) {
206-
return Err(err);
207-
}
208-
self.log.warn(format!(
209-
"Error getting version of {} {}. Retrying with {} {} (attempt {}/{})",
210-
&self.driver_name,
211-
browser_version_int,
212-
&self.driver_name,
213-
browser_version_int - 1,
214-
i + 1, FALLBACK_RETRIES
215-
));
216-
browser_version_int -= 1;
217-
}
218-
}
219-
}
322+
let major_browser_version = browser_version.parse::<i32>().unwrap_or_default();
323+
let driver_version = if !browser_version.is_empty() && major_browser_version < 115 {
324+
// For old versions (chromedriver 114-), the traditional method should work:
325+
// https://chromedriver.chromium.org/downloads
326+
self.request_driver_version_from_latest(
327+
self.create_latest_release_with_version_url(),
328+
)?
329+
} else if browser_version.is_empty() {
330+
// For discovering the latest driver version, the CfT endpoints are also used
331+
self.request_latest_driver_version_from_cft()?
332+
} else {
333+
// As of chromedriver 115+, the metadata for version discovery are published
334+
// by the "Chrome for Testing" (CfT) JSON endpoints:
335+
// https://googlechromelabs.github.io/chrome-for-testing/
336+
self.request_good_version_from_cft()?
337+
};
338+
220339
if !browser_version.is_empty() && !driver_version.is_empty() {
221340
metadata.drivers.push(create_driver_metadata(
222341
browser_version,
@@ -231,7 +350,22 @@ impl SeleniumManager for ChromeManager {
231350
}
232351
}
233352

234-
fn get_driver_url(&self) -> Result<String, Box<dyn Error>> {
353+
fn get_driver_url(&mut self) -> Result<String, Box<dyn Error>> {
354+
let major_driver_version = self
355+
.get_major_driver_version()
356+
.parse::<i32>()
357+
.unwrap_or_default();
358+
359+
if major_driver_version >= 115 && self.driver_url.is_none() {
360+
// This case happens when driver_version is set (e.g. using CLI flag)
361+
self.request_good_version_from_cft()?;
362+
}
363+
364+
// As of Chrome 115+, the driver URL is already gathered thanks to the CfT endpoints
365+
if self.driver_url.is_some() {
366+
return Ok(self.driver_url.as_ref().unwrap().to_string());
367+
}
368+
235369
let driver_version = self.get_driver_version();
236370
let os = self.get_os();
237371
let arch = self.get_arch();
@@ -241,10 +375,6 @@ impl SeleniumManager for ChromeManager {
241375
if ARM64.is(arch) {
242376
// As of chromedriver 106, the naming convention for macOS ARM64 releases changed. See:
243377
// https://groups.google.com/g/chromedriver-users/c/JRuQzH3qr2c
244-
let major_driver_version = self
245-
.get_major_version(driver_version)?
246-
.parse::<i32>()
247-
.unwrap_or_default();
248378
if major_driver_version < 106 {
249379
"mac64_m1"
250380
} else {
@@ -265,18 +395,7 @@ impl SeleniumManager for ChromeManager {
265395
fn get_driver_path_in_cache(&self) -> PathBuf {
266396
let driver_version = self.get_driver_version();
267397
let os = self.get_os();
268-
let arch = self.get_arch();
269-
let arch_folder = if WINDOWS.is(os) {
270-
"win32"
271-
} else if MACOS.is(os) {
272-
if ARM64.is(arch) {
273-
"mac-arm64"
274-
} else {
275-
"mac64"
276-
}
277-
} else {
278-
"linux64"
279-
};
398+
let arch_folder = self.get_platform_label();
280399
compose_driver_path_in_cache(self.driver_name, os, arch_folder, driver_version)
281400
}
282401

@@ -300,3 +419,54 @@ impl SeleniumManager for ChromeManager {
300419
self.log = log;
301420
}
302421
}
422+
423+
#[derive(Serialize, Deserialize)]
424+
pub struct LatestVersionsWithDownloads {
425+
pub timestamp: String,
426+
pub channels: Channels,
427+
}
428+
429+
#[derive(Serialize, Deserialize)]
430+
pub struct Channels {
431+
#[serde(rename = "Stable")]
432+
pub stable: Channel,
433+
#[serde(rename = "Beta")]
434+
pub beta: Channel,
435+
#[serde(rename = "Dev")]
436+
pub dev: Channel,
437+
#[serde(rename = "Canary")]
438+
pub canary: Channel,
439+
}
440+
441+
#[derive(Serialize, Deserialize, Debug)]
442+
pub struct Channel {
443+
pub channel: String,
444+
pub version: String,
445+
pub revision: String,
446+
pub downloads: Downloads,
447+
}
448+
449+
#[derive(Serialize, Deserialize, Debug)]
450+
pub struct VersionsWithDownloads {
451+
pub timestamp: String,
452+
pub versions: Vec<Version>,
453+
}
454+
455+
#[derive(Serialize, Deserialize, Debug)]
456+
pub struct Version {
457+
pub version: String,
458+
pub revision: String,
459+
pub downloads: Downloads,
460+
}
461+
462+
#[derive(Serialize, Deserialize, Debug)]
463+
pub struct Downloads {
464+
pub chrome: Vec<PlatformUrl>,
465+
pub chromedriver: Option<Vec<PlatformUrl>>,
466+
}
467+
468+
#[derive(Serialize, Deserialize, Debug)]
469+
pub struct PlatformUrl {
470+
pub platform: String,
471+
pub url: String,
472+
}

rust/src/downloads.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717

1818
use reqwest::Client;
19+
use serde::{Deserialize, Serialize};
1920
use std::error::Error;
2021
use std::fs::File;
2122
use std::io::copy;
@@ -92,3 +93,12 @@ pub async fn read_redirect_from_link(
9293
log,
9394
)
9495
}
96+
97+
pub fn parse_json_from_url<T>(http_client: &Client, url: String) -> Result<T, Box<dyn Error>>
98+
where
99+
T: Serialize + for<'a> Deserialize<'a>,
100+
{
101+
let content = read_content_from_link(http_client, url)?;
102+
let response: T = serde_json::from_str(&content)?;
103+
Ok(response)
104+
}

0 commit comments

Comments
 (0)