use std::path::Path;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DaemonOwner {
Daemon,
Mcp,
}
impl std::fmt::Display for DaemonOwner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Daemon => write!(f, "daemon"),
Self::Mcp => write!(f, "mcp"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonMetadata {
pub pid: u32,
pub session: Uuid,
pub owner: DaemonOwner,
}
impl DaemonMetadata {
pub fn new(owner: DaemonOwner) -> Self {
Self {
pid: std::process::id(),
session: Uuid::new_v4(),
owner,
}
}
}
const METADATA_FILENAME: &str = "mati.pid";
const METADATA_TMP_FILENAME: &str = "mati.pid.tmp";
const SOCKET_FILENAME: &str = "mati.sock";
pub(crate) fn metadata_path(root: &Path) -> std::path::PathBuf {
root.join(METADATA_FILENAME)
}
pub fn socket_path(root: &Path) -> std::path::PathBuf {
root.join(SOCKET_FILENAME)
}
pub fn ensure_runtime_dir(root: &Path) -> Result<()> {
std::fs::create_dir_all(root)
.with_context(|| format!("cannot create runtime dir at {}", root.display()))?;
set_mode(root, 0o700).with_context(|| format!("cannot set mode 0700 on {}", root.display()))?;
Ok(())
}
pub fn harden_socket(sock_path: &Path) -> Result<()> {
set_mode(sock_path, 0o600)
.with_context(|| format!("cannot set mode 0600 on {}", sock_path.display()))
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(mode);
std::fs::set_permissions(path, perms)?;
Ok(())
}
#[cfg(not(unix))]
fn set_mode(_path: &Path, _mode: u32) -> Result<()> {
Ok(())
}
pub fn publish_metadata(root: &Path, metadata: &DaemonMetadata) -> Result<()> {
let tmp_path = root.join(METADATA_TMP_FILENAME);
let final_path = metadata_path(root);
let json = serde_json::to_string(metadata).context("failed to serialize daemon metadata")?;
std::fs::write(&tmp_path, json.as_bytes())
.with_context(|| format!("failed to write {}", tmp_path.display()))?;
set_mode(&tmp_path, 0o600)?;
std::fs::rename(&tmp_path, &final_path).with_context(|| {
format!(
"failed to rename {} → {}",
tmp_path.display(),
final_path.display()
)
})?;
Ok(())
}
pub fn read_metadata(root: &Path) -> Option<DaemonMetadata> {
let content = std::fs::read_to_string(metadata_path(root)).ok()?;
let trimmed = content.trim();
if let Ok(meta) = serde_json::from_str::<DaemonMetadata>(trimmed) {
return Some(meta);
}
if let Ok(pid) = trimmed.parse::<u32>() {
return Some(DaemonMetadata {
pid,
session: Uuid::nil(),
owner: DaemonOwner::Daemon,
});
}
if let Ok(val) = serde_json::from_str::<serde_json::Value>(trimmed) {
let pid = val.get("pid").and_then(|v| v.as_u64())? as u32;
let owner_str = val
.get("owner")
.and_then(|v| v.as_str())
.unwrap_or("daemon");
let owner = match owner_str {
"mcp" => DaemonOwner::Mcp,
_ => DaemonOwner::Daemon,
};
return Some(DaemonMetadata {
pid,
session: Uuid::nil(),
owner,
});
}
None
}
#[cfg(unix)]
pub fn is_pid_alive(pid: u32) -> bool {
let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
if ret == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(not(unix))]
pub fn is_pid_alive(_pid: u32) -> bool {
true }
#[cfg(unix)]
pub fn current_euid() -> u32 {
unsafe { libc::geteuid() }
}
#[cfg(not(unix))]
pub fn current_euid() -> u32 {
0
}
#[cfg(target_os = "macos")]
pub fn current_qos_class_str() -> &'static str {
extern "C" {
fn qos_class_self() -> libc::c_uint;
}
match unsafe { qos_class_self() } {
0x21 => "user_interactive",
0x19 => "user_initiated",
0x15 => "default",
0x11 => "utility",
0x09 => "background",
_ => "unknown",
}
}
#[cfg(not(target_os = "macos"))]
pub fn current_qos_class_str() -> &'static str {
"n/a"
}
const SIGKILL_REAP_WINDOW: std::time::Duration = std::time::Duration::from_secs(5);
#[derive(Debug)]
pub enum KillOutcome {
ExitedClean(std::time::Duration),
KilledHard(std::time::Duration),
Stuck(StuckDiagnostic),
}
#[derive(Debug, Clone)]
pub struct StuckDiagnostic {
pub pid: u32,
pub total_elapsed_ms: u64,
pub sigterm_elapsed_ms: Option<u64>,
pub sigkill_elapsed_ms: u64,
pub initial_snapshot: PidSnapshot,
pub final_snapshot: PidSnapshot,
}
#[derive(Debug, Clone, Default)]
pub struct PidSnapshot {
pub lstart: Option<String>,
pub state: Option<String>,
pub comm: Option<String>,
}
impl PidSnapshot {
pub fn render(&self) -> String {
match (&self.lstart, &self.state, &self.comm) {
(None, None, None) => "ps:gone".into(),
_ => format!(
"lstart={:?} state={:?} comm={:?}",
self.lstart.as_deref().unwrap_or("?"),
self.state.as_deref().unwrap_or("?"),
self.comm.as_deref().unwrap_or("?")
),
}
}
}
fn ps_field(pid: u32, field: &str) -> Option<String> {
let pid_str = pid.to_string();
let output = std::process::Command::new("ps")
.args(["-o", &format!("{field}="), "-p", &pid_str])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let trimmed = String::from_utf8_lossy(&output.stdout).trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
pub fn snapshot_pid(pid: u32) -> PidSnapshot {
PidSnapshot {
lstart: ps_field(pid, "lstart"),
state: ps_field(pid, "state"),
comm: ps_field(pid, "comm"),
}
}
#[cfg(unix)]
fn send_sigterm(pid: u32) -> bool {
let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) };
if ret == 0 {
return true;
}
let errno = std::io::Error::last_os_error().raw_os_error();
matches!(errno, Some(libc::ESRCH))
}
#[cfg(not(unix))]
fn send_sigterm(_pid: u32) -> bool {
false
}
pub async fn kill_directly(pid: u32) -> KillOutcome {
let started = std::time::Instant::now();
let initial_snapshot = snapshot_pid(pid);
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
if ret != 0 {
let errno = std::io::Error::last_os_error().raw_os_error();
if !matches!(errno, Some(libc::ESRCH)) {
tracing::warn!(pid, ?errno, "kill_directly: SIGKILL rejected by kernel");
let elapsed_ms = started.elapsed().as_millis() as u64;
return KillOutcome::Stuck(StuckDiagnostic {
pid,
total_elapsed_ms: elapsed_ms,
sigterm_elapsed_ms: None,
sigkill_elapsed_ms: elapsed_ms,
initial_snapshot,
final_snapshot: snapshot_pid(pid),
});
}
return KillOutcome::KilledHard(started.elapsed());
}
}
let sigkill_start = std::time::Instant::now();
if poll_until_exit(pid, SIGKILL_REAP_WINDOW, started).await {
return KillOutcome::KilledHard(started.elapsed());
}
let sigkill_elapsed_ms = sigkill_start.elapsed().as_millis() as u64;
KillOutcome::Stuck(StuckDiagnostic {
pid,
total_elapsed_ms: started.elapsed().as_millis() as u64,
sigterm_elapsed_ms: None,
sigkill_elapsed_ms,
initial_snapshot,
final_snapshot: snapshot_pid(pid),
})
}
pub async fn kill_and_wait(pid: u32, timeout: std::time::Duration) -> KillOutcome {
let started = std::time::Instant::now();
let initial_snapshot = snapshot_pid(pid);
if !send_sigterm(pid) {
tracing::warn!(pid, "kill_and_wait: SIGTERM rejected by kernel");
let elapsed_ms = started.elapsed().as_millis() as u64;
return KillOutcome::Stuck(StuckDiagnostic {
pid,
total_elapsed_ms: elapsed_ms,
sigterm_elapsed_ms: Some(elapsed_ms),
sigkill_elapsed_ms: 0,
initial_snapshot,
final_snapshot: snapshot_pid(pid),
});
}
let sigterm_start = std::time::Instant::now();
if poll_until_exit(pid, timeout, started).await {
return KillOutcome::ExitedClean(started.elapsed());
}
let sigterm_elapsed_ms = sigterm_start.elapsed().as_millis() as u64;
tracing::warn!(
pid,
timeout_secs = timeout.as_secs(),
"process did not exit within SIGTERM budget — sending SIGKILL"
);
#[cfg(unix)]
{
let _ = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
}
let sigkill_start = std::time::Instant::now();
if poll_until_exit(pid, SIGKILL_REAP_WINDOW, sigkill_start).await {
return KillOutcome::KilledHard(started.elapsed());
}
let sigkill_elapsed_ms = sigkill_start.elapsed().as_millis() as u64;
KillOutcome::Stuck(StuckDiagnostic {
pid,
total_elapsed_ms: started.elapsed().as_millis() as u64,
sigterm_elapsed_ms: Some(sigterm_elapsed_ms),
sigkill_elapsed_ms,
initial_snapshot,
final_snapshot: snapshot_pid(pid),
})
}
async fn poll_until_exit(
pid: u32,
budget: std::time::Duration,
started: std::time::Instant,
) -> bool {
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
const ZOMBIE_CHECK_INTERVAL: u32 = 5;
let deadline = started + budget;
let mut iter: u32 = 0;
while std::time::Instant::now() < deadline {
if !is_pid_alive(pid) {
return true;
}
if iter % ZOMBIE_CHECK_INTERVAL == 0 {
if let Some(state) = ps_field(pid, "state") {
if state.starts_with('Z') {
return true;
}
}
}
iter = iter.wrapping_add(1);
tokio::time::sleep(POLL_INTERVAL).await;
}
false
}
#[derive(Debug, Clone)]
pub struct PeerContext {
pub uid: u32,
pub pid: Option<u32>,
}
pub fn check_peer_cred(stream: &tokio::net::UnixStream, daemon_euid: u32) -> Option<PeerContext> {
match stream.peer_cred() {
Ok(cred) => {
let peer_uid = cred.uid();
if peer_uid != daemon_euid {
tracing::warn!(
peer_uid,
daemon_uid = daemon_euid,
"peer UID mismatch — dropping connection"
);
return None;
}
let peer_pid = cred.pid().map(|p| p as u32);
tracing::trace!(peer_uid, ?peer_pid, "peer credential check passed");
Some(PeerContext {
uid: peer_uid,
pid: peer_pid,
})
}
Err(e) => {
tracing::warn!(error = %e, "peer_cred() failed — dropping connection");
None
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum StaleCheckResult {
Clean,
StaleRemoved,
LiveDaemon {
pid: u32,
owner: DaemonOwner,
session: Uuid,
},
OrphanSocket,
}
pub fn check_and_cleanup_stale(root: &Path) -> StaleCheckResult {
let meta_path = metadata_path(root);
let sock_path = socket_path(root);
let has_metadata = meta_path.exists();
let has_socket = sock_path.exists();
if !has_metadata && !has_socket {
return StaleCheckResult::Clean;
}
if !has_metadata && has_socket {
return StaleCheckResult::OrphanSocket;
}
let metadata = match read_metadata(root) {
Some(m) => m,
None => {
tracing::warn!("daemon metadata corrupt — removing stale files");
let _ = std::fs::remove_file(&meta_path);
let _ = std::fs::remove_file(&sock_path);
return StaleCheckResult::StaleRemoved;
}
};
if is_pid_alive(metadata.pid) {
return StaleCheckResult::LiveDaemon {
pid: metadata.pid,
owner: metadata.owner,
session: metadata.session,
};
}
tracing::info!(
pid = metadata.pid,
owner = %metadata.owner,
"removing stale daemon files (PID dead)"
);
let _ = std::fs::remove_file(&sock_path);
let _ = std::fs::remove_file(&meta_path);
let _ = std::fs::remove_file(root.join("mati.starting"));
StaleCheckResult::StaleRemoved
}
const LIFECYCLE_FILENAME: &str = "lifecycle.log";
const MAX_LIFECYCLE_LINES: usize = 10_000;
const LIFECYCLE_TRIM_MAX_READ_BYTES: u64 = 64 * 1024 * 1024;
fn trim_lifecycle_log(root: &Path, max_lines: usize) {
let path = root.join(LIFECYCLE_FILENAME);
if let Ok(meta) = std::fs::metadata(&path) {
if meta.is_file() && meta.len() > LIFECYCLE_TRIM_MAX_READ_BYTES {
let _ = std::fs::write(&path, b"");
return;
}
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return, };
let line_count = content.lines().count();
if line_count <= max_lines {
return;
}
let skip = line_count - max_lines;
let kept: String = content.lines().skip(skip).flat_map(|l| [l, "\n"]).collect();
let tmp = path.with_extension("log.tmp");
if std::fs::write(&tmp, kept).is_err() {
return;
}
let _ = std::fs::rename(&tmp, &path);
}
const LIFECYCLE_MAX_LINE_BYTES: usize = 3900;
pub fn record_lifecycle_event(root: &Path, event: &str, detail: &str) {
use std::io::Write;
let path = root.join(LIFECYCLE_FILENAME);
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let pid = std::process::id();
let safe_detail: String = detail
.chars()
.map(|c| match c {
'\t' | '\n' | '\r' => ' ',
c => c,
})
.collect();
let mut line = format!("{ts}\t{pid}\t{event}\t{safe_detail}\n");
if line.len() > LIFECYCLE_MAX_LINE_BYTES {
let mut cut = LIFECYCLE_MAX_LINE_BYTES - 1;
while cut > 0 && !line.is_char_boundary(cut) {
cut -= 1;
}
line.truncate(cut);
line.push('\n');
}
let used_preopen = if let Some(pre) = LIFECYCLE_LOG_FILE.get() {
if pre.path == path {
let _ = (&pre.file).write_all(line.as_bytes());
true
} else {
false
}
} else {
false
};
if !used_preopen {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
let _ = f.write_all(line.as_bytes());
}
}
}
static PANIC_HOOK_ROOT: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
struct PreOpenedLog {
path: std::path::PathBuf,
file: std::fs::File,
pid_prefix: Vec<u8>,
}
static LIFECYCLE_LOG_FILE: std::sync::OnceLock<PreOpenedLog> = std::sync::OnceLock::new();
#[doc(hidden)]
pub fn is_lifecycle_log_preopened() -> bool {
LIFECYCLE_LOG_FILE.get().is_some()
}
fn u64_to_decimal_bytes(mut n: u64, out: &mut [u8]) -> usize {
if n == 0 {
if out.is_empty() {
return 0;
}
out[0] = b'0';
return 1;
}
let mut tmp = [0u8; 20];
let mut len = 0;
while n > 0 && len < tmp.len() {
tmp[len] = b'0' + (n % 10) as u8;
n /= 10;
len += 1;
}
let take = len.min(out.len());
for i in 0..take {
out[i] = tmp[len - 1 - i];
}
take
}
fn write_lifecycle_line(
out: &mut [u8; LIFECYCLE_MAX_LINE_BYTES],
ts: u64,
pid_prefix: &[u8],
event: &str,
detail_parts: &[&str],
) -> usize {
let cap = LIFECYCLE_MAX_LINE_BYTES - 1;
let mut pos: usize = 0;
fn push_raw(out: &mut [u8], pos: &mut usize, src: &[u8], cap: usize) {
let remaining = cap.saturating_sub(*pos);
let n = src.len().min(remaining);
out[*pos..*pos + n].copy_from_slice(&src[..n]);
*pos += n;
}
let mut ts_buf = [0u8; 20];
let ts_len = u64_to_decimal_bytes(ts, &mut ts_buf);
push_raw(out, &mut pos, &ts_buf[..ts_len], cap);
push_raw(out, &mut pos, b"\t", cap);
push_raw(out, &mut pos, pid_prefix, cap);
push_raw(out, &mut pos, event.as_bytes(), cap);
push_raw(out, &mut pos, b"\t", cap);
for (i, part) in detail_parts.iter().enumerate() {
if i > 0 {
push_raw(out, &mut pos, b" ", cap);
}
let bytes = part.as_bytes();
let remaining = cap.saturating_sub(pos);
let mut take = bytes.len().min(remaining);
if take < bytes.len() {
while take > 0 && !part.is_char_boundary(take) {
take -= 1;
}
}
for j in 0..take {
out[pos + j] = match bytes[j] {
b'\t' | b'\n' | b'\r' => b' ',
b => b,
};
}
pos += take;
}
out[pos] = b'\n';
pos + 1
}
fn record_lifecycle_event_no_alloc(root: &Path, event: &str, detail_parts: &[&str]) -> bool {
use std::io::Write;
let Some(pre) = LIFECYCLE_LOG_FILE.get() else {
return false;
};
if pre.path.parent() != Some(root) {
return false;
}
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut buf = [0u8; LIFECYCLE_MAX_LINE_BYTES];
let n = write_lifecycle_line(&mut buf, ts, &pre.pid_prefix, event, detail_parts);
(&pre.file).write_all(&buf[..n]).is_ok()
}
pub(crate) fn run_panic_cleanup(root: &Path, location: &str, payload: &str) {
let _ = std::fs::remove_file(socket_path(root));
let _ = std::fs::remove_file(metadata_path(root));
if !record_lifecycle_event_no_alloc(root, "panic", &[location, payload]) {
record_lifecycle_event(root, "panic", &format!("{location} {payload}"));
}
}
pub fn install_panic_hook(root: std::path::PathBuf) {
if PANIC_HOOK_ROOT.set(root.clone()).is_err() {
return;
}
trim_lifecycle_log(&root, MAX_LIFECYCLE_LINES);
let log_path = root.join(LIFECYCLE_FILENAME);
if let Ok(f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
let pid = std::process::id();
let mut pid_buf = [0u8; 20];
let pid_len = u64_to_decimal_bytes(pid as u64, &mut pid_buf);
let mut pid_prefix = Vec::with_capacity(pid_len + 1);
pid_prefix.extend_from_slice(&pid_buf[..pid_len]);
pid_prefix.push(b'\t');
let _ = LIFECYCLE_LOG_FILE.set(PreOpenedLog {
path: log_path,
file: f,
pid_prefix,
});
}
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if let Some(root) = PANIC_HOOK_ROOT.get() {
let location = info
.location()
.map(|l| format!("{}:{}", l.file(), l.line()))
.unwrap_or_else(|| "<unknown>".to_string());
let payload = info
.payload()
.downcast_ref::<&str>()
.copied()
.or_else(|| info.payload().downcast_ref::<String>().map(String::as_str))
.unwrap_or("<non-string panic>");
run_panic_cleanup(root, &location, payload);
}
default_hook(info);
}));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metadata_roundtrip() {
let meta = DaemonMetadata::new(DaemonOwner::Daemon);
let json = serde_json::to_string(&meta).unwrap();
let back: DaemonMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(back.pid, meta.pid);
assert_eq!(back.session, meta.session);
assert_eq!(back.owner, DaemonOwner::Daemon);
}
#[test]
fn metadata_mcp_owner_roundtrip() {
let meta = DaemonMetadata {
pid: 42,
session: Uuid::new_v4(),
owner: DaemonOwner::Mcp,
};
let json = serde_json::to_string(&meta).unwrap();
let back: DaemonMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(back.owner, DaemonOwner::Mcp);
}
#[test]
fn read_metadata_v2_format() {
let dir = tempfile::tempdir().unwrap();
let session = Uuid::new_v4();
let meta = DaemonMetadata {
pid: 1234,
session,
owner: DaemonOwner::Daemon,
};
publish_metadata(dir.path(), &meta).unwrap();
let read = read_metadata(dir.path()).unwrap();
assert_eq!(read.pid, 1234);
assert_eq!(read.session, session);
assert_eq!(read.owner, DaemonOwner::Daemon);
}
#[test]
fn read_metadata_legacy_v1_json() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("mati.pid"), r#"{"pid":5678,"owner":"mcp"}"#).unwrap();
let read = read_metadata(dir.path()).unwrap();
assert_eq!(read.pid, 5678);
assert_eq!(read.owner, DaemonOwner::Mcp);
assert!(read.session.is_nil());
}
#[test]
fn read_metadata_legacy_plain_pid() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("mati.pid"), "9999\n").unwrap();
let read = read_metadata(dir.path()).unwrap();
assert_eq!(read.pid, 9999);
assert_eq!(read.owner, DaemonOwner::Daemon);
assert!(read.session.is_nil());
}
#[test]
fn read_metadata_missing_returns_none() {
let dir = tempfile::tempdir().unwrap();
assert!(read_metadata(dir.path()).is_none());
}
#[test]
fn read_metadata_corrupt_returns_none() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("mati.pid"), "not json at all ~~~").unwrap();
assert!(read_metadata(dir.path()).is_none());
}
#[cfg(unix)]
#[test]
fn publish_metadata_sets_mode_0600() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let meta = DaemonMetadata::new(DaemonOwner::Daemon);
publish_metadata(dir.path(), &meta).unwrap();
let perms = std::fs::metadata(dir.path().join("mati.pid"))
.unwrap()
.permissions();
assert_eq!(
perms.mode() & 0o777,
0o600,
"metadata file should be mode 0600"
);
}
#[cfg(unix)]
#[test]
fn publish_metadata_is_atomic() {
let dir = tempfile::tempdir().unwrap();
let meta1 = DaemonMetadata {
pid: 1,
session: Uuid::new_v4(),
owner: DaemonOwner::Daemon,
};
publish_metadata(dir.path(), &meta1).unwrap();
let meta2 = DaemonMetadata {
pid: 2,
session: Uuid::new_v4(),
owner: DaemonOwner::Mcp,
};
publish_metadata(dir.path(), &meta2).unwrap();
let read = read_metadata(dir.path()).unwrap();
assert_eq!(read.pid, 2);
assert_eq!(read.owner, DaemonOwner::Mcp);
assert!(!dir.path().join("mati.pid.tmp").exists());
}
#[cfg(unix)]
#[test]
fn ensure_runtime_dir_sets_mode_0700() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("test_root");
ensure_runtime_dir(&root).unwrap();
let perms = std::fs::metadata(&root).unwrap().permissions();
assert_eq!(
perms.mode() & 0o777,
0o700,
"runtime dir should be mode 0700"
);
}
#[test]
fn is_pid_alive_for_current_process() {
assert!(is_pid_alive(std::process::id()));
}
#[test]
fn is_pid_alive_for_dead_pid() {
assert!(!is_pid_alive(4_000_000));
}
#[test]
fn stale_check_clean_when_no_files() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(check_and_cleanup_stale(dir.path()), StaleCheckResult::Clean);
}
#[test]
fn stale_check_removes_dead_pid() {
let dir = tempfile::tempdir().unwrap();
let meta = DaemonMetadata {
pid: 4_000_000, session: Uuid::new_v4(),
owner: DaemonOwner::Daemon,
};
publish_metadata(dir.path(), &meta).unwrap();
std::fs::write(dir.path().join("mati.sock"), "").unwrap();
let result = check_and_cleanup_stale(dir.path());
assert_eq!(result, StaleCheckResult::StaleRemoved);
assert!(!dir.path().join("mati.pid").exists());
assert!(!dir.path().join("mati.sock").exists());
}
#[test]
fn stale_check_live_daemon_detected() {
let dir = tempfile::tempdir().unwrap();
let meta = DaemonMetadata {
pid: std::process::id(), session: Uuid::new_v4(),
owner: DaemonOwner::Daemon,
};
publish_metadata(dir.path(), &meta).unwrap();
match check_and_cleanup_stale(dir.path()) {
StaleCheckResult::LiveDaemon { pid, .. } => {
assert_eq!(pid, std::process::id());
}
other => panic!("expected LiveDaemon, got {:?}", other),
}
}
#[test]
fn stale_check_orphan_socket() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("mati.sock"), "").unwrap();
assert_eq!(
check_and_cleanup_stale(dir.path()),
StaleCheckResult::OrphanSocket
);
}
#[test]
fn stale_check_corrupt_metadata_cleaned_up() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("mati.pid"), "garbage!!!").unwrap();
std::fs::write(dir.path().join("mati.sock"), "").unwrap();
let result = check_and_cleanup_stale(dir.path());
assert_eq!(result, StaleCheckResult::StaleRemoved);
assert!(!dir.path().join("mati.pid").exists());
assert!(!dir.path().join("mati.sock").exists());
}
#[cfg(unix)]
#[tokio::test]
async fn peer_cred_accepts_same_uid() {
let dir = tempfile::tempdir().unwrap();
let sock_path = dir.path().join("test.sock");
let listener = tokio::net::UnixListener::bind(&sock_path).unwrap();
let connect_fut = tokio::net::UnixStream::connect(&sock_path);
let accept_fut = listener.accept();
let (client_result, accept_result) = tokio::join!(connect_fut, accept_fut);
let _client = client_result.unwrap();
let (server_stream, _) = accept_result.unwrap();
let daemon_euid = current_euid();
let peer = check_peer_cred(&server_stream, daemon_euid);
assert!(
peer.is_some(),
"same-user connection should pass peer check"
);
let ctx = peer.unwrap();
assert_eq!(ctx.uid, daemon_euid);
assert!(ctx.pid.is_some(), "peer PID should be available");
}
#[cfg(unix)]
#[tokio::test]
async fn peer_cred_rejects_uid_mismatch() {
let dir = tempfile::tempdir().unwrap();
let sock_path = dir.path().join("test_mismatch.sock");
let listener = tokio::net::UnixListener::bind(&sock_path).unwrap();
let connect_fut = tokio::net::UnixStream::connect(&sock_path);
let accept_fut = listener.accept();
let (client_result, accept_result) = tokio::join!(connect_fut, accept_fut);
let _client = client_result.unwrap();
let (server_stream, _) = accept_result.unwrap();
let fake_euid = current_euid().wrapping_add(1);
let peer = check_peer_cred(&server_stream, fake_euid);
assert!(peer.is_none(), "mismatched UID should be rejected");
}
#[test]
fn lifecycle_log_appends_one_line_per_event() {
let dir = tempfile::tempdir().unwrap();
record_lifecycle_event(dir.path(), "start", "owner=mcp");
record_lifecycle_event(dir.path(), "shutdown", "reason=signal");
let contents = std::fs::read_to_string(dir.path().join("lifecycle.log")).unwrap();
let lines: Vec<&str> = contents.lines().collect();
assert_eq!(lines.len(), 2, "exactly two events recorded");
for line in &lines {
let cols: Vec<&str> = line.split('\t').collect();
assert_eq!(cols.len(), 4, "each line has 4 tab-separated fields");
assert!(cols[0].parse::<u64>().is_ok());
assert!(cols[1].parse::<u32>().is_ok());
}
assert!(lines[0].contains("\tstart\towner=mcp"));
assert!(lines[1].contains("\tshutdown\treason=signal"));
}
#[test]
fn lifecycle_log_strips_newlines_and_tabs_in_detail() {
let dir = tempfile::tempdir().unwrap();
record_lifecycle_event(dir.path(), "panic", "line1\nline2\twith tab\rcr");
let contents = std::fs::read_to_string(dir.path().join("lifecycle.log")).unwrap();
assert_eq!(contents.matches('\n').count(), 1);
assert!(contents.contains("line1 line2 with tab cr"));
}
#[test]
fn lifecycle_log_silently_succeeds_when_dir_missing() {
let dir = tempfile::tempdir().unwrap();
let bogus = dir.path().join("nonexistent-subdir");
record_lifecycle_event(&bogus, "start", "x");
assert!(!bogus.join("lifecycle.log").exists());
}
#[test]
fn lifecycle_log_caps_line_below_pipe_buf() {
let dir = tempfile::tempdir().unwrap();
let huge_detail: String = "é".repeat(5_000); record_lifecycle_event(dir.path(), "panic", &huge_detail);
let log = std::fs::read_to_string(dir.path().join("lifecycle.log")).unwrap();
assert!(
log.len() <= LIFECYCLE_MAX_LINE_BYTES,
"line on disk ({} bytes) must not exceed cap ({})",
log.len(),
LIFECYCLE_MAX_LINE_BYTES
);
assert!(
log.ends_with('\n'),
"truncated line must still end with newline so lines() yields one record"
);
assert!(
log.contains("\tpanic\t"),
"event tag must survive truncation (it sits in the prefix)"
);
assert!(
log.is_char_boundary(log.len()),
"truncation must land on UTF-8 char boundary"
);
}
#[test]
fn run_panic_cleanup_removes_sock_pid_and_appends_lifecycle_event() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("mati.sock"), "").unwrap();
std::fs::write(dir.path().join("mati.pid"), r#"{"pid":42}"#).unwrap();
run_panic_cleanup(dir.path(), "src/example.rs:99", "boom");
assert!(
!dir.path().join("mati.sock").exists(),
"panic hook must remove mati.sock so sibling daemons can rebind"
);
assert!(
!dir.path().join("mati.pid").exists(),
"panic hook must remove mati.pid so sibling stale-checks see no live daemon"
);
let log = std::fs::read_to_string(dir.path().join("lifecycle.log")).unwrap();
assert!(log.contains("\tpanic\t"), "event tagged 'panic'");
assert!(log.contains("src/example.rs:99"), "location preserved");
assert!(log.contains("boom"), "payload preserved");
}
#[test]
fn run_panic_cleanup_is_safe_when_files_already_absent() {
let dir = tempfile::tempdir().unwrap();
run_panic_cleanup(dir.path(), "src/x.rs:1", "noop");
assert!(dir.path().join("lifecycle.log").exists());
}
#[test]
fn trim_lifecycle_log_keeps_last_n_lines() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(LIFECYCLE_FILENAME);
let body: String = (0..100)
.map(|i| format!("{i}\t{i}\tevent{i}\tdetail{i}\n"))
.collect();
std::fs::write(&path, body).unwrap();
trim_lifecycle_log(dir.path(), 10);
let after = std::fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = after.lines().collect();
assert_eq!(lines.len(), 10, "trimmed log should have exactly N lines");
assert!(
lines[0].contains("\tevent90\t"),
"first kept line: {}",
lines[0]
);
assert!(
lines[9].contains("\tevent99\t"),
"last kept line: {}",
lines[9]
);
assert!(!path.with_extension("log.tmp").exists());
}
#[test]
fn trim_lifecycle_log_noop_when_under_cap() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(LIFECYCLE_FILENAME);
let body = "0\t0\tstart\tdetail\n1\t0\tstop\tclean\n";
std::fs::write(&path, body).unwrap();
let before = std::fs::read(&path).unwrap();
trim_lifecycle_log(dir.path(), 10);
let after = std::fs::read(&path).unwrap();
assert_eq!(before, after, "trim must be a no-op when under cap");
}
#[test]
fn trim_lifecycle_log_truncates_pathologically_huge_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(LIFECYCLE_FILENAME);
{
use std::io::{Seek, SeekFrom, Write};
let mut f = std::fs::File::create(&path).unwrap();
f.seek(SeekFrom::Start(LIFECYCLE_TRIM_MAX_READ_BYTES + 1))
.unwrap();
f.write_all(b"x").unwrap();
}
let pre_size = std::fs::metadata(&path).unwrap().len();
assert!(
pre_size > LIFECYCLE_TRIM_MAX_READ_BYTES,
"test setup: file must exceed the read cap"
);
trim_lifecycle_log(dir.path(), 10);
let post_meta = std::fs::metadata(&path).unwrap();
assert!(
post_meta.is_file(),
"lifecycle.log should still exist after pathological trim"
);
assert_eq!(
post_meta.len(),
0,
"pathologically large lifecycle.log must be truncated to empty so startup does not OOM"
);
assert!(!path.with_extension("log.tmp").exists());
}
#[test]
fn trim_lifecycle_log_size_guard_does_not_fire_under_cap() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(LIFECYCLE_FILENAME);
let body: String = (0..100)
.map(|i| format!("{i}\t{i}\tevent{i}\tdetail{i}\n"))
.collect();
std::fs::write(&path, &body).unwrap();
trim_lifecycle_log(dir.path(), 10);
let after = std::fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = after.lines().collect();
assert_eq!(
lines.len(),
10,
"normal trim path must run for sub-cap files"
);
assert!(lines[0].contains("event90"));
assert!(lines[9].contains("event99"));
}
#[test]
fn trim_lifecycle_log_silently_succeeds_on_missing_log() {
let dir = tempfile::tempdir().unwrap();
trim_lifecycle_log(dir.path(), 10);
assert!(!dir.path().join(LIFECYCLE_FILENAME).exists());
}
#[test]
fn install_panic_hook_is_idempotent() {
let dir = tempfile::tempdir().unwrap();
install_panic_hook(dir.path().to_path_buf());
install_panic_hook(dir.path().join("a-different-root"));
}
#[test]
fn u64_to_decimal_bytes_matches_format() {
for n in [
0u64,
1,
9,
10,
99,
100,
12345,
1_700_000_000,
u64::MAX / 2,
u64::MAX,
] {
let mut buf = [0u8; 20];
let len = u64_to_decimal_bytes(n, &mut buf);
assert_eq!(
std::str::from_utf8(&buf[..len]).unwrap(),
n.to_string(),
"decimal mismatch for {n}"
);
}
}
#[test]
fn no_alloc_panic_format_matches_heap_format() {
let ts: u64 = 1_700_000_000;
let pid: u32 = 42;
let pid_prefix = format!("{pid}\t");
let event = "panic";
fn heap_format(ts: u64, pid_prefix: &str, event: &str, detail: &str) -> String {
let safe_detail: String = detail
.chars()
.map(|c| match c {
'\t' | '\n' | '\r' => ' ',
c => c,
})
.collect();
let mut line = format!("{ts}\t{pid_prefix}{event}\t{safe_detail}\n");
if line.len() > LIFECYCLE_MAX_LINE_BYTES {
let mut cut = LIFECYCLE_MAX_LINE_BYTES - 1;
while cut > 0 && !line.is_char_boundary(cut) {
cut -= 1;
}
line.truncate(cut);
line.push('\n');
}
line
}
let location = "src/mcp/server.rs:128";
let payload = "boom!";
let detail = format!("{location} {payload}");
let heap = heap_format(ts, &pid_prefix, event, &detail);
let mut buf = [0u8; LIFECYCLE_MAX_LINE_BYTES];
let n = write_lifecycle_line(
&mut buf,
ts,
pid_prefix.as_bytes(),
event,
&[location, payload],
);
assert_eq!(
std::str::from_utf8(&buf[..n]).unwrap(),
heap,
"panic-path format must match heap path for typical input"
);
let location_2 = "src/x.rs:1";
let payload_2 = "line1\nline2\twith tab\rcr";
let detail_2 = format!("{location_2} {payload_2}");
let heap_2 = heap_format(ts, &pid_prefix, event, &detail_2);
let mut buf_2 = [0u8; LIFECYCLE_MAX_LINE_BYTES];
let n2 = write_lifecycle_line(
&mut buf_2,
ts,
pid_prefix.as_bytes(),
event,
&[location_2, payload_2],
);
assert_eq!(
std::str::from_utf8(&buf_2[..n2]).unwrap(),
heap_2,
"panic-path format must match heap path with embedded control chars"
);
let heap_3 = heap_format(ts, &pid_prefix, "start", "");
let mut buf_3 = [0u8; LIFECYCLE_MAX_LINE_BYTES];
let n3 = write_lifecycle_line(&mut buf_3, ts, pid_prefix.as_bytes(), "start", &[""]);
assert_eq!(
std::str::from_utf8(&buf_3[..n3]).unwrap(),
heap_3,
"panic-path format must match heap path with empty detail"
);
}
#[test]
fn record_lifecycle_event_no_alloc_returns_false_for_unknown_root() {
let dir = tempfile::tempdir().unwrap();
assert!(!record_lifecycle_event_no_alloc(
dir.path(),
"smoke",
&["from-tests"]
));
}
#[test]
fn peer_context_pid_is_optional() {
let ctx = PeerContext {
uid: 501,
pid: None,
};
assert!(ctx.pid.is_none());
let ctx2 = PeerContext {
uid: 501,
pid: Some(1234),
};
assert_eq!(ctx2.pid, Some(1234));
}
}