1pub mod patcher;
64pub mod injections;
65
66#[cfg(feature = "webdriver")]
67pub mod client;
68
69#[cfg(feature = "tui")]
70pub mod widget;
71
72pub use patcher::ChromeDriverPatcher;
74pub use injections::COMPREHENSIVE_BOOTSTRAP;
75
76#[cfg(feature = "webdriver")]
77pub use client::OrbBrowser;
78
79use color_eyre::Result;
80use std::path::PathBuf;
81use std::process::{Command, Child};
82use std::fs;
83use std::net::TcpListener;
84
85fn get_chrome_version() -> Result<String> {
87 let chrome_path = find_chrome()
88 .ok_or_else(|| color_eyre::eyre::eyre!("Chrome/Chromium not found on system"))?;
89
90 let output = Command::new(&chrome_path)
91 .arg("--version")
92 .output()
93 .map_err(|e| color_eyre::eyre::eyre!("Failed to get Chrome version: {}", e))?;
94
95 let version_str = String::from_utf8(output.stdout)
96 .map_err(|e| color_eyre::eyre::eyre!("Failed to parse Chrome version: {}", e))?;
97
98 let version = version_str
100 .split_whitespace()
101 .nth(1)
102 .and_then(|v| v.split('.').next())
103 .ok_or_else(|| color_eyre::eyre::eyre!("Failed to parse Chrome version from: {}", version_str.trim()))?;
104
105 Ok(version.to_string())
106}
107
108fn get_chromedriver_version(chrome_major: &str) -> Result<String> {
110 let url = format!(
112 "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json"
113 );
114
115 let output = Command::new("curl")
116 .arg("-sL")
117 .arg(&url)
118 .output()
119 .map_err(|e| color_eyre::eyre::eyre!("Failed to fetch ChromeDriver version info: {}", e))?;
120
121 if !output.status.success() {
122 return Err(color_eyre::eyre::eyre!("Failed to fetch ChromeDriver version info"));
123 }
124
125 let json_str = String::from_utf8(output.stdout)
126 .map_err(|e| color_eyre::eyre::eyre!("Failed to parse version JSON: {}", e))?;
127
128 let json: serde_json::Value = serde_json::from_str(&json_str)
130 .map_err(|e| color_eyre::eyre::eyre!("Failed to parse version JSON: {}", e))?;
131
132 let version = json
133 .get("milestones")
134 .and_then(|m| m.get(chrome_major))
135 .and_then(|v| v.get("version"))
136 .and_then(|v| v.as_str())
137 .ok_or_else(|| color_eyre::eyre::eyre!(
138 "ChromeDriver version {} not found. Your Chrome version may be too new or too old. \
139 Visit https://googlechromelabs.github.io/chrome-for-testing/ for available versions.",
140 chrome_major
141 ))?;
142
143 Ok(version.to_string())
144}
145
146fn download_chromedriver() -> Result<PathBuf> {
148 let cache_dir = dirs::cache_dir()
149 .unwrap_or_else(|| PathBuf::from("/tmp"))
150 .join("orb-browse/drivers");
151
152 fs::create_dir_all(&cache_dir)?;
153
154 let chrome_major = get_chrome_version()?;
156
157 let driver_version = get_chromedriver_version(&chrome_major)?;
159
160 let driver_path = cache_dir.join(format!("chromedriver-{}", chrome_major));
162
163 if driver_path.exists() {
165 return Ok(driver_path);
166 }
167
168 let (os, arch) = if cfg!(target_os = "linux") {
170 if cfg!(target_arch = "x86_64") {
171 ("linux64", "chromedriver-linux64")
172 } else {
173 return Err(color_eyre::eyre::eyre!("Unsupported architecture"));
174 }
175 } else if cfg!(target_os = "macos") {
176 if cfg!(target_arch = "aarch64") {
177 ("mac-arm64", "chromedriver-mac-arm64")
178 } else {
179 ("mac-x64", "chromedriver-mac-x64")
180 }
181 } else {
182 return Err(color_eyre::eyre::eyre!("Unsupported OS"));
183 };
184
185 let url = format!(
187 "https://storage.googleapis.com/chrome-for-testing-public/{}/{}/chromedriver-{}.zip",
188 driver_version, os, os
189 );
190
191 let zip_path = cache_dir.join(format!("chromedriver-{}.zip", chrome_major));
193 let status = Command::new("curl")
194 .arg("-L")
195 .arg("-o")
196 .arg(&zip_path)
197 .arg(&url)
198 .stdout(std::process::Stdio::null())
199 .stderr(std::process::Stdio::null())
200 .status()?;
201
202 if !status.success() {
203 return Err(color_eyre::eyre::eyre!(
204 "Failed to download ChromeDriver {} for Chrome {}. \
205 Check your internet connection or visit https://googlechromelabs.github.io/chrome-for-testing/",
206 driver_version, chrome_major
207 ));
208 }
209
210 let status = Command::new("unzip")
212 .arg("-o")
213 .arg(&zip_path)
214 .arg("-d")
215 .arg(&cache_dir)
216 .stdout(std::process::Stdio::null())
217 .stderr(std::process::Stdio::null())
218 .status()?;
219
220 if !status.success() {
221 return Err(color_eyre::eyre::eyre!("Failed to extract ChromeDriver"));
222 }
223
224 let extracted_path = cache_dir.join(arch).join("chromedriver");
226 fs::rename(&extracted_path, &driver_path)?;
227
228 #[cfg(unix)]
230 {
231 use std::os::unix::fs::PermissionsExt;
232 let mut perms = fs::metadata(&driver_path)?.permissions();
233 perms.set_mode(0o755);
234 fs::set_permissions(&driver_path, perms)?;
235 }
236
237 let _ = fs::remove_file(&zip_path);
239 let _ = fs::remove_dir_all(cache_dir.join(arch));
240
241 if let Ok(entries) = fs::read_dir(&cache_dir) {
243 for entry in entries.flatten() {
244 let path = entry.path();
245 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
246 if (name.starts_with("chromedriver-") && name != format!("chromedriver-{}", chrome_major))
248 || name == "chromedriver" {
249 let _ = fs::remove_file(&path);
250 }
251 }
252 }
253 }
254
255 #[cfg(unix)]
257 {
258 let symlink_path = cache_dir.join("chromedriver");
259 let _ = fs::remove_file(&symlink_path); let _ = std::os::unix::fs::symlink(&driver_path, &symlink_path);
261 }
262
263 Ok(driver_path)
264}
265
266fn find_available_port() -> Result<u16> {
268 let listener = TcpListener::bind("127.0.0.1:0")?;
269 let port = listener.local_addr()?.port();
270 Ok(port)
271}
272
273pub fn launch_patched_chromedriver() -> Result<(String, Child)> {
298 let original_driver = download_chromedriver()?;
300
301 let patcher = ChromeDriverPatcher::new();
303 let patched_driver = patcher.patch_driver(&original_driver)?;
304
305 let port = find_available_port()?;
307
308 let verbose = std::env::var("ORB_BROWSE_VERBOSE").is_ok();
310
311 let mut cmd = Command::new(&patched_driver);
313 cmd.arg(format!("--port={}", port));
314
315 if verbose {
316 eprintln!("[orb-browse] Launching ChromeDriver on port {} with verbose output", port);
317 cmd.arg("--verbose");
318 } else {
319 cmd.arg("--silent")
320 .arg("--log-level=OFF")
321 .stdout(std::process::Stdio::null())
322 .stderr(std::process::Stdio::null());
323 }
324
325 let mut child = cmd
326 .spawn()
327 .map_err(|e| color_eyre::eyre::eyre!("Failed to launch ChromeDriver: {}", e))?;
328
329 let webdriver_url = format!("http://localhost:{}", port);
330
331 let mut attempts = 0;
333 let max_attempts = 30;
334 loop {
335 std::thread::sleep(std::time::Duration::from_millis(100));
336 attempts += 1;
337
338 match child.try_wait() {
340 Ok(Some(status)) => {
341 return Err(color_eyre::eyre::eyre!(
342 "ChromeDriver exited immediately with status: {}.\n\
343 This is often caused by:\n\
344 1. Missing dependencies (libnss3, libx11, etc.)\n\
345 2. Chrome/ChromeDriver version mismatch\n\
346 3. Chrome binary not executable\n\
347 \n\
348 Run with ORB_BROWSE_VERBOSE=1 to see detailed error output:\n\
349 ORB_BROWSE_VERBOSE=1 cargo run",
350 status
351 ));
352 }
353 Ok(None) => {
354 if std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() {
356 if verbose {
357 eprintln!("[orb-browse] ChromeDriver ready on port {}", port);
358 }
359 return Ok((webdriver_url, child));
360 }
361 }
362 Err(e) => {
363 return Err(color_eyre::eyre::eyre!("Failed to check ChromeDriver status: {}", e));
364 }
365 }
366
367 if attempts >= max_attempts {
368 let _ = child.kill();
369 return Err(color_eyre::eyre::eyre!(
370 "ChromeDriver failed to become responsive after 3 seconds.\n\
371 The process is running but not accepting connections.\n\
372 Run with ORB_BROWSE_VERBOSE=1 to see detailed output:\n\
373 ORB_BROWSE_VERBOSE=1 cargo run"
374 ));
375 }
376 }
377}
378
379pub fn find_chrome() -> Option<PathBuf> {
387 if let Ok(chrome_path) = std::env::var("ORB_BROWSE_CHROME_PATH") {
389 let p = PathBuf::from(chrome_path);
390 if p.exists() {
391 return Some(p);
392 } else {
393 eprintln!("Warning: ORB_BROWSE_CHROME_PATH points to non-existent file: {}", p.display());
394 }
395 }
396
397 let candidates = vec![
398 "/usr/bin/chromium-browser",
400 "/usr/bin/chromium",
401 "/usr/bin/google-chrome",
402 "/usr/bin/google-chrome-stable",
403 "/usr/bin/chrome",
404 "/snap/bin/chromium",
405 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
406 "/Applications/Chromium.app/Contents/MacOS/Chromium",
407 ];
408
409 for candidate in candidates {
410 let p = PathBuf::from(candidate);
411 if p.exists() {
412 return Some(p);
413 }
414 }
415
416 None
417}