fix: Windows Chrome extraction and debugging environment (#1088)
* windows debugging
* fixes
* fixes
* fix: handle Windows path separators in Chrome zip extraction
The zip crate's enclosed_name() normalizes paths to use backslashes on
Windows, but extract_zip used split_once('/') which only matches forward
slashes. This caused Chrome to be extracted into a nested chrome-win64/
subdirectory instead of directly into the version directory.
Also adds debug diagnostics to find_installed_chrome() (gated behind
AGENT_BROWSER_DEBUG) and better error messages when Chrome cache exists
but no binary is found.
Fixes #1076
* feat: add Puppeteer browser cache as Chrome fallback
Search ~/.cache/puppeteer/chrome/ (or PUPPETEER_CACHE_DIR) for Chrome
binaries before falling back to Playwright's cache. Puppeteer v19+
stores Chrome for Testing in this location, so users with an existing
Puppeteer install can use agent-browser without a separate install step.
* fmt
This commit is contained in:
+67
-8
@@ -16,30 +16,85 @@ pub fn get_browsers_dir() -> PathBuf {
|
||||
|
||||
pub fn find_installed_chrome() -> Option<PathBuf> {
|
||||
let browsers_dir = get_browsers_dir();
|
||||
let debug = std::env::var("AGENT_BROWSER_DEBUG").is_ok();
|
||||
|
||||
if debug {
|
||||
let _ = writeln!(
|
||||
io::stderr(),
|
||||
"[chrome-search] home_dir={:?} browsers_dir={}",
|
||||
dirs::home_dir(),
|
||||
browsers_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
if !browsers_dir.exists() {
|
||||
if debug {
|
||||
let _ = writeln!(io::stderr(), "[chrome-search] browsers_dir does not exist");
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut versions: Vec<_> = fs::read_dir(&browsers_dir)
|
||||
.ok()?
|
||||
let entries = match fs::read_dir(&browsers_dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
let _ = writeln!(
|
||||
io::stderr(),
|
||||
"Warning: cannot read Chrome cache directory {}: {}",
|
||||
browsers_dir.display(),
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut versions: Vec<_> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.file_name()
|
||||
let matches = e
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_some_and(|n| n.starts_with("chrome-"))
|
||||
.is_some_and(|n| n.starts_with("chrome-"));
|
||||
if debug {
|
||||
let _ = writeln!(
|
||||
io::stderr(),
|
||||
"[chrome-search] entry {:?} matches={}",
|
||||
e.file_name(),
|
||||
matches
|
||||
);
|
||||
}
|
||||
matches
|
||||
})
|
||||
.collect();
|
||||
|
||||
versions.sort_by_key(|b| std::cmp::Reverse(b.file_name()));
|
||||
|
||||
for entry in versions {
|
||||
if let Some(bin) = chrome_binary_in_dir(&entry.path()) {
|
||||
if bin.exists() {
|
||||
let dir = entry.path();
|
||||
if let Some(bin) = chrome_binary_in_dir(&dir) {
|
||||
let exists = bin.exists();
|
||||
if debug {
|
||||
let _ = writeln!(
|
||||
io::stderr(),
|
||||
"[chrome-search] candidate {} exists={}",
|
||||
bin.display(),
|
||||
exists
|
||||
);
|
||||
}
|
||||
if exists {
|
||||
return Some(bin);
|
||||
}
|
||||
} else if debug {
|
||||
let _ = writeln!(
|
||||
io::stderr(),
|
||||
"[chrome-search] no binary found in {}",
|
||||
dir.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
let _ = writeln!(io::stderr(), "[chrome-search] no installed Chrome found");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -225,10 +280,14 @@ fn extract_zip(bytes: Vec<u8>, dest: &Path) -> Result<(), String> {
|
||||
None => continue,
|
||||
};
|
||||
let raw_name = enclosed.to_string_lossy().to_string();
|
||||
// Strip the top-level "chrome-<platform>/" directory from zip entries.
|
||||
// On Windows, enclosed_name() normalizes paths to backslashes, so we
|
||||
// must split on either separator.
|
||||
let rel_path = raw_name
|
||||
.strip_prefix("chrome-")
|
||||
.and_then(|s| s.split_once('/'))
|
||||
.map(|(_, rest)| rest.to_string())
|
||||
.and_then(|s| s.find(['/', '\\']).map(|i| &s[i + 1..]))
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or(raw_name.clone());
|
||||
|
||||
if rel_path.is_empty() {
|
||||
|
||||
@@ -219,9 +219,18 @@ fn build_chrome_args(options: &LaunchOptions) -> Result<ChromeArgs, String> {
|
||||
pub fn launch_chrome(options: &LaunchOptions) -> Result<ChromeProcess, String> {
|
||||
let chrome_path = match &options.executable_path {
|
||||
Some(p) => PathBuf::from(p),
|
||||
None => {
|
||||
find_chrome().ok_or("Chrome not found. Run `agent-browser install` to download Chrome, or use --executable-path.")?
|
||||
}
|
||||
None => find_chrome().ok_or_else(|| {
|
||||
let cache_dir = crate::install::get_browsers_dir();
|
||||
format!(
|
||||
"Chrome not found. Checked:\n \
|
||||
- agent-browser cache: {}\n \
|
||||
- System Chrome installations\n \
|
||||
- Puppeteer browser cache\n \
|
||||
- Playwright browser cache\n\
|
||||
Run `agent-browser install` to download Chrome, or use --executable-path.",
|
||||
cache_dir.display()
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let max_attempts = 3;
|
||||
@@ -438,6 +447,18 @@ pub fn find_chrome() -> Option<PathBuf> {
|
||||
return Some(p);
|
||||
}
|
||||
|
||||
// If the cache directory exists but no Chrome was found, warn -- this
|
||||
// likely means the cache is corrupted or the directory layout is unexpected.
|
||||
let cache_dir = crate::install::get_browsers_dir();
|
||||
if cache_dir.exists() {
|
||||
let _ = writeln!(
|
||||
std::io::stderr(),
|
||||
"Warning: Chrome cache directory exists ({}) but no Chrome binary found inside. \
|
||||
Falling back to system Chrome. Run `agent-browser install` to re-download.",
|
||||
cache_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check system-installed Chrome
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -502,7 +523,10 @@ pub fn find_chrome() -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback: check Playwright's browser cache (for existing installs)
|
||||
// 3. Fallback: check Puppeteer / Playwright browser caches
|
||||
if let Some(p) = find_puppeteer_chrome() {
|
||||
return Some(p);
|
||||
}
|
||||
if let Some(p) = find_playwright_chromium() {
|
||||
return Some(p);
|
||||
}
|
||||
@@ -684,6 +708,76 @@ fn should_disable_dev_shm(existing_args: &[String]) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Search Puppeteer's browser cache for a Chrome binary.
|
||||
/// Puppeteer v19+ stores Chrome in ~/.cache/puppeteer/chrome/<platform>-<version>/
|
||||
fn find_puppeteer_chrome() -> Option<PathBuf> {
|
||||
let mut search_dirs = Vec::new();
|
||||
|
||||
if let Ok(custom) = std::env::var("PUPPETEER_CACHE_DIR") {
|
||||
search_dirs.push(PathBuf::from(custom).join("chrome"));
|
||||
}
|
||||
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
search_dirs.push(home.join(".cache/puppeteer/chrome"));
|
||||
}
|
||||
|
||||
for dir in &search_dirs {
|
||||
if !dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
let mut matches: Vec<PathBuf> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().is_dir())
|
||||
.filter_map(|e| {
|
||||
let candidate = build_puppeteer_binary_path(&e.path());
|
||||
if candidate.exists() {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
matches.sort();
|
||||
matches.reverse();
|
||||
if let Some(p) = matches.into_iter().next() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||
version_dir.join("chrome-linux64/chrome")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||
// Puppeteer uses chrome-mac-arm64 or chrome-mac-x64 depending on arch
|
||||
let arm = version_dir.join(
|
||||
"chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
||||
);
|
||||
if arm.exists() {
|
||||
return arm;
|
||||
}
|
||||
version_dir.join(
|
||||
"chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||
version_dir.join(r"chrome-win64\chrome.exe")
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
fn build_puppeteer_binary_path(version_dir: &Path) -> PathBuf {
|
||||
version_dir.join("chrome")
|
||||
}
|
||||
|
||||
/// Search Playwright's browser cache for a Chromium binary.
|
||||
/// Legacy fallback for users who previously installed Chromium via Playwright.
|
||||
fn find_playwright_chromium() -> Option<PathBuf> {
|
||||
|
||||
Reference in New Issue
Block a user