agentchrome/chrome/
platform.rs1use std::path::PathBuf;
2
3use super::ChromeError;
4
5#[derive(Debug, Clone, Copy)]
7pub enum Channel {
8 Stable,
9 Canary,
10 Beta,
11 Dev,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ProbeResult {
20 Alive,
22 Dead,
24 Unknown,
26}
27
28#[must_use]
35pub fn is_process_alive(pid: u32) -> ProbeResult {
36 #[cfg(unix)]
37 {
38 #[allow(clippy::cast_possible_wrap)]
40 let pid_i32 = pid as i32;
41 let rc = unsafe { libc::kill(pid_i32, 0) };
44 if rc == 0 {
45 return ProbeResult::Alive;
46 }
47 let err = std::io::Error::last_os_error();
50 match err.raw_os_error() {
51 Some(libc::ESRCH) => ProbeResult::Dead,
52 Some(libc::EPERM) => ProbeResult::Alive, _ => ProbeResult::Unknown,
54 }
55 }
56 #[cfg(windows)]
57 {
58 let output = std::process::Command::new("tasklist")
59 .args(["/FI", &format!("PID eq {pid}"), "/NH"])
60 .output();
61 match output {
62 Ok(out) if out.status.success() => {
63 let stdout = String::from_utf8_lossy(&out.stdout);
64 if stdout.contains(&pid.to_string()) {
67 ProbeResult::Alive
68 } else {
69 ProbeResult::Dead
70 }
71 }
72 _ => ProbeResult::Unknown,
73 }
74 }
75 #[cfg(not(any(unix, windows)))]
76 {
77 let _ = pid;
78 ProbeResult::Unknown
79 }
80}
81
82pub fn find_chrome_executable(channel: Channel) -> Result<PathBuf, ChromeError> {
91 let env_override = std::env::var("CHROME_PATH").ok().map(PathBuf::from);
92 find_chrome_from(channel, env_override.as_deref())
93}
94
95fn find_chrome_from(
100 channel: Channel,
101 env_override: Option<&std::path::Path>,
102) -> Result<PathBuf, ChromeError> {
103 if let Some(p) = env_override
104 && p.exists()
105 {
106 return Ok(p.to_path_buf());
107 }
108
109 for candidate in chrome_candidates(channel) {
110 if candidate.exists() {
111 return Ok(candidate);
112 }
113 }
114
115 Err(ChromeError::NotFound(format!(
116 "could not find Chrome ({channel:?} channel). Use --chrome-path to specify the executable"
117 )))
118}
119
120#[must_use]
122pub fn default_user_data_dir() -> Option<PathBuf> {
123 #[cfg(target_os = "macos")]
124 {
125 home_dir().map(|h| h.join("Library/Application Support/Google/Chrome"))
126 }
127
128 #[cfg(target_os = "linux")]
129 {
130 home_dir().map(|h| h.join(".config/google-chrome"))
131 }
132
133 #[cfg(target_os = "windows")]
134 {
135 std::env::var("LOCALAPPDATA").ok().map(|d| {
136 PathBuf::from(d)
137 .join("Google")
138 .join("Chrome")
139 .join("User Data")
140 })
141 }
142
143 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
144 {
145 None
146 }
147}
148
149#[cfg(any(target_os = "macos", target_os = "linux"))]
150fn home_dir() -> Option<PathBuf> {
151 std::env::var("HOME").ok().map(PathBuf::from)
152}
153
154fn chrome_candidates(channel: Channel) -> Vec<PathBuf> {
156 #[cfg(target_os = "macos")]
157 {
158 macos_candidates(channel)
159 }
160
161 #[cfg(target_os = "linux")]
162 {
163 linux_candidates(channel)
164 }
165
166 #[cfg(target_os = "windows")]
167 {
168 windows_candidates(channel)
169 }
170
171 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
172 {
173 let _ = channel;
174 vec![]
175 }
176}
177
178#[cfg(target_os = "macos")]
179fn macos_candidates(channel: Channel) -> Vec<PathBuf> {
180 match channel {
181 Channel::Stable => vec![
182 PathBuf::from("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
183 PathBuf::from("/Applications/Chromium.app/Contents/MacOS/Chromium"),
184 ],
185 Channel::Canary => vec![PathBuf::from(
186 "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
187 )],
188 Channel::Beta => vec![PathBuf::from(
189 "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
190 )],
191 Channel::Dev => vec![PathBuf::from(
192 "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
193 )],
194 }
195}
196
197#[cfg(target_os = "linux")]
198fn linux_candidates(channel: Channel) -> Vec<PathBuf> {
199 let path_dirs: Vec<PathBuf> = std::env::var("PATH")
200 .unwrap_or_default()
201 .split(':')
202 .map(PathBuf::from)
203 .collect();
204
205 let names: &[&str] = match channel {
206 Channel::Stable => &[
207 "google-chrome",
208 "google-chrome-stable",
209 "chromium-browser",
210 "chromium",
211 ],
212 Channel::Canary => &["google-chrome-canary"],
213 Channel::Beta => &["google-chrome-beta"],
214 Channel::Dev => &["google-chrome-unstable"],
215 };
216
217 let mut candidates = Vec::new();
218 for name in names {
219 for dir in &path_dirs {
220 candidates.push(dir.join(name));
221 }
222 }
223 candidates
224}
225
226#[cfg(target_os = "windows")]
227fn windows_candidates(channel: Channel) -> Vec<PathBuf> {
228 let program_files = std::env::var("ProgramFiles").unwrap_or_default();
229 let program_files_x86 = std::env::var("ProgramFiles(x86)").unwrap_or_default();
230 let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
231
232 match channel {
233 Channel::Stable => vec![
234 PathBuf::from(&program_files).join("Google/Chrome/Application/chrome.exe"),
235 PathBuf::from(&program_files_x86).join("Google/Chrome/Application/chrome.exe"),
236 ],
237 Channel::Canary => {
238 vec![PathBuf::from(&local_app_data).join("Google/Chrome SxS/Application/chrome.exe")]
239 }
240 Channel::Beta => vec![
241 PathBuf::from(&program_files).join("Google/Chrome Beta/Application/chrome.exe"),
242 PathBuf::from(&program_files_x86).join("Google/Chrome Beta/Application/chrome.exe"),
243 ],
244 Channel::Dev => vec![
245 PathBuf::from(&program_files).join("Google/Chrome Dev/Application/chrome.exe"),
246 PathBuf::from(&program_files_x86).join("Google/Chrome Dev/Application/chrome.exe"),
247 ],
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn default_user_data_dir_returns_some() {
257 let dir = default_user_data_dir();
259 assert!(dir.is_some(), "Expected a default user data directory");
260 }
261
262 #[test]
263 fn chrome_candidates_is_not_empty() {
264 let candidates = chrome_candidates(Channel::Stable);
265 assert!(
266 !candidates.is_empty(),
267 "Expected at least one candidate path"
268 );
269 }
270
271 #[test]
272 fn chrome_path_override_existing_file() {
273 let exe = std::env::current_exe().unwrap();
275 let result = find_chrome_from(Channel::Stable, Some(&exe));
276 assert_eq!(result.unwrap(), exe);
277 }
278
279 #[test]
280 fn is_process_alive_self_is_alive() {
281 let me = std::process::id();
282 assert_eq!(is_process_alive(me), ProbeResult::Alive);
283 }
284
285 #[cfg(unix)]
286 #[test]
287 fn is_process_alive_high_pid_is_dead() {
288 assert_eq!(is_process_alive(999_999_999), ProbeResult::Dead);
292 }
293
294 #[test]
295 fn chrome_path_override_nonexistent_is_skipped() {
296 let fake = std::path::Path::new("/nonexistent/chrome-test-binary");
297 let result = find_chrome_from(Channel::Stable, Some(fake));
298 if let Ok(path) = &result {
301 assert_ne!(path.as_path(), fake);
302 }
303 }
304}