use std::path::PathBuf;
use crate::{Error, Result};
pub fn chrome_path() -> Result<PathBuf> {
for var in ["CHROME_BIN", "DRISSION_CHROME"] {
if let Some(v) = std::env::var_os(var) {
let pb = PathBuf::from(v);
if pb.is_file() {
return Ok(pb);
}
}
}
if let Some(p) = first_existing(install_candidates()) {
return Ok(p);
}
#[cfg(windows)]
if let Some(s) = from_registry() {
let pb = PathBuf::from(&s);
if pb.is_file() {
return Ok(pb);
}
}
if let Some(p) = from_path_env() {
return Ok(p);
}
Err(Error::msg(
"CDP: 未找到 Chrome/Edge/Brave/Chromium。请安装 Google Chrome,\
或用环境变量 CHROME_BIN / DRISSION_CHROME 指定浏览器可执行文件路径。",
))
}
fn install_candidates() -> Vec<PathBuf> {
let mut v = Vec::new();
#[cfg(target_os = "macos")]
{
for p in [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
] {
v.push(PathBuf::from(p));
}
}
#[cfg(target_os = "windows")]
{
let suffixes = [
r"Google\Chrome\Application\chrome.exe",
r"Google\Chrome Beta\Application\chrome.exe",
r"Chromium\Application\chrome.exe",
r"BraveSoftware\Brave-Browser\Application\brave.exe",
r"Microsoft\Edge\Application\msedge.exe",
];
for base_var in [
"LOCALAPPDATA",
"PROGRAMFILES",
"ProgramFiles(x86)",
"PROGRAMW6432",
] {
if let Some(base) = std::env::var_os(base_var) {
let base = PathBuf::from(base);
for s in suffixes {
v.push(base.join(s));
}
}
}
}
#[cfg(all(unix, not(target_os = "macos")))]
{
for p in [
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/opt/google/chrome/google-chrome",
"/opt/google/chrome/chrome",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
"/usr/bin/brave-browser",
"/usr/bin/microsoft-edge",
] {
v.push(PathBuf::from(p));
}
}
v
}
fn first_existing(paths: Vec<PathBuf>) -> Option<PathBuf> {
paths.into_iter().find(|p| p.is_file())
}
fn browser_exe_names() -> &'static [&'static str] {
#[cfg(windows)]
{
&["chrome.exe", "chromium.exe", "brave.exe", "msedge.exe"]
}
#[cfg(not(windows))]
{
&[
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
"brave-browser",
"microsoft-edge",
]
}
}
fn scan_dirs(dirs: impl IntoIterator<Item = PathBuf>, names: &[&str]) -> Option<PathBuf> {
for dir in dirs {
for name in names {
let cand = dir.join(name);
if cand.is_file() {
return Some(cand);
}
}
}
None
}
fn from_path_env() -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
scan_dirs(std::env::split_paths(&path), browser_exe_names())
}
#[cfg(windows)]
fn from_registry() -> Option<String> {
use windows_sys::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, RegCloseKey, RegOpenKeyExW,
RegQueryValueExW,
};
let subkey: Vec<u16> = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe"
.encode_utf16()
.chain(std::iter::once(0))
.collect();
for root in [HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE] {
unsafe {
let mut hkey: HKEY = std::ptr::null_mut();
if RegOpenKeyExW(root, subkey.as_ptr(), 0, KEY_READ, &mut hkey) != 0 {
continue;
}
let mut buf = [0u16; 1024];
let mut len: u32 = (buf.len() * 2) as u32; let st = RegQueryValueExW(
hkey,
std::ptr::null(), std::ptr::null_mut(), std::ptr::null_mut(), buf.as_mut_ptr() as *mut u8,
&mut len,
);
RegCloseKey(hkey);
if st == 0 && len >= 2 {
let count = (len as usize / 2).min(buf.len());
let s = String::from_utf16_lossy(&buf[..count]);
let s = s.trim_end_matches('\0').trim().trim_matches('"').trim();
if !s.is_empty() {
return Some(s.to_string());
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn install_candidates_nonempty_and_chromium_family() {
let c = install_candidates();
assert!(!c.is_empty(), "每个支持平台都应有候选安装路径");
assert!(
c.iter().any(|p| {
let s = p.to_string_lossy().to_lowercase();
s.contains("chrome")
|| s.contains("chromium")
|| s.contains("brave")
|| s.contains("edge")
}),
"候选路径应含 chromium 家族浏览器: {c:?}"
);
}
#[test]
fn browser_exe_names_prefers_chrome() {
let names = browser_exe_names();
assert!(!names.is_empty());
assert!(
names[0].contains("chrome"),
"PATH 扫描应优先 Google Chrome: {names:?}"
);
}
#[test]
fn scan_dirs_finds_created_file_and_skips_missing() {
let dir = std::env::temp_dir().join(format!(
"drission_locate_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&dir).expect("建临时目录");
let exe = dir.join("mybrowser-bin");
std::fs::write(&exe, b"x").expect("写临时可执行文件");
let found = scan_dirs(vec![dir.clone()], &["nope", "mybrowser-bin"]);
assert_eq!(found.as_deref(), Some(exe.as_path()));
assert!(scan_dirs(vec![dir.clone()], &["does-not-exist"]).is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn first_existing_returns_none_for_all_missing() {
let missing = vec![
PathBuf::from("/no/such/chrome/binary/xyz"),
PathBuf::from("/another/missing/edge"),
];
assert!(first_existing(missing).is_none());
}
}