278 lines
7.3 KiB
Rust
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))
|
|
}
|