Skip to main content

chromedriver_manager/
manager.rs

1/*
2    TODO: Make platform compatable
3    TODO: Make auto updater + option to set version
4*/
5
6use crate::utils::{
7    appdata::get_cache_dir,
8    downloader::{download_chrome, download_chromedriver},
9    functions::{get_latest_chrome_package, get_platform},
10};
11
12use crate::{
13    loglevel::LogLevel,
14    structs::{
15        chrome::{ChromeDownload, DriverDownload},
16        packages::ChromePackage,
17    },
18};
19
20use std::{
21    path::PathBuf,
22    process::{self, Command},
23};
24
25use anyhow::{Context, Ok};
26use thirtyfour::{ChromeCapabilities, ChromiumLikeCapabilities};
27
28const CHROME_DOWNLOADS_URL: &str =
29    "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json";
30
31pub struct Handler {
32    client: reqwest::Client,
33    platform: String,
34}
35
36impl Default for Handler {
37    fn default() -> Self {
38        Self {
39            client: reqwest::Client::new(),
40            platform: get_platform(),
41        }
42    }
43}
44
45impl Handler {
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    fn get_default_paths(&self) -> (PathBuf, PathBuf) {
51        let chrome_path = get_cache_dir().join(format!("chrome-{}", self.platform));
52        let driver_path = get_cache_dir().join(format!("chromedriver-{}", self.platform));
53
54        (chrome_path, driver_path)
55    }
56
57    fn package_downloaded(&self) -> bool {
58        let (chrome_path, driver_path) = self.get_default_paths();
59
60        if chrome_path.exists() && driver_path.exists() {
61            return true;
62        }
63
64        false
65    }
66
67    async fn get_packages(&self) -> anyhow::Result<Vec<ChromePackage>> {
68        let client = reqwest::Client::new();
69
70        let response: serde_json::Value = client
71            .get(CHROME_DOWNLOADS_URL)
72            .send()
73            .await?
74            .text()
75            .await?
76            .parse()?;
77
78        let chrome_packages: serde_json::Value = response["versions"].clone();
79        let chrome_packages: Vec<ChromePackage> = serde_json::from_value(chrome_packages)?;
80
81        Ok(chrome_packages)
82    }
83
84    // TODO: Allow users to specify version
85    async fn get_selected_package(
86        &self,
87        packages: &Vec<ChromePackage>,
88    ) -> anyhow::Result<ChromePackage> {
89        get_latest_chrome_package(packages).context("Could not get the latest version of chrome")
90    }
91
92    async fn download_files(&self) -> anyhow::Result<(PathBuf, PathBuf)> {
93        let chrome_packages = self.get_packages().await?;
94        let selected_package = self.get_selected_package(&chrome_packages).await?;
95
96        // TODO: Make platform configurable
97        let chrome_download: &ChromeDownload = selected_package
98            .get_chrome_download(&self.platform)
99            .expect("Chrome download not found");
100
101        let chromedriver_download: &DriverDownload = selected_package
102            .get_chromedriver_download(&self.platform)
103            .expect("Chromedriver download not found");
104
105        // Download Chrome and Chromedriver
106
107        let version = &selected_package.version;
108        println!(
109            "Installing chrome version {}.{}.{}.{}...\n",
110            version.major, version.minor, version.patch, version.build
111        );
112
113        download_chrome(&self.client, chrome_download).await?;
114        println!();
115        download_chromedriver(&self.client, chromedriver_download).await?;
116
117        let chrome_path = chrome_download.to_folder_path();
118        let driver_path = chromedriver_download.to_folder_path();
119
120        println!("Chrome path: {:?}", chrome_path);
121        println!("Chromedriver path: {:?}", driver_path);
122
123        Ok((chrome_path, driver_path))
124    }
125
126    // Return chrome.exe and chromedriver.exe if on windows, otherwise return chrome and chromedriver
127    fn get_file_names(&self) -> (String, String) {
128        let chrome_exe: String;
129        let chromedriver_exe: String;
130
131        if cfg!(target_os = "windows") {
132            chrome_exe = "chrome.exe".to_string();
133            chromedriver_exe = "chromedriver.exe".to_string();
134        } else if cfg!(target_os = "macos") {
135            chrome_exe = "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing".to_string();
136            chromedriver_exe = "chromedriver".to_string();
137        } else {
138            chrome_exe = "chrome".to_string();
139            chromedriver_exe = "chromedriver".to_string();
140        }
141
142        (chrome_exe, chromedriver_exe)
143    }
144
145    pub async fn launch_chromedriver(
146        &mut self,
147        capabilities: &mut ChromeCapabilities,
148        port: &str,
149        loglevel: LogLevel,
150    ) -> Result<process::Child, anyhow::Error> {
151        self.client = reqwest::Client::new();
152
153        let chrome_exe: PathBuf;
154        let chromedriver_exe: PathBuf;
155
156        let (chrome_exe_name, chromedriver_exe_name) = self.get_file_names();
157
158        if !self.package_downloaded() {
159            let (chrome_path, driver_path) = self.download_files().await?;
160
161            chrome_exe = chrome_path.join(chrome_exe_name);
162            chromedriver_exe = driver_path.join(chromedriver_exe_name);
163        } else {
164            let (default_chrome_path, default_driver_path) = self.get_default_paths();
165
166            chrome_exe = default_chrome_path.join(chrome_exe_name);
167            chromedriver_exe = default_driver_path.join(chromedriver_exe_name);
168        }
169
170        capabilities.set_binary(chrome_exe.to_str().unwrap())?;
171
172        let chromedriver_exe = chromedriver_exe.to_str().unwrap();
173
174        let mut command = Command::new(chromedriver_exe);
175        let mut command = command
176            .arg(format!("--port={}", port))
177            .arg(format!("--log-level={loglevel}"));
178
179        if loglevel == LogLevel::Off {
180            // command = command.creation_flags(0x08000000);
181            command = self.apply_creation_flags(command);
182        }
183
184        Ok(command.spawn()?)
185    }
186
187    #[cfg(target_os = "windows")]
188    fn apply_creation_flags<'a>(&self, command: &'a mut Command) -> &'a mut Command {
189        use std::os::windows::process::CommandExt;
190
191        command.creation_flags(0x08000000)
192    }
193
194    #[cfg(not(target_os = "windows"))]
195    fn apply_creation_flags<'a>(&self, command: &'a mut Command) -> &'a mut Command {
196        command
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use std::{thread, time::Duration};
203
204    use crate::{loglevel::LogLevel, manager::Handler};
205    use thirtyfour::prelude::*;
206
207    #[tokio::test]
208    async fn test_launch_chromedriver() -> anyhow::Result<()> {
209        let mut caps = DesiredCapabilities::chrome();
210        
211        caps.set_headless()?;
212        caps.set_no_sandbox()?;
213        caps.set_disable_gpu()?;
214
215        // Launch chromedriver on port 3000
216        let mut chromedriver = Handler::new()
217            .launch_chromedriver(&mut caps, "3000", LogLevel::Off)
218            .await?;
219
220        println!("Launched Chromedriver");
221
222        let driver = WebDriver::new("http://localhost:3000", caps).await?;
223        driver.goto("https://www.gimkit.com/join").await?;
224
225        thread::sleep(Duration::from_secs(10));
226
227        driver.quit().await?;
228        chromedriver.kill()?;
229        
230        Ok(())
231    }
232}