pub mod auth;
pub mod http;
pub mod registry;
pub mod socket;
use std::fs;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use anyhow::{Context, Result};
use clap::Subcommand;
#[cfg(unix)]
use serde_json::json;
use registry::SessionRegistry;
pub const DEFAULT_HTTP_PORT: u16 = 54321;
#[derive(Subcommand, Debug, Clone)]
pub enum DaemonCommand {
Start {
#[arg(long)]
socket: Option<PathBuf>,
#[arg(long, default_value_t = DEFAULT_HTTP_PORT)]
port: u16,
#[arg(short, long)]
foreground: bool,
},
Status,
Stop,
}
pub fn run_cli(cmd: &DaemonCommand) -> Result<()> {
match cmd {
DaemonCommand::Start {
socket,
port,
foreground,
} => start_daemon(socket.as_deref(), *port, *foreground),
DaemonCommand::Status => query_daemon_status(),
DaemonCommand::Stop => stop_daemon(),
}
}
pub fn start_daemon(custom_socket: Option<&Path>, port: u16, foreground: bool) -> Result<()> {
let state_dir = auth::default_daemon_dir()?;
auth::ensure_daemon_dir(&state_dir)?;
let token = auth::ensure_daemon_token(&state_dir)?;
let socket_path = custom_socket
.map(PathBuf::from)
.unwrap_or_else(|| state_dir.join(socket::SOCKET_FILENAME));
let pid_path = state_dir.join("daemon.pid");
let my_pid = std::process::id();
fs::write(&pid_path, my_pid.to_string())
.with_context(|| format!("failed to write PID file {}", pid_path.display()))?;
let registry = Arc::new(SessionRegistry::new(state_dir.clone()));
println!("Starting Vetto Daemon (PID: {})...", my_pid);
println!(" State Directory: {}", state_dir.display());
println!(" Unix Socket: {}", socket_path.display());
println!(" HTTP REST API: http://127.0.0.1:{}", port);
println!(
" Token File: {}",
state_dir.join(auth::TOKEN_FILENAME).display()
);
let http_addr: SocketAddr = format!("127.0.0.1:{}", port)
.parse()
.context("invalid HTTP listen address")?;
let http_server = http::HttpServer::bind(http_addr, token, Arc::clone(®istry))?;
thread::spawn(move || {
let _ = http_server.run_loop();
});
#[cfg(unix)]
{
let socket_server = socket::SocketServer::bind(socket_path, Arc::clone(®istry))?;
if foreground {
socket_server.run_loop()?;
} else {
thread::spawn(move || {
let _ = socket_server.run_loop();
});
loop {
thread::sleep(std::time::Duration::from_secs(3600));
}
}
}
#[cfg(not(unix))]
{
let _ = foreground;
println!("Unix domain socket listener is disabled on Windows. REST API is active.");
loop {
thread::sleep(std::time::Duration::from_secs(3600));
}
}
#[cfg(unix)]
Ok(())
}
pub fn query_daemon_status() -> Result<()> {
let state_dir = auth::default_daemon_dir()?;
let pid_path = state_dir.join("daemon.pid");
if !pid_path.exists() {
println!(
"Vetto daemon is NOT running (no daemon.pid at {})",
pid_path.display()
);
return Ok(());
}
let pid_str = fs::read_to_string(&pid_path)?;
println!("Vetto daemon PID: {}", pid_str.trim());
#[cfg(unix)]
{
let socket_path = state_dir.join(socket::SOCKET_FILENAME);
if socket_path.exists() {
match socket::send_socket_request(&socket_path, &json!({"action": "list_sessions"})) {
Ok(resp) => {
println!("Daemon Socket: ONLINE ({})", socket_path.display());
if let Some(sessions) = resp.get("data").and_then(|d| d.as_array()) {
println!("Active Sandboxed Sessions: {}", sessions.len());
for s in sessions {
println!(
" - ID: {} | PID: {} | Status: {} | Policy: {}",
s.get("id").and_then(|v| v.as_str()).unwrap_or("?"),
s.get("pid").and_then(|v| v.as_u64()).unwrap_or(0),
s.get("status").and_then(|v| v.as_str()).unwrap_or("?"),
s.get("policy").and_then(|v| v.as_str()).unwrap_or("?"),
);
}
}
}
Err(e) => {
println!(
"Daemon Socket: UNREACHABLE ({}) - error: {e}",
socket_path.display()
);
}
}
}
}
Ok(())
}
pub fn stop_daemon() -> Result<()> {
let state_dir = auth::default_daemon_dir()?;
let pid_path = state_dir.join("daemon.pid");
if !pid_path.exists() {
println!("No running daemon found.");
return Ok(());
}
let pid_str = fs::read_to_string(&pid_path)?;
if let Ok(pid) = pid_str.trim().parse::<i32>() {
#[cfg(unix)]
unsafe {
libc::kill(pid, libc::SIGTERM);
}
println!("Sent SIGTERM to daemon process {}", pid);
}
let _ = fs::remove_file(&pid_path);
#[cfg(unix)]
{
let socket_path = state_dir.join(socket::SOCKET_FILENAME);
let _ = fs::remove_file(&socket_path);
}
println!("Vetto daemon stopped.");
Ok(())
}