Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 65 additions & 22 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,36 @@ fn get_port_for_session(session: &str) -> u16 {
49152 + ((hash.unsigned_abs() as u32 % 16383) as u16)
}

#[cfg(windows)]
fn read_port_file(session: &str) -> Option<u16> {
// On Windows the daemon writes the actual port it bound to.
// This lets the CLI connect even if the hashed port was unavailable.
let port_path = get_port_path(session);
let port_str = fs::read_to_string(port_path).ok()?;
port_str.trim().parse::<u16>().ok()
}

#[cfg(windows)]
fn get_session_port(session: &str) -> u16 {
// Prefer the port file if present, otherwise fall back to the hash.
read_port_file(session).unwrap_or_else(|| get_port_for_session(session))
}

fn escape_powershell_single_quotes(value: &str) -> String {
value.replace('\'', "''")
}

#[cfg(windows)]
fn normalize_windows_path(path: &PathBuf) -> String {
// Start-Process can fail with "\\?\"-prefixed paths ("Windows cannot access \\\?\").
// Strip the prefix to keep PowerShell and Explorer happy.
let path_str = path.to_string_lossy().into_owned();
path_str
.strip_prefix(r"\\?\")
.unwrap_or(&path_str)
.to_string()
}

#[cfg(unix)]
fn is_daemon_running(session: &str) -> bool {
let pid_path = get_pid_path(session);
Expand All @@ -172,10 +202,10 @@ fn is_daemon_running(session: &str) -> bool {
if !pid_path.exists() {
return false;
}
let port = get_port_for_session(session);
let port = get_session_port(session);
TcpStream::connect_timeout(
&format!("127.0.0.1:{}", port).parse().unwrap(),
Duration::from_millis(100),
Duration::from_millis(200),
)
.is_ok()
}
Expand All @@ -188,10 +218,10 @@ fn daemon_ready(session: &str) -> bool {
}
#[cfg(windows)]
{
let port = get_port_for_session(session);
let port = get_session_port(session);
TcpStream::connect_timeout(
&format!("127.0.0.1:{}", port).parse().unwrap(),
Duration::from_millis(50),
Duration::from_millis(200),
)
.is_ok()
}
Expand Down Expand Up @@ -376,14 +406,20 @@ pub fn ensure_daemon(

#[cfg(windows)]
{
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)
.env("AGENT_BROWSER_DAEMON", "1")
.env("AGENT_BROWSER_SESSION", session);
// On Windows, spawn via PowerShell Start-Process for reliability.
let node_exe = "node";
let daemon_arg = escape_powershell_single_quotes(&normalize_windows_path(daemon_path));
let ps_cmd = format!(
"Start-Process -FilePath '{}' -ArgumentList @('{}') -WindowStyle Hidden",
node_exe, daemon_arg
);
let mut cmd = Command::new("powershell");
cmd.arg("-NoProfile")
.arg("-NonInteractive")
.arg("-Command")
.arg(ps_cmd)
.env("AGENT_BROWSER_SESSION", session)
.env("AGENT_BROWSER_DAEMON", "1");

if headed {
cmd.env("AGENT_BROWSER_HEADED", "1");
Expand Down Expand Up @@ -436,17 +472,24 @@ pub fn ensure_daemon(
if let Some(d) = device {
cmd.env("AGENT_BROWSER_IOS_DEVICE", d);
}

// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008;

cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
let output = cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| format!("Failed to start daemon: {}", e))?;

if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let msg = format!(
"Failed to start daemon (powershell). stdout: {} stderr: {}",
stdout.trim(),
stderr.trim()
);
return Err(msg);
}

}

for _ in 0..50 {
Expand Down Expand Up @@ -474,7 +517,7 @@ fn connect(session: &str) -> Result<Connection, String> {
}
#[cfg(windows)]
{
let port = get_port_for_session(session);
let port = get_session_port(session);
TcpStream::connect(format!("127.0.0.1:{}", port))
.map(Connection::Tcp)
.map_err(|e| format!("Failed to connect: {}", e))
Expand Down
43 changes: 32 additions & 11 deletions src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,28 +371,49 @@ export async function startDaemon(options?: {
// Write PID file before listening
fs.writeFileSync(pidFile, process.pid.toString());

const handleServerError = (err: NodeJS.ErrnoException): void => {
console.error('Server error:', err);
cleanupSocket();
process.exit(1);
};

if (isWindows) {
// Windows: use TCP socket on localhost
const port = getPortForSession(currentSession);
const portFile = getPortFile();
fs.writeFileSync(portFile, port.toString());
server.listen(port, '127.0.0.1', () => {
// Daemon is ready on TCP port
});
const preferredPort = getPortForSession(currentSession);

const listenOnPort = (port: number, allowFallback: boolean): void => {
// Try the hashed port first; if unavailable, retry with an OS-assigned port.
server.once('error', (err: NodeJS.ErrnoException) => {
if (allowFallback && (err.code === 'EADDRINUSE' || err.code === 'EACCES')) {
listenOnPort(0, false);
return;
}
handleServerError(err);
});

server.listen(port, '127.0.0.1', () => {
const address = server.address();
const actualPort = typeof address === 'string' ? parseInt(address, 10) : address?.port;
if (typeof actualPort === 'number') {
// Persist the actual port so the CLI can discover it reliably.
fs.writeFileSync(portFile, actualPort.toString());
}
server.on('error', handleServerError);
// Daemon is ready on TCP port
});
};

listenOnPort(preferredPort, true);
} else {
// Unix: use Unix domain socket
const socketPath = getSocketPath();
server.listen(socketPath, () => {
// Daemon is ready
});
server.on('error', handleServerError);
}

server.on('error', (err) => {
console.error('Server error:', err);
cleanupSocket();
process.exit(1);
});

// Handle shutdown signals
const shutdown = async () => {
if (shuttingDown) return;
Expand Down