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:
Chris Tate
2026-03-30 12:37:01 -05:00
committed by GitHub
parent 312db04e5e
commit 8d78fcbbb3
9 changed files with 640 additions and 12 deletions
+67 -8
View File
@@ -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() {
+98 -4
View File
@@ -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> {