use crate::cx::Cx;
use crate::error::{Error, ErrorKind};
use crate::types::RegionId;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
#[cfg(unix)]
use sysinfo::Signal as SysSignal;
use sysinfo::{Pid, ProcessesToUpdate, System};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DaemonControlCapability {
Status,
Start,
Stop,
ForceKill,
Restart,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DaemonProcessInfo {
pub pid: Option<u32>,
pub command: String,
pub working_dir: PathBuf,
pub user: String,
pub cpu_usage: f32,
pub memory_usage: u64,
pub start_time: Option<Instant>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DaemonControlResult {
Success {
previous_state: DaemonState,
new_state: DaemonState,
duration: Duration,
},
Failed {
operation: DaemonControlCapability,
error: String,
current_state: DaemonState,
},
PermissionDenied {
required_capability: DaemonControlCapability,
current_privileges: Vec<DaemonControlCapability>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DaemonState {
Stopped,
Starting,
Running,
Stopping,
Unknown,
Failed,
}
#[derive(Debug)]
pub struct SecureDaemonController {
region_id: RegionId,
capabilities: Vec<DaemonControlCapability>,
daemon_path: PathBuf,
config_path: PathBuf,
pid_file: PathBuf,
system: System,
}
impl SecureDaemonController {
pub fn new(
region_id: RegionId,
capabilities: Vec<DaemonControlCapability>,
daemon_path: PathBuf,
config_path: PathBuf,
) -> Result<Self, Error> {
if !daemon_path.exists() {
return Err(Error::new(ErrorKind::ConfigError).with_message(format!(
"Daemon executable not found: {}",
daemon_path.display()
)));
}
if !Self::is_executable(&daemon_path)? {
return Err(Error::new(ErrorKind::AdmissionDenied)
.with_message("Daemon executable lacks execute permissions"));
}
if !config_path.exists() {
return Err(Error::new(ErrorKind::ConfigError).with_message(format!(
"Daemon config not found: {}",
config_path.display()
)));
}
let pid_file = config_path
.parent()
.ok_or_else(|| {
Error::new(ErrorKind::ConfigError)
.with_message("Invalid config path: no parent directory")
})?
.join("daemon.pid");
Ok(Self {
region_id,
capabilities,
daemon_path,
config_path,
pid_file,
system: System::new_all(),
})
}
pub fn status(&mut self, cx: &Cx) -> Result<DaemonProcessInfo, Error> {
self.require_capability(DaemonControlCapability::Status)?;
self.validate_operation_authorized(cx, DaemonControlCapability::Status)?;
self.system.refresh_processes(ProcessesToUpdate::All, true);
let pid = self.read_pid_file().ok();
if let Some(pid) = pid {
if let Some(process) = self.system.process(Pid::from_u32(pid)) {
if self.is_our_daemon_process(process) {
return Ok(DaemonProcessInfo {
pid: Some(pid),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(" "),
working_dir: process
.cwd()
.map_or_else(platform_fallback_work_dir, Path::to_path_buf),
user: format!(
"{:?}",
process
.user_id()
.map_or_else(|| "0".to_string(), |uid| uid.to_string())
),
cpu_usage: process.cpu_usage(),
memory_usage: process.memory(),
start_time: Some(Instant::now()), });
}
}
}
if pid.is_some() {
let _ = fs::remove_file(&self.pid_file); }
Ok(DaemonProcessInfo {
pid: None,
command: "Not running".to_string(),
working_dir: self
.config_path
.parent()
.map_or_else(platform_fallback_work_dir, Path::to_path_buf),
user: "none".to_string(),
cpu_usage: 0.0,
memory_usage: 0,
start_time: None,
})
}
pub fn start(&mut self, cx: &Cx) -> Result<DaemonControlResult, Error> {
self.require_capability(DaemonControlCapability::Start)?;
self.validate_operation_authorized(cx, DaemonControlCapability::Start)?;
let start_time = Instant::now();
let previous_state = self.get_daemon_state();
if previous_state == DaemonState::Running {
return Ok(DaemonControlResult::Success {
previous_state,
new_state: DaemonState::Running,
duration: start_time.elapsed(),
});
}
let mut cmd = Command::new(&self.daemon_path);
cmd.arg("--config")
.arg(&self.config_path)
.arg("--daemon")
.stdout(Stdio::null()) .stderr(Stdio::null())
.stdin(Stdio::null());
let safe_work_dir = self
.config_path
.parent()
.map_or_else(platform_fallback_work_dir, Path::to_path_buf);
cmd.current_dir(&safe_work_dir);
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
self.write_pid_file(pid)?;
std::thread::sleep(Duration::from_millis(100));
let new_state = self.get_daemon_state();
Ok(DaemonControlResult::Success {
previous_state,
new_state,
duration: start_time.elapsed(),
})
}
Err(e) => Ok(DaemonControlResult::Failed {
operation: DaemonControlCapability::Start,
error: format!("Failed to start daemon: {}", e),
current_state: self.get_daemon_state(),
}),
}
}
pub fn stop(&mut self, cx: &Cx) -> Result<DaemonControlResult, Error> {
self.require_capability(DaemonControlCapability::Stop)?;
self.validate_operation_authorized(cx, DaemonControlCapability::Stop)?;
let start_time = Instant::now();
let previous_state = self.get_daemon_state();
if previous_state == DaemonState::Stopped {
return Ok(DaemonControlResult::Success {
previous_state,
new_state: DaemonState::Stopped,
duration: start_time.elapsed(),
});
}
let pid = match self.read_pid_file() {
Ok(pid) => pid,
Err(_) => {
return Ok(DaemonControlResult::Failed {
operation: DaemonControlCapability::Stop,
error: "No PID file found - daemon may already be stopped".to_string(),
current_state: DaemonState::Stopped,
});
}
};
let stop_result = self.send_signal_to_process(pid, Signal::Term);
match stop_result {
Ok(()) => {
let shutdown_timeout = Duration::from_secs(10);
let shutdown_start = Instant::now();
while shutdown_start.elapsed() < shutdown_timeout {
if self.get_daemon_state() == DaemonState::Stopped {
let _ = fs::remove_file(&self.pid_file);
return Ok(DaemonControlResult::Success {
previous_state,
new_state: DaemonState::Stopped,
duration: start_time.elapsed(),
});
}
std::thread::sleep(Duration::from_millis(100));
}
Ok(DaemonControlResult::Failed {
operation: DaemonControlCapability::Stop,
error: "Daemon did not shut down gracefully within timeout".to_string(),
current_state: self.get_daemon_state(),
})
}
Err(e) => Ok(DaemonControlResult::Failed {
operation: DaemonControlCapability::Stop,
error: format!("Failed to send stop signal: {}", e),
current_state: self.get_daemon_state(),
}),
}
}
pub fn force_kill(&mut self, cx: &Cx) -> Result<DaemonControlResult, Error> {
self.require_capability(DaemonControlCapability::ForceKill)?;
self.validate_operation_authorized(cx, DaemonControlCapability::ForceKill)?;
let start_time = Instant::now();
let previous_state = self.get_daemon_state();
let pid = match self.read_pid_file() {
Ok(pid) => pid,
Err(_) => {
return Ok(DaemonControlResult::Failed {
operation: DaemonControlCapability::ForceKill,
error: "No PID file found - daemon may already be stopped".to_string(),
current_state: DaemonState::Stopped,
});
}
};
let kill_result = self.send_signal_to_process(pid, Signal::Kill);
let _ = fs::remove_file(&self.pid_file);
match kill_result {
Ok(()) => Ok(DaemonControlResult::Success {
previous_state,
new_state: DaemonState::Stopped,
duration: start_time.elapsed(),
}),
Err(e) => Ok(DaemonControlResult::Failed {
operation: DaemonControlCapability::ForceKill,
error: format!("Failed to force kill daemon: {}", e),
current_state: self.get_daemon_state(),
}),
}
}
pub fn restart(&mut self, cx: &Cx) -> Result<DaemonControlResult, Error> {
if !self
.capabilities
.contains(&DaemonControlCapability::Restart)
{
self.require_capability(DaemonControlCapability::Stop)?;
self.require_capability(DaemonControlCapability::Start)?;
}
self.validate_operation_authorized(cx, DaemonControlCapability::Restart)?;
let start_time = Instant::now();
let previous_state = self.get_daemon_state();
if previous_state != DaemonState::Stopped {
let stop_result = self.stop(cx)?;
if let DaemonControlResult::Failed { .. } = stop_result {
return Ok(stop_result);
}
}
let start_result = self.start(cx)?;
match start_result {
DaemonControlResult::Success { new_state, .. } => Ok(DaemonControlResult::Success {
previous_state,
new_state,
duration: start_time.elapsed(),
}),
other => Ok(other),
}
}
fn require_capability(&self, cap: DaemonControlCapability) -> Result<(), Error> {
if self.capabilities.contains(&cap) {
Ok(())
} else {
Err(Error::new(ErrorKind::AdmissionDenied).with_message(format!(
"Operation requires {:?} capability, but only have: {:?}",
cap, self.capabilities
)))
}
}
fn validate_operation_authorized(
&self,
cx: &Cx,
_operation: DaemonControlCapability,
) -> Result<(), Error> {
if cx.budget().remaining_cost().unwrap_or(0) == 0 {
return Err(Error::new(ErrorKind::AdmissionDenied).with_message(format!(
"Daemon control operation for region {:?} not authorized by capability context",
self.region_id
)));
}
Ok(())
}
fn get_daemon_state(&mut self) -> DaemonState {
self.system.refresh_processes(ProcessesToUpdate::All, true);
if let Ok(pid) = self.read_pid_file() {
if let Some(process) = self.system.process(Pid::from_u32(pid)) {
if self.is_our_daemon_process(process) {
return DaemonState::Running;
}
}
}
DaemonState::Stopped
}
fn read_pid_file(&self) -> Result<u32, Error> {
let contents = fs::read_to_string(&self.pid_file).map_err(|e| {
Error::new(ErrorKind::InvalidInput)
.with_message(format!("Failed to read PID file: {}", e))
})?;
contents.trim().parse::<u32>().map_err(|e| {
Error::new(ErrorKind::InvalidInput).with_message(format!("Invalid PID in file: {}", e))
})
}
fn write_pid_file(&self, pid: u32) -> Result<(), Error> {
fs::write(&self.pid_file, pid.to_string()).map_err(|e| {
Error::new(ErrorKind::AdmissionDenied)
.with_message(format!("Failed to write PID file: {}", e))
})
}
fn is_our_daemon_process(&self, process: &sysinfo::Process) -> bool {
let cmd = process.cmd();
if cmd.is_empty() {
return false;
}
let exe_path = Path::new(&cmd[0]);
exe_path == self.daemon_path || exe_path.file_name() == self.daemon_path.file_name()
}
fn send_signal_to_process(&self, pid: u32, signal: Signal) -> Result<(), Error> {
#[cfg(unix)]
{
let signal = match signal {
Signal::Term => SysSignal::Term,
Signal::Kill => SysSignal::Kill,
};
let mut system = System::new_all();
system.refresh_processes(ProcessesToUpdate::All, true);
match system
.process(Pid::from_u32(pid))
.and_then(|process| process.kill_with(signal))
{
Some(true) => Ok(()),
Some(false) => Err(Error::new(ErrorKind::Internal)
.with_message(format!("Failed to send {:?} to pid {}", signal, pid))),
None => Err(Error::new(ErrorKind::Internal).with_message(format!(
"Signal {:?} is unsupported or pid {} no longer exists",
signal, pid
))),
}
}
#[cfg(windows)]
{
let mut command = Command::new("taskkill.exe");
command.arg("/PID").arg(pid.to_string()).arg("/T");
if matches!(signal, Signal::Kill) {
command.arg("/F");
}
let output = command.output().map_err(|e| {
Error::new(ErrorKind::Internal)
.with_message(format!("Failed to invoke taskkill for pid {}: {}", pid, e))
})?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(Error::new(ErrorKind::Internal)
.with_message(format!("taskkill failed for pid {}: {}", pid, stderr)))
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = signal;
Err(Error::new(ErrorKind::Internal).with_message(format!(
"Process signalling is unsupported on this platform for pid {}",
pid
)))
}
}
fn is_executable(path: &Path) -> Result<bool, Error> {
let metadata = fs::metadata(path).map_err(|e| {
Error::new(ErrorKind::InvalidInput).with_message(format!("Cannot access file: {}", e))
})?;
if !metadata.is_file() {
return Ok(false);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Ok(metadata.permissions().mode() & 0o111 != 0)
}
#[cfg(windows)]
{
Ok(path_has_windows_executable_extension(
path,
std::env::var_os("PATHEXT").as_deref(),
))
}
#[cfg(not(any(unix, windows)))]
{
Ok(true)
}
}
}
fn platform_fallback_work_dir() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir())
}
#[cfg(any(windows, test))]
fn path_has_windows_executable_extension(path: &Path, pathext: Option<&std::ffi::OsStr>) -> bool {
let Some(extension) = path.extension().and_then(|extension| extension.to_str()) else {
return false;
};
let extension = extension.trim_start_matches('.');
let default_pathext = ".COM;.EXE;.BAT;.CMD;.PS1";
let pathext = pathext
.and_then(|value| value.to_str())
.filter(|value| !value.trim().is_empty())
.unwrap_or(default_pathext);
pathext
.split(';')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.any(|entry| {
entry
.trim_start_matches('.')
.eq_ignore_ascii_case(extension)
})
}
#[derive(Debug, Clone, Copy)]
enum Signal {
Term,
Kill,
}
pub fn create_atp_daemon_controller(
region_id: RegionId,
capabilities: Vec<DaemonControlCapability>,
config_dir: &Path,
) -> Result<SecureDaemonController, Error> {
let daemon_path = std::env::current_exe().map_err(|e| {
Error::new(ErrorKind::ConfigError)
.with_message(format!("Cannot determine daemon executable path: {}", e))
})?;
let config_path = config_dir.join("config.toml");
SecureDaemonController::new(region_id, capabilities, daemon_path, config_path)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_capability_validation() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "# test config").unwrap();
let controller = SecureDaemonController::new(
RegionId::new_for_test(1, 0),
vec![DaemonControlCapability::Status],
std::env::current_exe().unwrap(),
config_path,
)
.unwrap();
assert!(
controller
.require_capability(DaemonControlCapability::Status)
.is_ok()
);
assert!(
controller
.require_capability(DaemonControlCapability::Start)
.is_err()
);
}
#[test]
fn test_daemon_controller_creation() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "# test config").unwrap();
let controller = SecureDaemonController::new(
RegionId::new_for_test(1, 0),
vec![
DaemonControlCapability::Status,
DaemonControlCapability::Start,
],
std::env::current_exe().unwrap(),
config_path,
);
assert!(controller.is_ok());
}
#[test]
fn test_invalid_daemon_path() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "# test config").unwrap();
let controller = SecureDaemonController::new(
RegionId::new_for_test(1, 0),
vec![DaemonControlCapability::Status],
PathBuf::from("/nonexistent/daemon"),
config_path,
);
assert!(controller.is_err());
}
#[test]
fn windows_executable_extension_matching_uses_pathext_semantics() {
assert!(path_has_windows_executable_extension(
Path::new("atpd.ExE"),
Some(std::ffi::OsStr::new(".COM;.EXE;.BAT;.CMD"))
));
assert!(path_has_windows_executable_extension(
Path::new("atpd.cmd"),
Some(std::ffi::OsStr::new("EXE;CMD"))
));
assert!(!path_has_windows_executable_extension(
Path::new("atpd.sh"),
Some(std::ffi::OsStr::new(".COM;.EXE;.BAT;.CMD"))
));
assert!(!path_has_windows_executable_extension(
Path::new("atpd"),
Some(std::ffi::OsStr::new(".COM;.EXE;.BAT;.CMD"))
));
}
#[test]
fn test_atp_daemon_controller_factory() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "# test config").unwrap();
let controller = create_atp_daemon_controller(
RegionId::new_for_test(1, 0),
vec![DaemonControlCapability::Status],
temp_dir.path(),
);
assert!(controller.is_ok());
}
}