Files
chrome-use/cli/src/connection.rs
T
2026-01-11 10:08:08 -06:00

278 lines
7.3 KiB
Rust

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<Value>,
pub error: Option<String>,
}
#[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<usize> {
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<usize> {
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<Duration>) -> 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<Duration>) -> 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::<i32>() {
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<Connection, String> {
#[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<Response, String> {
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))
}