fix(windows): resolve daemon startup failures and Git Bash compatibility (#582)

* fix(windows): resolve daemon startup failures and Git Bash compatibility

Three root causes behind 27 open Windows issues:

1. Path::canonicalize() returns \\?\ prefixed paths on Windows that
   Node.js cannot parse, preventing daemon startup. Strip the prefix
   before passing to Node. (fixes #522, #390, #56, #25, #37, #89)

2. Git Bash/MSYS2 translates Unix-style paths and resolves node to
   a shell wrapper script. Use node.exe explicitly and set
   MSYS_NO_PATHCONV/MSYS2_ARG_CONV_EXCL to prevent argument mangling.
   (fixes #148, #108, #171)

3. postinstall fixWindowsShims() hardcoded x64 arch and did not verify
   the native binary exists before rewriting shims. Now detects arch
   dynamically and validates the binary path. (fixes #262)

Also:
- Error messages now show TCP port on Windows instead of Unix socket path
- Windows CI expanded to test full daemon lifecycle (open, snapshot, close)

* fix(windows): strip \\?\ prefix in auth-cli path (fixes #579)

Same canonicalize() issue as the daemon spawn path, but in
run_auth_cli() which passes the script path to Node.js.
This commit is contained in:
Chris Tate
2026-03-03 08:00:14 -06:00
committed by GitHub
parent d97e2016f5
commit c6a33b6338
4 changed files with 77 additions and 26 deletions
+17
View File
@@ -173,6 +173,23 @@ jobs:
}
shell: pwsh
- name: Test daemon lifecycle (open, snapshot, close)
run: |
$env:PATH = "$pwd\bin;$env:PATH"
Write-Host "--- Opening page ---"
bin/agent-browser-win32-x64.exe open https://example.com
if ($LASTEXITCODE -ne 0) { Write-Error "open failed"; exit 1 }
Write-Host "--- Taking snapshot ---"
$snapshot = bin/agent-browser-win32-x64.exe snapshot
if ($LASTEXITCODE -ne 0) { Write-Error "snapshot failed"; exit 1 }
Write-Host $snapshot
Write-Host "--- Closing browser ---"
bin/agent-browser-win32-x64.exe close
if ($LASTEXITCODE -ne 0) { Write-Error "close failed"; exit 1 }
Write-Host "--- Windows daemon lifecycle test passed ---"
shell: pwsh
timeout-minutes: 5
serverless-chromium:
name: Serverless Chromium (@sparticuz/chromium)
runs-on: ubuntu-latest
+30 -8
View File
@@ -355,6 +355,17 @@ pub fn ensure_daemon(
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
// Canonicalize to resolve symlinks (e.g., npm global bin symlink -> actual binary)
let exe_path = exe_path.canonicalize().unwrap_or(exe_path);
// On Windows, canonicalize() returns \\?\ prefixed extended-length paths.
// Node.js cannot handle these, so strip the prefix.
#[cfg(windows)]
let exe_path = {
let p = exe_path.to_string_lossy();
if let Some(stripped) = p.strip_prefix(r"\\?\") {
PathBuf::from(stripped)
} else {
exe_path
}
};
let exe_dir = exe_path.parent().unwrap();
let mut daemon_paths = vec![
@@ -404,10 +415,11 @@ pub fn ensure_daemon(
{
use std::os::windows::process::CommandExt;
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
// and automatically quotes arguments containing spaces.
let mut cmd = Command::new("node");
cmd.arg(daemon_path);
// Use node.exe explicitly to avoid Git Bash/MSYS2 shell wrapper resolution
let mut cmd = Command::new("node.exe");
cmd.arg(daemon_path)
.env("MSYS_NO_PATHCONV", "1")
.env("MSYS2_ARG_CONV_EXCL", "*");
apply_daemon_env(&mut cmd, session, opts);
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
@@ -431,10 +443,20 @@ pub fn ensure_daemon(
thread::sleep(Duration::from_millis(100));
}
Err(format!(
"Daemon failed to start (socket: {})",
get_socket_dir().join(format!("{}.sock", session)).display()
))
#[cfg(unix)]
let endpoint_info = format!(
"socket: {}",
get_socket_dir()
.join(format!("{}.sock", session))
.display()
);
#[cfg(windows)]
let endpoint_info = format!(
"port: 127.0.0.1:{}",
get_port_for_session(session)
);
Err(format!("Daemon failed to start ({})", endpoint_info))
}
fn connect(session: &str) -> Result<Connection, String> {
+16
View File
@@ -31,6 +31,15 @@ use std::process::Command as ProcessCommand;
fn run_auth_cli(cmd: &serde_json::Value, json_mode: bool) -> ! {
let exe_path = env::current_exe().unwrap_or_default();
let exe_path = exe_path.canonicalize().unwrap_or(exe_path);
#[cfg(windows)]
let exe_path = {
let p = exe_path.to_string_lossy();
if let Some(stripped) = p.strip_prefix(r"\\?\") {
PathBuf::from(stripped)
} else {
exe_path
}
};
let exe_dir = exe_path.parent().unwrap_or(std::path::Path::new("."));
let mut script_paths = vec![
@@ -233,6 +242,13 @@ fn main() {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
// Prevent MSYS/Git Bash path translation from mangling arguments
#[cfg(windows)]
{
env::set_var("MSYS_NO_PATHCONV", "1");
env::set_var("MSYS2_ARG_CONV_EXCL", "*");
}
let args: Vec<String> = env::args().skip(1).collect();
let flags = parse_flags(&args);
let clean = clean_args(&args);
+14 -18
View File
@@ -187,46 +187,42 @@ async function fixUnixSymlink() {
* We overwrite them to invoke the native .exe directly.
*/
async function fixWindowsShims() {
// Check if this is a global install by looking for npm's global prefix
let npmBinDir;
try {
npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
} catch {
return; // Not a global install or npm not available
return;
}
// The shims are in the npm prefix directory (not prefix/bin on Windows)
const cmdShim = join(npmBinDir, 'agent-browser.cmd');
const ps1Shim = join(npmBinDir, 'agent-browser.ps1');
// Only fix if shims exist (indicates global install)
// Shims may not exist yet during postinstall (npm creates them after
// lifecycle scripts). If missing, fall back: the JS wrapper at
// bin/agent-browser.js handles Windows correctly via child_process.spawn.
if (!existsSync(cmdShim)) {
return;
}
// Path to native binary relative to npm prefix
const relativeBinaryPath = 'node_modules\\agent-browser\\bin\\agent-browser-win32-x64.exe';
// Detect architecture so ARM64 Windows is handled correctly
const cpuArch = arch() === 'arm64' ? 'arm64' : 'x64';
const relativeBinaryPath = `node_modules\\agent-browser\\bin\\agent-browser-win32-${cpuArch}.exe`;
const absoluteBinaryPath = join(npmBinDir, relativeBinaryPath);
// Only rewrite shims if the native binary actually exists
if (!existsSync(absoluteBinaryPath)) {
return;
}
try {
// Overwrite .cmd shim
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
writeFileSync(cmdShim, cmdContent);
// Overwrite .ps1 shim
const ps1Content = `#!/usr/bin/env pwsh
$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe = ""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
$exe = ".exe"
}
& "$basedir/${relativeBinaryPath.replace(/\\/g, '/')}" $args
exit $LASTEXITCODE
`;
const ps1Content = `#!/usr/bin/env pwsh\r\n$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent\r\n& "$basedir\\${relativeBinaryPath}" $args\r\nexit $LASTEXITCODE\r\n`;
writeFileSync(ps1Shim, ps1Content);
console.log('✓ Optimized: shims point to native binary (zero overhead)');
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize shims: ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
}