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:
@@ -173,6 +173,23 @@ jobs:
|
|||||||
}
|
}
|
||||||
shell: pwsh
|
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:
|
serverless-chromium:
|
||||||
name: Serverless Chromium (@sparticuz/chromium)
|
name: Serverless Chromium (@sparticuz/chromium)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
+30
-8
@@ -355,6 +355,17 @@ pub fn ensure_daemon(
|
|||||||
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
|
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
|
||||||
// Canonicalize to resolve symlinks (e.g., npm global bin symlink -> actual binary)
|
// Canonicalize to resolve symlinks (e.g., npm global bin symlink -> actual binary)
|
||||||
let exe_path = exe_path.canonicalize().unwrap_or(exe_path);
|
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 exe_dir = exe_path.parent().unwrap();
|
||||||
|
|
||||||
let mut daemon_paths = vec![
|
let mut daemon_paths = vec![
|
||||||
@@ -404,10 +415,11 @@ pub fn ensure_daemon(
|
|||||||
{
|
{
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
// On Windows, call node directly. Command::new handles PATH resolution (node.exe or node.cmd)
|
// Use node.exe explicitly to avoid Git Bash/MSYS2 shell wrapper resolution
|
||||||
// and automatically quotes arguments containing spaces.
|
let mut cmd = Command::new("node.exe");
|
||||||
let mut cmd = Command::new("node");
|
cmd.arg(daemon_path)
|
||||||
cmd.arg(daemon_path);
|
.env("MSYS_NO_PATHCONV", "1")
|
||||||
|
.env("MSYS2_ARG_CONV_EXCL", "*");
|
||||||
apply_daemon_env(&mut cmd, session, opts);
|
apply_daemon_env(&mut cmd, session, opts);
|
||||||
|
|
||||||
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||||
@@ -431,10 +443,20 @@ pub fn ensure_daemon(
|
|||||||
thread::sleep(Duration::from_millis(100));
|
thread::sleep(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(format!(
|
#[cfg(unix)]
|
||||||
"Daemon failed to start (socket: {})",
|
let endpoint_info = format!(
|
||||||
get_socket_dir().join(format!("{}.sock", session)).display()
|
"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> {
|
fn connect(session: &str) -> Result<Connection, String> {
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ use std::process::Command as ProcessCommand;
|
|||||||
fn run_auth_cli(cmd: &serde_json::Value, json_mode: bool) -> ! {
|
fn run_auth_cli(cmd: &serde_json::Value, json_mode: bool) -> ! {
|
||||||
let exe_path = env::current_exe().unwrap_or_default();
|
let exe_path = env::current_exe().unwrap_or_default();
|
||||||
let exe_path = exe_path.canonicalize().unwrap_or(exe_path);
|
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 exe_dir = exe_path.parent().unwrap_or(std::path::Path::new("."));
|
||||||
|
|
||||||
let mut script_paths = vec![
|
let mut script_paths = vec![
|
||||||
@@ -233,6 +242,13 @@ fn main() {
|
|||||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
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 args: Vec<String> = env::args().skip(1).collect();
|
||||||
let flags = parse_flags(&args);
|
let flags = parse_flags(&args);
|
||||||
let clean = clean_args(&args);
|
let clean = clean_args(&args);
|
||||||
|
|||||||
+14
-18
@@ -187,46 +187,42 @@ async function fixUnixSymlink() {
|
|||||||
* We overwrite them to invoke the native .exe directly.
|
* We overwrite them to invoke the native .exe directly.
|
||||||
*/
|
*/
|
||||||
async function fixWindowsShims() {
|
async function fixWindowsShims() {
|
||||||
// Check if this is a global install by looking for npm's global prefix
|
|
||||||
let npmBinDir;
|
let npmBinDir;
|
||||||
try {
|
try {
|
||||||
npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
npmBinDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
|
||||||
} catch {
|
} 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 cmdShim = join(npmBinDir, 'agent-browser.cmd');
|
||||||
const ps1Shim = join(npmBinDir, 'agent-browser.ps1');
|
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)) {
|
if (!existsSync(cmdShim)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path to native binary relative to npm prefix
|
// Detect architecture so ARM64 Windows is handled correctly
|
||||||
const relativeBinaryPath = 'node_modules\\agent-browser\\bin\\agent-browser-win32-x64.exe';
|
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 {
|
try {
|
||||||
// Overwrite .cmd shim
|
|
||||||
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
|
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
|
||||||
writeFileSync(cmdShim, cmdContent);
|
writeFileSync(cmdShim, cmdContent);
|
||||||
|
|
||||||
// Overwrite .ps1 shim
|
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`;
|
||||||
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
|
|
||||||
`;
|
|
||||||
writeFileSync(ps1Shim, ps1Content);
|
writeFileSync(ps1Shim, ps1Content);
|
||||||
|
|
||||||
console.log('✓ Optimized: shims point to native binary (zero overhead)');
|
console.log('✓ Optimized: shims point to native binary (zero overhead)');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Permission error or other issue - not critical, JS wrapper still works
|
|
||||||
console.log(`⚠ Could not optimize shims: ${err.message}`);
|
console.log(`⚠ Could not optimize shims: ${err.message}`);
|
||||||
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user