diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 870f7cd..33eb95a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/cli/src/connection.rs b/cli/src/connection.rs index dc4de8c..6405985 100644 --- a/cli/src/connection.rs +++ b/cli/src/connection.rs @@ -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 { diff --git a/cli/src/main.rs b/cli/src/main.rs index c56a423..fa1550e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -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 = env::args().skip(1).collect(); let flags = parse_flags(&args); let clean = clean_args(&args); diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 6301cf8..8cf2a88 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -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)'); }