use serde::{Deserialize, Serialize}; use serde_json::Value; use std::env; use std::fs; use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::thread; use std::time::Duration; #[cfg(unix)] use std::os::unix::net::UnixStream; #[derive(Serialize)] #[allow(dead_code)] pub struct Request { pub id: String, pub action: String, #[serde(flatten)] pub extra: Value, } #[derive(Deserialize, Serialize, Default)] pub struct Response { pub success: bool, pub data: Option, pub error: Option, } #[allow(dead_code)] pub 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 { pub 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), } } pub 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)) } fn get_pid_path(session: &str) -> PathBuf { let tmp = env::temp_dir(); 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)) } #[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); } 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() { return false; } if let Ok(pid_str) = fs::read_to_string(&pid_path) { if let Ok(pid) = pid_str.trim().parse::() { unsafe { return libc::kill(pid, 0) == 0; } } } false } #[cfg(windows)] fn is_daemon_running(session: &str) -> bool { let pid_path = get_pid_path(session); if !pid_path.exists() { return false; } 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)] { let port = get_port_for_session(session); TcpStream::connect_timeout( &format!("127.0.0.1:{}", port).parse().unwrap(), Duration::from_millis(50), ) .is_ok() } } pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> { if is_daemon_running(session) && daemon_ready(session) { return Ok(()); } let exe_path = env::current_exe().map_err(|e| e.to_string())?; let exe_dir = exe_path.parent().unwrap(); let daemon_paths = [ exe_dir.join("daemon.js"), exe_dir.join("../dist/daemon.js"), PathBuf::from("dist/daemon.js"), ]; let daemon_path = daemon_paths .iter() .find(|p| p.exists()) .ok_or("Daemon not found. Run from project directory or ensure daemon.js is alongside binary.")?; // Spawn daemon as a fully detached background process #[cfg(unix)] { use std::os::unix::process::CommandExt; let mut cmd = Command::new("node"); cmd.arg(daemon_path) .env("AGENT_BROWSER_DAEMON", "1") .env("AGENT_BROWSER_SESSION", session); if headed { cmd.env("AGENT_BROWSER_HEADED", "1"); } // Create new process group and session to fully detach unsafe { cmd.pre_exec(|| { // Create new session (detach from terminal) libc::setsid(); Ok(()) }); } cmd.stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .map_err(|e| format!("Failed to start daemon: {}", e))?; } #[cfg(windows)] { use std::os::windows::process::CommandExt; let mut cmd = Command::new("node"); cmd.arg(daemon_path) .env("AGENT_BROWSER_DAEMON", "1") .env("AGENT_BROWSER_SESSION", session); if headed { cmd.env("AGENT_BROWSER_HEADED", "1"); } // 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) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .map_err(|e| format!("Failed to start daemon: {}", e))?; } for _ in 0..50 { if daemon_ready(session) { return Ok(()); } thread::sleep(Duration::from_millis(100)); } 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)) } } pub fn send_command(cmd: Value, session: &str) -> Result { 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(); let mut json_str = serde_json::to_string(&cmd).map_err(|e| e.to_string())?; json_str.push('\n'); stream .write_all(json_str.as_bytes()) .map_err(|e| format!("Failed to send: {}", e))?; let mut reader = BufReader::new(stream); let mut response_line = String::new(); reader .read_line(&mut response_line) .map_err(|e| format!("Failed to read: {}", e))?; serde_json::from_str(&response_line).map_err(|e| format!("Invalid response: {}", e)) }