use crate::LockFile;
use serde::Serialize;
use std::path::Path;
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AgentStatusKind {
Running,
Stale,
Stopped,
}
#[derive(Debug, Clone, Copy)]
pub struct AgentStatus {
pub kind: AgentStatusKind,
pub pid: Option<u32>,
}
pub fn read(lock_path: &Path) -> std::io::Result<Option<LockFile>> {
let bytes = match std::fs::read(lock_path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
serde_json::from_slice(&bytes)
.map(Some)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
#[cfg(unix)]
pub fn pid_alive(pid: u32) -> bool {
unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
}
#[cfg(windows)]
pub fn pid_alive(pid: u32) -> bool {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
unsafe {
let h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if h.is_null() {
return false;
}
CloseHandle(h);
true
}
}
#[cfg(not(any(unix, windows)))]
pub fn pid_alive(_pid: u32) -> bool {
true
}
pub fn classify(lock_path: &Path) -> AgentStatus {
match read(lock_path) {
Ok(None) => AgentStatus {
kind: AgentStatusKind::Stopped,
pid: None,
},
Err(_) => AgentStatus {
kind: AgentStatusKind::Stale,
pid: None,
},
Ok(Some(lock)) => {
let kind = if pid_alive(lock.pid) {
AgentStatusKind::Running
} else {
AgentStatusKind::Stale
};
AgentStatus {
kind,
pid: Some(lock.pid),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::LockTransports;
fn make_lock(pid: u32) -> LockFile {
LockFile {
schema: 1,
uuid: "01JQX4TM8Y9K7VQH6B2N3R5DPE".into(),
name: "agent_a".into(),
pid,
ppid: 1,
started_at: "2026-04-22T08:00:00Z".into(),
binary_version: "mur-agent-runtime 0.1.0".into(),
transports: LockTransports {
stdio: false,
unix_socket: Some("/tmp/x.sock".into()),
tcp: None,
webhook: None,
},
card_digest: "sha256:abc".into(),
capabilities: vec!["a2a.message.send".into()],
}
}
fn write_lock_file(dir: &std::path::Path, pid: u32) -> std::path::PathBuf {
let path = dir.join("running.lock");
let lock = make_lock(pid);
std::fs::write(&path, serde_json::to_vec_pretty(&lock).unwrap()).unwrap();
path
}
#[test]
fn classify_returns_stopped_when_no_lock() {
let tmp = tempfile::tempdir().unwrap();
let lock_path = tmp.path().join("running.lock");
let status = classify(&lock_path);
assert_eq!(status.kind, AgentStatusKind::Stopped);
assert_eq!(status.pid, None);
}
#[cfg(unix)]
#[test]
fn classify_returns_running_when_pid_alive() {
let tmp = tempfile::tempdir().unwrap();
let lock_path = write_lock_file(tmp.path(), std::process::id());
let status = classify(&lock_path);
assert_eq!(status.kind, AgentStatusKind::Running);
assert_eq!(status.pid, Some(std::process::id()));
}
#[cfg(unix)]
#[test]
fn classify_returns_stale_when_pid_dead() {
let tmp = tempfile::tempdir().unwrap();
let dead_pid: u32 = 999_999;
let lock_path = write_lock_file(tmp.path(), dead_pid);
let status = classify(&lock_path);
assert_eq!(status.kind, AgentStatusKind::Stale);
assert_eq!(status.pid, Some(dead_pid));
}
#[test]
fn classify_returns_stale_when_lock_malformed() {
let tmp = tempfile::tempdir().unwrap();
let lock_path = tmp.path().join("running.lock");
std::fs::write(&lock_path, b"not json").unwrap();
let status = classify(&lock_path);
assert_eq!(status.kind, AgentStatusKind::Stale);
assert_eq!(status.pid, None);
}
#[test]
fn read_returns_none_for_missing_file() {
let tmp = tempfile::tempdir().unwrap();
let lock_path = tmp.path().join("running.lock");
let result = read(&lock_path).unwrap();
assert!(result.is_none());
}
#[test]
fn read_returns_ok_for_valid_lock() {
let tmp = tempfile::tempdir().unwrap();
let lock_path = write_lock_file(tmp.path(), 42);
let result = read(&lock_path).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().pid, 42);
}
#[test]
fn read_returns_err_for_malformed_json() {
let tmp = tempfile::tempdir().unwrap();
let lock_path = tmp.path().join("running.lock");
std::fs::write(&lock_path, b"not json").unwrap();
let result = read(&lock_path);
assert!(result.is_err());
}
#[cfg(unix)]
#[test]
fn pid_alive_returns_true_for_self() {
assert!(pid_alive(std::process::id()));
}
#[cfg(unix)]
#[test]
fn pid_alive_returns_false_for_dead_pid() {
assert!(!pid_alive(999_999));
}
}