use serde_json::Value; use std::env; use std::fs; use std::io::Write; use std::path::PathBuf; use std::process; use std::sync::Arc; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::signal; use tokio::sync::{mpsc, RwLock}; use super::actions::{execute_command, DaemonState}; use super::cdp::client::CdpClient; use super::state; use super::stream::StreamServer; pub async fn run_daemon(session: &str) { let socket_dir = get_daemon_socket_dir(); if !socket_dir.exists() { let _ = fs::create_dir_all(&socket_dir); } // When debug mode is on, redirect stderr to a log file so daemon // output can be inspected (the daemon normally has stderr piped to its // parent which drops the read end after startup). #[cfg(unix)] if env::var("AGENT_BROWSER_DEBUG").is_ok() { let log_path = socket_dir.join(format!("{}.log", session)); if let Ok(file) = fs::File::create(&log_path) { use std::os::unix::io::IntoRawFd; let fd = file.into_raw_fd(); unsafe { libc::dup2(fd, 2); libc::close(fd); } let _ = writeln!( std::io::stderr(), "[daemon] Debug logging started for session: {}", session ); } } let pid_path = socket_dir.join(format!("{}.pid", session)); let _ = fs::write(&pid_path, process::id().to_string()); // On Unix the daemon listens on a Unix domain socket; on Windows it uses // TCP, so there is no .sock file — only a .port file written by the server. let socket_path = socket_dir.join(format!("{}.sock", session)); #[cfg(unix)] if socket_path.exists() { let _ = fs::remove_file(&socket_path); } #[cfg(windows)] { let _ = fs::remove_file(socket_dir.join(format!("{}.port", session))); } let stream_path = socket_dir.join(format!("{}.stream", session)); let _ = fs::remove_file(&stream_path); let _ = fs::remove_file(socket_dir.join(format!("{}.engine", session))); let _ = fs::remove_file(socket_dir.join(format!("{}.extensions", session))); if let Ok(days_str) = env::var("AGENT_BROWSER_STATE_EXPIRE_DAYS") { if let Ok(days) = days_str.parse::() { if days > 0 { let _ = state::state_clean(days); } } } let mut stream_client: Option>>>> = None; let mut stream_server_instance: Option> = None; let preferred_port = env::var("AGENT_BROWSER_STREAM_PORT") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(0); match StreamServer::start_without_client(preferred_port, session.to_string(), true).await { Ok((stream_server, client_slot)) => { stream_client = Some(client_slot.clone()); if let Err(e) = fs::write(&stream_path, stream_server.port().to_string()) { let _ = writeln!(std::io::stderr(), "Failed to write .stream file: {}", e); } stream_server_instance = Some(Arc::new(stream_server)); } Err(e) => { let _ = writeln!(std::io::stderr(), "Stream server failed to start: {}", e); } } // Auto-shutdown the daemon after this many ms of inactivity (no commands received). // Disabled when unset or 0. let idle_timeout_ms = env::var("AGENT_BROWSER_IDLE_TIMEOUT_MS") .ok() .and_then(|s| s.parse::().ok()) .filter(|&ms| ms > 0); let result = run_socket_server( &socket_path, session, stream_client, stream_server_instance, idle_timeout_ms, ) .await; #[cfg(unix)] { let _ = fs::remove_file(&socket_path); } #[cfg(windows)] { let _ = fs::remove_file(socket_dir.join(format!("{}.port", session))); } let _ = fs::remove_file(&pid_path); let _ = fs::remove_file(&stream_path); let _ = fs::remove_file(socket_dir.join(format!("{}.engine", session))); let _ = fs::remove_file(socket_dir.join(format!("{}.extensions", session))); if let Err(e) = result { let _ = writeln!(std::io::stderr(), "Daemon error: {}", e); process::exit(1); } } #[cfg(unix)] async fn run_socket_server( socket_path: &PathBuf, session: &str, stream_client: Option>>>>, stream_server: Option>, idle_timeout_ms: Option, ) -> Result<(), String> { use tokio::net::UnixListener; let listener = UnixListener::bind(socket_path).map_err(|e| format!("Failed to bind socket: {}", e))?; let stream_file: Option = if stream_server.is_some() { let dir = socket_path.parent().unwrap_or(std::path::Path::new(".")); Some(dir.join(format!("{}.stream", session))) } else { None }; let state: std::sync::Arc> = std::sync::Arc::new( tokio::sync::Mutex::new(DaemonState::new_with_stream(stream_client, stream_server)), ); let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64); let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx)); // Listen for SIGCHLD to reap zombie child processes (e.g. crashed Chrome). // Without this, a crashed Chrome becomes and is never reaped until // the daemon exits. let mut sigchld = signal::unix::signal(signal::unix::SignalKind::child()) .map_err(|e| format!("Failed to install SIGCHLD handler: {}", e))?; let mut drain_interval = tokio::time::interval(Duration::from_millis(500)); drain_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { let sleep_future = idle_timeout_ms.map(|ms| tokio::time::sleep(Duration::from_millis(ms))); let mut sleep_pin = sleep_future.map(Box::pin); tokio::select! { accept_result = listener.accept() => { match accept_result { Ok((stream, _)) => { let state = state.clone(); let reset_tx = reset_tx.clone(); let sf = stream_file.clone(); tokio::spawn(async move { handle_connection(stream, state, reset_tx, sf).await; }); } Err(e) => { let _ = writeln!(std::io::stderr(), "Accept error: {}", e); } } } _ = sigchld.recv() => { reap_children(); } _ = drain_interval.tick() => { let mut s = state.lock().await; if s.request_tracking || s.har_recording { s.drain_cdp_events_background(); } } _ = async { if let Some(ref mut s) = sleep_pin { s.as_mut().await } else { std::future::pending::<()>().await } }, if idle_timeout_ms.is_some() => { let mut s = state.lock().await; if let Some(ref mut mgr) = s.browser { let _ = mgr.close().await; } break; } _ = reset_rx.recv(), if idle_timeout_ms.is_some() => { continue; } _ = shutdown_signal() => { let mut s = state.lock().await; if let Some(ref mut mgr) = s.browser { let _ = mgr.close().await; } break; } } } Ok(()) } /// Reap all zombie child processes by calling waitpid(-1, WNOHANG) in a loop. #[cfg(unix)] fn reap_children() { loop { let result = unsafe { libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) }; if result <= 0 { break; } } } #[cfg(windows)] async fn run_socket_server( socket_path: &PathBuf, session: &str, stream_client: Option>>>>, stream_server: Option>, idle_timeout_ms: Option, ) -> Result<(), String> { use tokio::net::TcpListener; let preferred_port = get_port_for_session(session); // Try the hash-derived port first; if it is blocked (e.g. Windows Hyper-V // excluded port range), fall back to an OS-assigned ephemeral port. let listener = match TcpListener::bind(format!("127.0.0.1:{}", preferred_port)).await { Ok(l) => l, Err(_) => TcpListener::bind("127.0.0.1:0") .await .map_err(|e| format!("Failed to bind TCP: {}", e))?, }; let actual_port = listener .local_addr() .map_err(|e| format!("Failed to get local address: {}", e))? .port(); let socket_dir = socket_path.parent().unwrap_or(std::path::Path::new(".")); let port_path = socket_dir.join(format!("{}.port", session)); let _ = fs::write(&port_path, actual_port.to_string()); let stream_file: Option = if stream_server.is_some() { Some(socket_dir.join(format!("{}.stream", session))) } else { None }; let state: std::sync::Arc> = std::sync::Arc::new( tokio::sync::Mutex::new(DaemonState::new_with_stream(stream_client, stream_server)), ); let (reset_tx, mut reset_rx) = mpsc::channel::<()>(64); let reset_tx = idle_timeout_ms.map(|_| Arc::new(reset_tx)); loop { let sleep_future = idle_timeout_ms.map(|ms| tokio::time::sleep(Duration::from_millis(ms))); let mut sleep_pin = sleep_future.map(Box::pin); tokio::select! { accept_result = listener.accept() => { match accept_result { Ok((stream, _)) => { let state = state.clone(); let reset_tx = reset_tx.clone(); let sf = stream_file.clone(); tokio::spawn(async move { handle_connection(stream, state, reset_tx, sf).await; }); } Err(e) => { let _ = writeln!(std::io::stderr(), "Accept error: {}", e); } } } _ = async { if let Some(ref mut s) = sleep_pin { s.as_mut().await } else { std::future::pending::<()>().await } }, if idle_timeout_ms.is_some() => { let mut s = state.lock().await; if let Some(ref mut mgr) = s.browser { let _ = mgr.close().await; } let _ = fs::remove_file(&port_path); break; } _ = reset_rx.recv(), if idle_timeout_ms.is_some() => { continue; } _ = shutdown_signal() => { let mut s = state.lock().await; if let Some(ref mut mgr) = s.browser { let _ = mgr.close().await; } let _ = fs::remove_file(&port_path); break; } } } Ok(()) } async fn handle_connection( stream: S, state: std::sync::Arc>, idle_reset_tx: Option>>, stream_file_cleanup: Option, ) where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, { let (reader, mut writer) = tokio::io::split(stream); let mut buf_reader = BufReader::new(reader); let mut line = String::new(); loop { line.clear(); match buf_reader.read_line(&mut line).await { Ok(0) => break, Ok(_) => { let trimmed = line.trim(); if trimmed.is_empty() { continue; } if looks_like_http(trimmed) { break; } let cmd: Value = match serde_json::from_str(trimmed) { Ok(v) => v, Err(e) => { let err = serde_json::json!({ "success": false, "error": format!("Invalid JSON: {}", e), }); let mut resp = serde_json::to_string(&err).unwrap_or_default(); resp.push('\n'); let _ = writer.write_all(resp.as_bytes()).await; continue; } }; if let Some(ref tx) = idle_reset_tx { let _ = tx.try_send(()); } let is_close = cmd.get("action").and_then(|v| v.as_str()) == Some("close"); let response = { let mut s = state.lock().await; execute_command(&cmd, &mut s).await }; let mut resp = serde_json::to_string(&response).unwrap_or_default(); resp.push('\n'); if writer.write_all(resp.as_bytes()).await.is_err() { break; } if is_close { if let Some(ref path) = stream_file_cleanup { let _ = fs::remove_file(path); } tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; process::exit(0); } } Err(_) => break, } } } fn looks_like_http(line: &str) -> bool { let prefixes = [ "GET ", "POST ", "PUT ", "DELETE ", "PATCH ", "HEAD ", "OPTIONS ", "CONNECT ", "TRACE ", ]; prefixes.iter().any(|p| line.starts_with(p)) } async fn shutdown_signal() { #[cfg(unix)] { let mut sigint = match signal::unix::signal(signal::unix::SignalKind::interrupt()) { Ok(s) => s, Err(e) => { let _ = writeln!(std::io::stderr(), "Failed to install SIGINT handler: {}", e); process::exit(1); } }; let mut sigterm = match signal::unix::signal(signal::unix::SignalKind::terminate()) { Ok(s) => s, Err(e) => { let _ = writeln!( std::io::stderr(), "Failed to install SIGTERM handler: {}", e ); process::exit(1); } }; let mut sighup = match signal::unix::signal(signal::unix::SignalKind::hangup()) { Ok(s) => s, Err(e) => { let _ = writeln!(std::io::stderr(), "Failed to install SIGHUP handler: {}", e); process::exit(1); } }; tokio::select! { _ = sigint.recv() => {} _ = sigterm.recv() => {} _ = sighup.recv() => {} } } #[cfg(windows)] { if let Err(e) = signal::ctrl_c().await { let _ = writeln!(std::io::stderr(), "Failed to install Ctrl+C handler: {}", e); process::exit(1); } } } fn get_daemon_socket_dir() -> PathBuf { if let Ok(dir) = env::var("AGENT_BROWSER_SOCKET_DIR") { if !dir.is_empty() { return PathBuf::from(dir); } } if let Ok(xdg) = env::var("XDG_RUNTIME_DIR") { if !xdg.is_empty() { return PathBuf::from(xdg).join("agent-browser"); } } if let Some(home) = dirs::home_dir() { return home.join(".agent-browser"); } std::env::temp_dir().join("agent-browser") } #[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.unsigned_abs() as u32 % 16383) as u16) } #[cfg(test)] #[cfg(windows)] mod tests { use super::*; #[test] fn test_port_matches_client_algorithm() { // These values are computed by the identical djb2 implementation in // connection.rs. Both sides must agree on the port for the daemon to // start successfully. assert_eq!(get_port_for_session("default"), 50838); assert_eq!(get_port_for_session("my-session"), 63105); assert_eq!(get_port_for_session("work"), 51184); assert_eq!(get_port_for_session(""), 49152); } }