From 36b1af29f674bbdec92e922b162eb7e3100ddb76 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 11 Jan 2026 09:54:51 -0600 Subject: [PATCH] windows --- AGENTS.md | 4 + cli/Cargo.toml | 2 + cli/src/main.rs | 175 +++++++++++++++++++++++++++++++++----- docker/Dockerfile.build | 5 +- docker/docker-compose.yml | 25 +++++- package.json | 3 +- src/daemon.ts | 72 ++++++++++++++-- 7 files changed, 253 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5548ea1..077a2fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,10 @@ Instructions for AI coding agents working with this codebase. +## Code Style + +- Do not use emojis in code, output, or documentation. Unicode symbols (✓, ✗, →, ⚠) are acceptable. + ## Source Code Reference diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8ba543a..b81b4a2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -8,6 +8,8 @@ license = "Apache-2.0" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" + +[target.'cfg(unix)'.dependencies] libc = "0.2" [profile.release] diff --git a/cli/src/main.rs b/cli/src/main.rs index c51db40..535ce22 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,14 +2,19 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::env; use std::fs; -use std::io::{BufRead, BufReader, Write}; -use std::os::unix::net::UnixStream; +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::TcpStream; use std::path::PathBuf; use std::process::{exit, Command, Stdio}; use std::thread; use std::time::Duration; +// Unix socket support (Unix only) +#[cfg(unix)] +use std::os::unix::net::UnixStream; + #[derive(Serialize)] +#[allow(dead_code)] struct Request { id: String, action: String, @@ -24,6 +29,61 @@ struct Response { error: Option, } +// Connection type abstraction +#[allow(dead_code)] +enum Connection { + #[cfg(unix)] + Unix(UnixStream), + Tcp(TcpStream), +} + +impl Read for Connection { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + #[cfg(unix)] + Connection::Unix(s) => s.read(buf), + Connection::Tcp(s) => s.read(buf), + } + } +} + +impl Write for Connection { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match self { + #[cfg(unix)] + Connection::Unix(s) => s.write(buf), + Connection::Tcp(s) => s.write(buf), + } + } + + fn flush(&mut self) -> std::io::Result<()> { + match self { + #[cfg(unix)] + Connection::Unix(s) => s.flush(), + Connection::Tcp(s) => s.flush(), + } + } +} + +impl Connection { + fn set_read_timeout(&self, dur: Option) -> std::io::Result<()> { + match self { + #[cfg(unix)] + Connection::Unix(s) => s.set_read_timeout(dur), + Connection::Tcp(s) => s.set_read_timeout(dur), + } + } + + fn set_write_timeout(&self, dur: Option) -> std::io::Result<()> { + match self { + #[cfg(unix)] + Connection::Unix(s) => s.set_write_timeout(dur), + Connection::Tcp(s) => s.set_write_timeout(dur), + } + } +} + +#[cfg(unix)] fn get_socket_path(session: &str) -> PathBuf { let tmp = env::temp_dir(); tmp.join(format!("agent-browser-{}.sock", session)) @@ -34,6 +94,25 @@ fn get_pid_path(session: &str) -> PathBuf { tmp.join(format!("agent-browser-{}.pid", session)) } +#[cfg(windows)] +fn get_port_path(session: &str) -> PathBuf { + let tmp = env::temp_dir(); + tmp.join(format!("agent-browser-{}.port", session)) +} + +/// Get port number for TCP mode (Windows) +/// Uses a hash of the session name to get a consistent port +#[cfg(windows)] +fn get_port_for_session(session: &str) -> u16 { + let mut hash: i32 = 0; + for c in session.chars() { + hash = ((hash << 5).wrapping_sub(hash)).wrapping_add(c as i32); + } + // Port range 49152-65535 (dynamic/private ports) + 49152 + ((hash.abs() as u16) % 16383) +} + +#[cfg(unix)] fn is_daemon_running(session: &str) -> bool { let pid_path = get_pid_path(session); if !pid_path.exists() { @@ -49,10 +128,38 @@ fn is_daemon_running(session: &str) -> bool { false } +#[cfg(windows)] +fn is_daemon_running(session: &str) -> bool { + let pid_path = get_pid_path(session); + if !pid_path.exists() { + return false; + } + // On Windows, try to connect to the port to check if daemon is running + let port = get_port_for_session(session); + TcpStream::connect_timeout( + &format!("127.0.0.1:{}", port).parse().unwrap(), + Duration::from_millis(100) + ).is_ok() +} + +fn daemon_ready(session: &str) -> bool { + #[cfg(unix)] + { + get_socket_path(session).exists() + } + #[cfg(windows)] + { + // On Windows, try to connect to verify daemon is ready + let port = get_port_for_session(session); + TcpStream::connect_timeout( + &format!("127.0.0.1:{}", port).parse().unwrap(), + Duration::from_millis(50) + ).is_ok() + } +} + fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> { - let socket_path = get_socket_path(session); - - if is_daemon_running(session) && socket_path.exists() { + if is_daemon_running(session) && daemon_ready(session) { return Ok(()); } @@ -86,7 +193,7 @@ fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> { .map_err(|e| format!("Failed to start daemon: {}", e))?; for _ in 0..50 { - if socket_path.exists() { + if daemon_ready(session) { return Ok(()); } thread::sleep(Duration::from_millis(100)); @@ -95,10 +202,25 @@ fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> { Err("Daemon failed to start".to_string()) } +fn connect(session: &str) -> Result { + #[cfg(unix)] + { + let socket_path = get_socket_path(session); + UnixStream::connect(&socket_path) + .map(Connection::Unix) + .map_err(|e| format!("Failed to connect: {}", e)) + } + #[cfg(windows)] + { + let port = get_port_for_session(session); + TcpStream::connect(format!("127.0.0.1:{}", port)) + .map(Connection::Tcp) + .map_err(|e| format!("Failed to connect: {}", e)) + } +} + fn send_command(cmd: Value, session: &str) -> Result { - let socket_path = get_socket_path(session); - let mut stream = UnixStream::connect(&socket_path) - .map_err(|e| format!("Failed to connect: {}", e))?; + let mut stream = connect(session)?; stream.set_read_timeout(Some(Duration::from_secs(30))).ok(); stream.set_write_timeout(Some(Duration::from_secs(5))).ok(); @@ -166,7 +288,7 @@ fn clean_args(args: &[String]) -> Vec { let mut result = Vec::new(); let mut skip_next = false; - for (i, arg) in args.iter().enumerate() { + for (_i, arg) in args.iter().enumerate() { if skip_next { skip_next = false; continue; @@ -491,7 +613,7 @@ fn print_response(resp: &Response, json_mode: bool) { } if !resp.success { - eprintln!("\x1b[31m✗ Error:\x1b[0m {}", resp.error.as_deref().unwrap_or("Unknown error")); + eprintln!("\x1b[31m✗\x1b[0m {}", resp.error.as_deref().unwrap_or("Unknown error")); return; } @@ -804,13 +926,26 @@ fn run_install(with_deps: bool) { } fn which_exists(cmd: &str) -> bool { - Command::new("which") - .arg(cmd) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) + #[cfg(unix)] + { + Command::new("which") + .arg(cmd) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } + #[cfg(windows)] + { + Command::new("where") + .arg(cmd) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + } } fn main() { @@ -843,7 +978,7 @@ fn main() { if flags.json { println!(r#"{{"success":false,"error":"{}"}}"#, e); } else { - eprintln!("\x1b[31m✗ Error:\x1b[0m {}", e); + eprintln!("\x1b[31m✗\x1b[0m {}", e); } exit(1); } @@ -870,7 +1005,7 @@ fn main() { if flags.json { println!(r#"{{"success":false,"error":"{}"}}"#, e); } else { - eprintln!("\x1b[31m✗ Error:\x1b[0m {}", e); + eprintln!("\x1b[31m✗\x1b[0m {}", e); } exit(1); } diff --git a/docker/Dockerfile.build b/docker/Dockerfile.build index 693201b..b00ee26 100644 --- a/docker/Dockerfile.build +++ b/docker/Dockerfile.build @@ -10,12 +10,13 @@ RUN apt-get update && apt-get install -y \ mingw-w64 \ && rm -rf /var/lib/apt/lists/* -# Add Rust targets (Unix only - Windows uses Node.js fallback due to Unix socket dependency) +# Add Rust targets for all platforms RUN rustup target add \ x86_64-unknown-linux-gnu \ aarch64-unknown-linux-gnu \ x86_64-apple-darwin \ - aarch64-apple-darwin + aarch64-apple-darwin \ + x86_64-pc-windows-gnu # Install cargo-zigbuild for easier cross-compilation (especially macOS) RUN curl -sSL https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar -xJ -C /opt \ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8dda619..d575d91 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,11 +1,11 @@ # Docker Compose for building agent-browser # Usage: docker compose -f docker/docker-compose.yml run build-linux +# docker compose -f docker/docker-compose.yml run build-windows # # Note: macOS builds should be done natively on macOS for compatibility. -# Windows uses Node.js fallback (Unix sockets not supported). services: - # Build for Linux platforms only (macOS should be built natively) + # Build for Linux platforms build-linux: build: context: .. @@ -35,6 +35,27 @@ services: ls -la /output/agent-browser-linux-* ' + # Build for Windows + build-windows: + build: + context: .. + dockerfile: docker/Dockerfile.build + volumes: + - ../cli:/build + - ../bin:/output + command: | + -c ' + set -e + echo "Building for Windows x64..." + + cargo build --release --target x86_64-pc-windows-gnu + cp /build/target/x86_64-pc-windows-gnu/release/agent-browser.exe /output/agent-browser-win32-x64.exe + + echo "" + echo "✓ Windows build completed!" + ls -la /output/agent-browser-win32-* + ' + # Build for a single target (override with TARGET env var) build-single: build: diff --git a/package.json b/package.json index 06cd264..189c448 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "build:native": "cargo build --release --manifest-path cli/Cargo.toml && node scripts/copy-native.js", "build:linux": "docker compose -f docker/docker-compose.yml run --rm build-linux", "build:macos": "cargo build --release --manifest-path cli/Cargo.toml --target aarch64-apple-darwin && cargo build --release --manifest-path cli/Cargo.toml --target x86_64-apple-darwin && cp cli/target/aarch64-apple-darwin/release/agent-browser bin/agent-browser-darwin-arm64 && cp cli/target/x86_64-apple-darwin/release/agent-browser bin/agent-browser-darwin-x64", - "build:all-platforms": "npm run build:linux && npm run build:macos", + "build:windows": "docker compose -f docker/docker-compose.yml run --rm build-windows", + "build:all-platforms": "npm run build:linux && npm run build:windows && npm run build:macos", "build:docker": "docker build -t agent-browser-builder -f docker/Dockerfile.build .", "start": "node dist/index.js", "dev": "tsx src/index.ts", diff --git a/src/daemon.ts b/src/daemon.ts index 24ff87b..e3b5fec 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -6,6 +6,9 @@ import { BrowserManager } from './browser.js'; import { parseCommand, serializeResponse, errorResponse } from './protocol.js'; import { executeCommand } from './actions.js'; +// Platform detection +const isWindows = process.platform === 'win32'; + // Session support - each session gets its own socket/pid let currentSession = process.env.AGENT_BROWSER_SESSION || 'default'; @@ -24,13 +27,38 @@ export function getSession(): string { } /** - * Get the socket path for the current session + * Get port number for TCP mode (Windows) + * Uses a hash of the session name to get a consistent port + */ +function getPortForSession(session: string): number { + let hash = 0; + for (let i = 0; i < session.length; i++) { + hash = ((hash << 5) - hash) + session.charCodeAt(i); + hash |= 0; + } + // Port range 49152-65535 (dynamic/private ports) + return 49152 + (Math.abs(hash) % 16383); +} + +/** + * Get the socket path for the current session (Unix) or port (Windows) */ export function getSocketPath(session?: string): string { const sess = session ?? currentSession; + if (isWindows) { + return String(getPortForSession(sess)); + } return path.join(os.tmpdir(), `agent-browser-${sess}.sock`); } +/** + * Get the port file path for Windows (stores the port number) + */ +export function getPortFile(session?: string): string { + const sess = session ?? currentSession; + return path.join(os.tmpdir(), `agent-browser-${sess}.port`); +} + /** * Get the PID file path for the current session */ @@ -48,7 +76,7 @@ export function isDaemonRunning(session?: string): boolean { try { const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10); - // Check if process exists + // Check if process exists (works on both Unix and Windows) process.kill(pid, 0); return true; } catch { @@ -58,15 +86,32 @@ export function isDaemonRunning(session?: string): boolean { } } +/** + * Get connection info for the current session + * Returns { type: 'unix', path: string } or { type: 'tcp', port: number } + */ +export function getConnectionInfo(session?: string): { type: 'unix'; path: string } | { type: 'tcp'; port: number } { + const sess = session ?? currentSession; + if (isWindows) { + return { type: 'tcp', port: getPortForSession(sess) }; + } + return { type: 'unix', path: path.join(os.tmpdir(), `agent-browser-${sess}.sock`) }; +} + /** * Clean up socket and PID file for the current session */ export function cleanupSocket(session?: string): void { - const socketPath = getSocketPath(session); const pidFile = getPidFile(session); try { - if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath); if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); + if (isWindows) { + const portFile = getPortFile(session); + if (fs.existsSync(portFile)) fs.unlinkSync(portFile); + } else { + const socketPath = getSocketPath(session); + if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath); + } } catch { // Ignore cleanup errors } @@ -144,15 +189,26 @@ export async function startDaemon(): Promise { }); }); - const socketPath = getSocketPath(); const pidFile = getPidFile(); // Write PID file before listening fs.writeFileSync(pidFile, process.pid.toString()); - server.listen(socketPath, () => { - // Daemon is ready - }); + 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 + }); + } else { + // Unix: use Unix domain socket + const socketPath = getSocketPath(); + server.listen(socketPath, () => { + // Daemon is ready + }); + } server.on('error', (err) => { console.error('Server error:', err);