1717
1818use crate :: config:: ManagerConfig ;
1919use reqwest:: Client ;
20+ use serde:: { Deserialize , Serialize } ;
2021use std:: collections:: HashMap ;
2122use std:: error:: Error ;
23+ use std:: option:: Option ;
2224use std:: path:: PathBuf ;
2325
24- use crate :: config:: ARCH :: ARM64 ;
26+ use crate :: config:: ARCH :: { ARM64 , X32 } ;
2527use 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 } ;
2830use crate :: logger:: Logger ;
2931use crate :: metadata:: {
3032 create_driver_metadata, get_driver_version_from_metadata, get_metadata, write_metadata,
3133} ;
3234use 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
3840pub const CHROME_NAME : & str = "chrome" ;
3941pub const CHROMEDRIVER_NAME : & str = "chromedriver" ;
4042const DRIVER_URL : & str = "https://chromedriver.storage.googleapis.com/" ;
4143const 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
4348pub 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
5157impl 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
68209impl 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+ }
0 commit comments