earl_protocol_browser/
launcher.rs1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use chromiumoxide::browser::BrowserConfig;
5use chromiumoxide::handler::Handler;
6use chromiumoxide::{Browser, Page};
7use futures::StreamExt;
8
9use crate::error::BrowserError;
10
11pub fn chrome_binary_candidates() -> Vec<PathBuf> {
18 if let Ok(p) = std::env::var("EARL_BROWSER_PATH") {
20 return vec![PathBuf::from(p)];
21 }
22
23 let mut candidates = vec![];
24
25 #[cfg(target_os = "macos")]
26 {
27 candidates.push(PathBuf::from(
28 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
29 ));
30 candidates.push(PathBuf::from(
31 "/Applications/Chromium.app/Contents/MacOS/Chromium",
32 ));
33 }
34
35 #[cfg(target_os = "linux")]
36 {
37 candidates.push(PathBuf::from("/usr/bin/google-chrome"));
38 candidates.push(PathBuf::from("/usr/bin/google-chrome-stable"));
39 candidates.push(PathBuf::from("/usr/bin/chromium-browser"));
40 candidates.push(PathBuf::from("/usr/bin/chromium"));
41 }
42
43 #[cfg(target_os = "windows")]
44 {
45 candidates.push(PathBuf::from(
46 r"C:\Program Files\Google\Chrome\Application\chrome.exe",
47 ));
48 candidates.push(PathBuf::from(
49 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
50 ));
51 }
52
53 for name in &["chrome", "google-chrome", "chromium", "chromium-browser"] {
55 if let Ok(p) = which::which(name)
56 && !candidates.contains(&p)
57 {
58 candidates.push(p);
59 }
60 }
61
62 candidates
63}
64
65pub fn find_chrome() -> Result<PathBuf> {
69 let candidates = chrome_binary_candidates();
70 for path in &candidates {
71 if path.exists() {
72 return Ok(path.clone());
73 }
74 }
75 let paths = candidates
76 .iter()
77 .map(|p| format!(" - {}", p.display()))
78 .collect::<Vec<_>>()
79 .join("\n");
80 Err(BrowserError::ChromeNotFound { paths }.into())
81}
82
83fn spawn_handler(mut handler: Handler) {
89 tokio::spawn(async move { while handler.next().await.is_some() {} });
90}
91
92pub async fn launch_chrome(headless: bool) -> Result<(Browser, String)> {
101 let chrome = find_chrome()?;
102
103 let mut config_builder = BrowserConfig::builder()
104 .chrome_executable(chrome)
105 .request_timeout(std::time::Duration::from_secs(3600));
107
108 if !headless {
109 config_builder = config_builder.with_head();
110 }
111
112 let config = config_builder
113 .build()
114 .map_err(|e| anyhow::anyhow!("browser config error: {e}"))?;
115
116 let (browser, handler) = Browser::launch(config).await.context("launching Chrome")?;
117
118 spawn_handler(handler);
119
120 let ws_url = browser.websocket_address().clone();
121 Ok((browser, ws_url))
122}
123
124pub async fn connect_chrome(ws_url: &str) -> Result<Browser> {
128 let (browser, handler) = Browser::connect(ws_url)
129 .await
130 .context("connecting to Chrome CDP")?;
131
132 spawn_handler(handler);
133
134 Ok(browser)
135}
136
137pub async fn configure_page(page: &Page) -> Result<()> {
142 use chromiumoxide::cdp::browser_protocol::browser::{
143 SetDownloadBehaviorBehavior, SetDownloadBehaviorParams,
144 };
145
146 let params = SetDownloadBehaviorParams::new(SetDownloadBehaviorBehavior::Deny);
148
149 page.execute(params)
150 .await
151 .context("setting download behavior to deny")?;
152
153 Ok(())
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn chrome_binary_candidates_non_empty() {
162 let candidates = chrome_binary_candidates();
163 assert!(!candidates.is_empty());
164 }
165
166 #[test]
167 fn find_chrome_returns_result() {
168 let _ = find_chrome();
171 }
172}