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 validate_chrome_installation(chrome_path: &PathBuf) -> Result<()> {
87 let path_str = chrome_path.to_string_lossy();
88
89 if path_str.contains("/snap/") {
90 return Err(color_eyre::eyre::eyre!(
91 "Snap Chromium detected at: {}\n\
92 \n\
93 Snap-packaged Chromium cannot be controlled by ChromeDriver due to snap sandboxing.\n\
94 \n\
95 Solutions:\n\
96 1. Install Chrome/Chromium via apt (recommended):\n\
97 sudo snap remove chromium\n\
98 sudo apt update\n\
99 sudo apt install chromium-browser\n\
100 \n\
101 2. Or install Google Chrome:\n\
102 wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb\n\
103 sudo apt install ./google-chrome-stable_current_amd64.deb\n\
104 \n\
105 3. Set ORB_BROWSE_CHROME_PATH to a non-snap Chrome:\n\
106 export ORB_BROWSE_CHROME_PATH=/usr/bin/chromium-browser",
107 path_str
108 ));
109 }
110
111 Ok(())
112}
113
114fn get_chrome_version() -> Result<String> {
116 let chrome_path = find_chrome()
117 .ok_or_else(|| color_eyre::eyre::eyre!("Chrome/Chromium not found on system"))?;
118
119 validate_chrome_installation(&chrome_path)?;
121
122 let output = Command::new(&chrome_path)
123 .arg("--version")
124 .output()
125 .map_err(|e| color_eyre::eyre::eyre!("Failed to get Chrome version: {}", e))?;
126
127 let version_str = String::from_utf8(output.stdout)
128 .map_err(|e| color_eyre::eyre::eyre!("Failed to parse Chrome version: {}", e))?;
129
130 let version = version_str
132 .split_whitespace()
133 .nth(1)
134 .and_then(|v| v.split('.').next())
135 .ok_or_else(|| color_eyre::eyre::eyre!("Failed to parse Chrome version from: {}", version_str.trim()))?;
136
137 Ok(version.to_string())
138}
139
140fn get_chromedriver_version(chrome_major: &str) -> Result<String> {
142 let url = format!(
144 "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json"
145 );
146
147 let output = Command::new("curl")
148 .arg("-sL")
149 .arg(&url)
150 .output()
151 .map_err(|e| color_eyre::eyre::eyre!("Failed to fetch ChromeDriver version info: {}", e))?;
152
153 if !output.status.success() {
154 return Err(color_eyre::eyre::eyre!("Failed to fetch ChromeDriver version info"));
155 }
156
157 let json_str = String::from_utf8(output.stdout)
158 .map_err(|e| color_eyre::eyre::eyre!("Failed to parse version JSON: {}", e))?;
159
160 let json: serde_json::Value = serde_json::from_str(&json_str)
162 .map_err(|e| color_eyre::eyre::eyre!("Failed to parse version JSON: {}", e))?;
163
164 let version = json
165 .get("milestones")
166 .and_then(|m| m.get(chrome_major))
167 .and_then(|v| v.get("version"))
168 .and_then(|v| v.as_str())
169 .ok_or_else(|| color_eyre::eyre::eyre!(
170 "ChromeDriver version {} not found. Your Chrome version may be too new or too old. \
171 Visit https://googlechromelabs.github.io/chrome-for-testing/ for available versions.",
172 chrome_major
173 ))?;
174
175 Ok(version.to_string())
176}
177
178fn download_chromedriver() -> Result<PathBuf> {
180 let cache_dir = dirs::cache_dir()
181 .unwrap_or_else(|| PathBuf::from("/tmp"))
182 .join("orb-browse/drivers");
183
184 fs::create_dir_all(&cache_dir)?;
185
186 let chrome_major = get_chrome_version()?;
188
189 let driver_version = get_chromedriver_version(&chrome_major)?;
191
192 let driver_path = cache_dir.join(format!("chromedriver-{}", chrome_major));
194
195 if driver_path.exists() {
197 return Ok(driver_path);
198 }
199
200 let (os, arch) = if cfg!(target_os = "linux") {
202 if cfg!(target_arch = "x86_64") {
203 ("linux64", "chromedriver-linux64")
204 } else {
205 return Err(color_eyre::eyre::eyre!("Unsupported architecture"));
206 }
207 } else if cfg!(target_os = "macos") {
208 if cfg!(target_arch = "aarch64") {
209 ("mac-arm64", "chromedriver-mac-arm64")
210 } else {
211 ("mac-x64", "chromedriver-mac-x64")
212 }
213 } else {
214 return Err(color_eyre::eyre::eyre!("Unsupported OS"));
215 };
216
217 let url = format!(
219 "https://storage.googleapis.com/chrome-for-testing-public/{}/{}/chromedriver-{}.zip",
220 driver_version, os, os
221 );
222
223 let zip_path = cache_dir.join(format!("chromedriver-{}.zip", chrome_major));
225 let status = Command::new("curl")
226 .arg("-L")
227 .arg("-o")
228 .arg(&zip_path)
229 .arg(&url)
230 .stdout(std::process::Stdio::null())
231 .stderr(std::process::Stdio::null())
232 .status()?;
233
234 if !status.success() {
235 return Err(color_eyre::eyre::eyre!(
236 "Failed to download ChromeDriver {} for Chrome {}. \
237 Check your internet connection or visit https://googlechromelabs.github.io/chrome-for-testing/",
238 driver_version, chrome_major
239 ));
240 }
241
242 let status = Command::new("unzip")
244 .arg("-o")
245 .arg(&zip_path)
246 .arg("-d")
247 .arg(&cache_dir)
248 .stdout(std::process::Stdio::null())
249 .stderr(std::process::Stdio::null())
250 .status()?;
251
252 if !status.success() {
253 return Err(color_eyre::eyre::eyre!("Failed to extract ChromeDriver"));
254 }
255
256 let extracted_path = cache_dir.join(arch).join("chromedriver");
258 fs::rename(&extracted_path, &driver_path)?;
259
260 #[cfg(unix)]
262 {
263 use std::os::unix::fs::PermissionsExt;
264 let mut perms = fs::metadata(&driver_path)?.permissions();
265 perms.set_mode(0o755);
266 fs::set_permissions(&driver_path, perms)?;
267 }
268
269 let _ = fs::remove_file(&zip_path);
271 let _ = fs::remove_dir_all(cache_dir.join(arch));
272
273 if let Ok(entries) = fs::read_dir(&cache_dir) {
275 for entry in entries.flatten() {
276 let path = entry.path();
277 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
278 if (name.starts_with("chromedriver-") && name != format!("chromedriver-{}", chrome_major))
280 || name == "chromedriver" {
281 let _ = fs::remove_file(&path);
282 }
283 }
284 }
285 }
286
287 #[cfg(unix)]
289 {
290 let symlink_path = cache_dir.join("chromedriver");
291 let _ = fs::remove_file(&symlink_path); let _ = std::os::unix::fs::symlink(&driver_path, &symlink_path);
293 }
294
295 Ok(driver_path)
296}
297
298fn find_available_port() -> Result<u16> {
300 let listener = TcpListener::bind("127.0.0.1:0")?;
301 let port = listener.local_addr()?.port();
302 Ok(port)
303}
304
305pub fn launch_patched_chromedriver() -> Result<(String, Child)> {
330 let original_driver = download_chromedriver()?;
332
333 let patcher = ChromeDriverPatcher::new();
335 let patched_driver = patcher.patch_driver(&original_driver)?;
336
337 let port = find_available_port()?;
339
340 let verbose = std::env::var("ORB_BROWSE_VERBOSE").is_ok();
342
343 let mut cmd = Command::new(&patched_driver);
345 cmd.arg(format!("--port={}", port));
346
347 if verbose {
348 eprintln!("[orb-browse] Launching ChromeDriver on port {} with verbose output", port);
349 cmd.arg("--verbose");
350 } else {
351 cmd.arg("--silent")
352 .arg("--log-level=OFF")
353 .stdout(std::process::Stdio::null())
354 .stderr(std::process::Stdio::null());
355 }
356
357 let mut child = cmd
358 .spawn()
359 .map_err(|e| color_eyre::eyre::eyre!("Failed to launch ChromeDriver: {}", e))?;
360
361 let webdriver_url = format!("http://localhost:{}", port);
362
363 let mut attempts = 0;
365 let max_attempts = 30;
366 loop {
367 std::thread::sleep(std::time::Duration::from_millis(100));
368 attempts += 1;
369
370 match child.try_wait() {
372 Ok(Some(status)) => {
373 return Err(color_eyre::eyre::eyre!(
374 "ChromeDriver exited immediately with status: {}.\n\
375 This is often caused by:\n\
376 1. Snap Chromium sandboxing (use apt-installed Chrome instead)\n\
377 2. Missing dependencies\n\
378 3. Chrome/ChromeDriver version mismatch\n\
379 \n\
380 Run with ORB_BROWSE_VERBOSE=1 to see detailed error output:\n\
381 ORB_BROWSE_VERBOSE=1 cargo run",
382 status
383 ));
384 }
385 Ok(None) => {
386 if std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() {
388 if verbose {
389 eprintln!("[orb-browse] ChromeDriver ready on port {}", port);
390 }
391 return Ok((webdriver_url, child));
392 }
393 }
394 Err(e) => {
395 return Err(color_eyre::eyre::eyre!("Failed to check ChromeDriver status: {}", e));
396 }
397 }
398
399 if attempts >= max_attempts {
400 let _ = child.kill();
401 return Err(color_eyre::eyre::eyre!(
402 "ChromeDriver failed to become responsive after 3 seconds.\n\
403 The process is running but not accepting connections.\n\
404 Run with ORB_BROWSE_VERBOSE=1 to see detailed output:\n\
405 ORB_BROWSE_VERBOSE=1 cargo run"
406 ));
407 }
408 }
409}
410
411pub fn find_chrome() -> Option<PathBuf> {
421 if let Ok(chrome_path) = std::env::var("ORB_BROWSE_CHROME_PATH") {
423 let p = PathBuf::from(chrome_path);
424 if p.exists() {
425 return Some(p);
426 } else {
427 eprintln!("Warning: ORB_BROWSE_CHROME_PATH points to non-existent file: {}", p.display());
428 }
429 }
430
431 let candidates = vec![
432 "/usr/bin/google-chrome",
434 "/usr/bin/google-chrome-stable",
435 "/usr/bin/chromium-browser",
436 "/usr/bin/chromium",
437 "/usr/bin/chrome",
438 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
439 "/Applications/Chromium.app/Contents/MacOS/Chromium",
440 "/snap/bin/chromium",
442 ];
443
444 for candidate in candidates {
445 let p = PathBuf::from(candidate);
446 if p.exists() {
447 return Some(p);
448 }
449 }
450
451 None
452}