webdriver_install/
chromedriver.rs1use eyre::{eyre, Result};
6use regex::Regex;
7use tracing::debug;
8use url::Url;
9
10use std::process::{Command, Stdio};
11
12use crate::DriverFetcher;
13
14#[cfg(target_os = "windows")]
15use crate::run_powershell_cmd;
16
17use std::path::PathBuf;
18
19pub struct Chromedriver;
20
21impl DriverFetcher for Chromedriver {
22 const BASE_URL: &'static str = "https://chromedriver.storage.googleapis.com";
23
24 fn latest_version(&self) -> Result<String> {
26 let latest_release_url = format!(
27 "{}/LATEST_RELEASE_{}",
28 Self::BASE_URL,
29 Version::find()?.build_version()
30 );
31 debug!("latest_release_url: {}", latest_release_url);
32 let resp = reqwest::blocking::get(&latest_release_url)?;
33 Ok(resp.text()?)
34 }
35
36 fn direct_download_url(&self, version: &str) -> Result<Url> {
38 Ok(Url::parse(&format!(
39 "{}/{version}/chromedriver_{platform}",
40 Self::BASE_URL,
41 version = version,
42 platform = Self::platform()?
43 ))?)
44 }
45}
46
47impl Chromedriver {
48 pub fn new() -> Self {
49 Self {}
50 }
51
52 fn platform() -> Result<String> {
60 match sys_info::os_type()?.as_str() {
61 "Linux" => Ok(String::from("linux64.zip")),
62 "Darwin" => Ok(String::from("mac64.zip")),
63 "Windows" => Ok(String::from("win32.zip")),
64 other => Err(eyre!(
65 "webdriver-install doesn't support '{}' currently",
66 other
67 )),
68 }
69 }
70}
71
72#[derive(Debug, PartialEq)]
73pub struct Version {
74 major: i16,
75 minor: i16,
76 build: i16,
77 patch: i16,
78}
79
80struct Location {}
81
82#[cfg(target_os = "linux")]
83static LINUX_CHROME_DIRS: &[&'static str] = &[
84 "/usr/local/sbin",
85 "/usr/local/bin",
86 "/usr/sbin",
87 "/usr/bin",
88 "/sbin",
89 "/bin",
90 "/opt/google/chrome",
91];
92#[cfg(target_os = "linux")]
93static LINUX_CHROME_FILES: &[&'static str] =
94 &["google-chrome", "chrome", "chromium", "chromium-browser"];
95
96#[cfg(target_os = "windows")]
97static WIN_CHROME_DIRS: &[&'static str] = &["Google\\Chrome\\Application", "Chromium\\Application"];
98
99#[cfg(target_os = "macos")]
100static MAC_CHROME_DIRS: &[&'static str] = &[
101 "/Applications/Chromium.app",
102 "/Applications/Google Chrome.app",
103];
104#[cfg(target_os = "macos")]
105static MAC_CHROME_FILES: &[&'static str] =
106 &["Contents/MacOS/Chromium", "Contents/MacOS/Google Chrome"];
107
108impl Version {
109 pub fn find() -> Result<Self> {
111 #[cfg(target_os = "linux")]
112 return Self::linux_version();
113 #[cfg(target_os = "windows")]
114 return Self::windows_version();
115 #[cfg(target_os = "macos")]
116 return Self::mac_version();
117 }
118
119 #[allow(dead_code)]
121 pub fn full_version(&self) -> String {
122 format!(
123 "{}.{}.{}.{}",
124 self.major, self.minor, self.build, self.patch
125 )
126 }
127
128 pub fn build_version(&self) -> String {
130 format!("{}.{}.{}", self.major, self.minor, self.build)
131 }
132
133 #[cfg(target_os = "linux")]
134 fn linux_version() -> Result<Self> {
135 let output = Command::new(Location::location()?)
137 .arg("--version")
138 .stdout(Stdio::piped())
139 .output()?
140 .stdout;
141
142 let output = String::from_utf8(output)?;
143 debug!("Chrome --version output: {}", output);
144
145 Ok(Self::version_from_output(&output)?)
146 }
147
148 #[cfg(target_os = "windows")]
149 fn windows_version() -> Result<Self> {
150 let output = run_powershell_cmd(&format!(
151 "(Get-ItemProperty '{}').VersionInfo.ProductVersion",
152 Location::location()?.display()
153 ));
154
155 let stdout = String::from_utf8(output.stdout)?;
156 debug!("chrome version: {}", stdout);
157
158 Ok(Self::version_from_output(&stdout)?)
159 }
160
161 #[cfg(target_os = "macos")]
162 fn mac_version() -> Result<Self> {
163 let output = Command::new(Location::location()?)
164 .arg("--version")
165 .stdout(Stdio::piped())
166 .output()?
167 .stdout;
168
169 let output = String::from_utf8(output)?;
170 debug!("Chrome --version output: {}", output);
171
172 Ok(Self::version_from_output(&output)?)
173 }
174
175 fn version_from_output(output: &str) -> Result<Self> {
176 let version_pattern = Regex::new(r"\d+\.\d+\.\d+\.\d+")?;
177 let version = version_pattern
178 .captures(&output)
179 .ok_or(eyre!(
180 "regex: Could not find 4-part Chrome version string in '{}'",
181 output
182 ))?
183 .get(0)
184 .map_or("", |m| m.as_str());
185 let parts: Vec<i16> = version
186 .split(".")
187 .map(|i| i.parse::<i16>().unwrap())
188 .collect();
189
190 Ok(Self {
191 major: parts[0],
192 minor: parts[1],
193 build: parts[2],
194 patch: parts[3],
195 })
196 }
197}
198
199impl Location {
200 pub fn location() -> Result<PathBuf> {
202 #[cfg(target_os = "linux")]
203 return Self::linux_location();
204 #[cfg(target_os = "windows")]
205 return Self::windows_location();
206 #[cfg(target_os = "macos")]
207 return Self::mac_location();
208 }
209
210 #[cfg(target_os = "linux")]
211 fn linux_location() -> Result<PathBuf> {
212 for dir in LINUX_CHROME_DIRS.into_iter().map(PathBuf::from) {
214 for file in LINUX_CHROME_FILES {
215 let path = dir.join(file);
216 if path.exists() {
217 return Ok(path);
218 }
219 }
220 }
221 Err(eyre!("Unable to find chrome executable"))
222 }
223
224 #[cfg(target_os = "windows")]
225 fn windows_location() -> Result<PathBuf> {
226 use dirs_sys::known_folder;
227
228 let roots = vec![
229 known_folder(&winapi::um::knownfolders::FOLDERID_ProgramFiles),
230 known_folder(&winapi::um::knownfolders::FOLDERID_ProgramFilesX86),
231 known_folder(&winapi::um::knownfolders::FOLDERID_ProgramFilesX64),
232 ]
233 .into_iter()
234 .flatten()
235 .collect::<Vec<PathBuf>>();
236 for dir in WIN_CHROME_DIRS.into_iter().map(PathBuf::from) {
237 for root in &roots {
238 let path = root.join(&dir).join("chrome.exe");
239 debug!("root: {}", root.display());
240 debug!("checking path {}", &path.display());
241 if path.exists() {
242 return Ok(path);
243 }
244 }
245 }
246 Err(eyre!("Unable to find chrome executable"))
247 }
248
249 #[cfg(target_os = "macos")]
250 fn mac_location() -> Result<PathBuf> {
251 for dir in MAC_CHROME_DIRS.into_iter().map(PathBuf::from) {
252 for file in MAC_CHROME_FILES {
253 let path = dir.join(file);
254 if path.exists() {
255 return Ok(path);
256 }
257 }
258 }
259 Err(eyre!("Unable to find chrome executable"))
260 }
261}
262
263#[test]
264fn version_from_output_test() {
265 assert_eq!(
266 Version::version_from_output("Chromium 87.0.4280.141 snap").unwrap(),
267 Version {
268 major: 87,
269 minor: 0,
270 build: 4280,
271 patch: 141
272 }
273 );
274 assert_eq!(
275 Version::version_from_output("127.0.0.1").unwrap(),
276 Version {
277 major: 127,
278 minor: 0,
279 build: 0,
280 patch: 1
281 }
282 );
283}
284
285#[test]
286#[should_panic(expected = "Could not find 4-part Chrome version string in 'a.0.0.1'")]
287fn version_from_output_panic_test() {
288 Version::version_from_output("a.0.0.1").unwrap();
289}
290
291#[test]
292#[should_panic(expected = "Could not find 4-part Chrome version string in 'abc 1.0.1 def'")]
293fn version_from_output_panic_not_4_parts_test() {
294 Version::version_from_output("abc 1.0.1 def").unwrap();
295}
296
297#[test]
298fn direct_download_url_test() {
299 #[cfg(target_os = "linux")]
300 assert_eq!(
301 "https://chromedriver.storage.googleapis.com/v1/chromedriver_linux64.zip",
302 Chromedriver::new()
303 .direct_download_url("v1")
304 .unwrap()
305 .to_string()
306 );
307 #[cfg(target_os = "macos")]
308 assert_eq!(
309 "https://chromedriver.storage.googleapis.com/v1/chromedriver_mac64.zip",
310 Chromedriver::new()
311 .direct_download_url("v1")
312 .unwrap()
313 .to_string()
314 );
315 #[cfg(target_os = "windows")]
316 assert_eq!(
317 "https://chromedriver.storage.googleapis.com/v1/chromedriver_win32.zip",
318 Chromedriver::new()
319 .direct_download_url("v1")
320 .unwrap()
321 .to_string()
322 );
323}