use super::*;
pub const STARTING_STALE_SECS: u64 = 30;
pub(super) fn format_sentinel(ts: u64, pid: u32) -> String {
format!("{ts} {pid}\n")
}
pub fn parse_sentinel(content: &str) -> Option<(u64, u32)> {
let mut parts = content.split_whitespace();
let ts = parts.next()?.parse::<u64>().ok()?;
let pid = parts.next()?.parse::<u32>().ok()?;
Some((ts, pid))
}
pub(super) fn check_starting_peer_active(mati_root: &Path) -> bool {
let starting_path = mati_root.join("mati.starting");
let content = match std::fs::read_to_string(&starting_path) {
Ok(c) => c,
Err(_) => return false, };
let now = wall_secs();
let active = if let Some((_ts, pid)) = parse_sentinel(&content) {
pid != std::process::id() && mati_core::mcp::metadata::is_pid_alive(pid)
} else if let Ok(ts) = content.trim().parse::<u64>() {
now.saturating_sub(ts) < STARTING_STALE_SECS
} else {
false
};
if !active {
let _ = std::fs::remove_file(&starting_path);
}
active
}
pub fn mati_root_for(cwd: &Path) -> Result<PathBuf> {
mati_root_for_ident(&RepoIdent::discover(cwd), cwd)
}
pub fn mati_root_for_ident(ident: &RepoIdent, fallback: &Path) -> Result<PathBuf> {
let slug = ident.slug(fallback);
Ok(mati_core::store::mati_home()?.join(slug))
}
pub fn read_pid_file(root: &Path) -> Option<(u32, String)> {
let content = std::fs::read_to_string(root.join("mati.pid")).ok()?;
let trimmed = content.trim();
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 = val
.get("owner")
.and_then(|v| v.as_str())
.unwrap_or("daemon")
.to_string();
return Some((pid, owner));
}
if let Ok(pid) = trimmed.parse::<u32>() {
return Some((pid, "daemon".to_string()));
}
None
}
pub(super) fn project_root() -> Result<PathBuf> {
let cwd = std::env::current_dir()?;
mati_root_for(&cwd)
}
pub(super) fn wall_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub(super) use mati_core::mcp::metadata::KillOutcome as ExitOutcome;
#[derive(Debug)]
pub(super) enum DaemonState {
Empty,
StaleFiles,
LiveOwnerDaemon { pid: u32 },
LiveOwnerMcp { pid: u32 },
LiveOwnerUnknown {
pid: Option<u32>,
from_metadata: bool,
},
StartingSentinelOnly { pid: u32 },
Unresponsive { pid: u32 },
}
fn live_starting_pid(root: &Path) -> Option<u32> {
let content = std::fs::read_to_string(root.join("mati.starting")).ok()?;
let (_, pid) = parse_sentinel(&content)?;
if mati_core::mcp::metadata::is_pid_alive(pid) {
Some(pid)
} else {
None
}
}
#[cfg(unix)]
fn lsof_owning_pid(sock_path: &Path) -> Option<u32> {
let out = std::process::Command::new("lsof")
.args(["-tU"])
.arg(sock_path)
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !out.status.success() {
return None;
}
String::from_utf8_lossy(&out.stdout)
.split_whitespace()
.find_map(|tok| tok.parse::<u32>().ok())
}
#[cfg(not(unix))]
fn lsof_owning_pid(_sock_path: &Path) -> Option<u32> {
None
}
pub(super) async fn classify_daemon(root: &Path, force: bool) -> DaemonState {
let pid_path = root.join("mati.pid");
let sock_path = root.join("mati.sock");
let starting_path = root.join("mati.starting");
let has_pid = pid_path.exists();
let has_sock = sock_path.exists();
let has_starting = starting_path.exists();
if !has_pid && !has_sock {
if has_starting {
if let Some(pid) = live_starting_pid(root) {
return DaemonState::StartingSentinelOnly { pid };
}
}
return DaemonState::Empty;
}
let pid_info = read_pid_file(root);
match pid_info {
Some((pid, owner)) => {
if !mati_core::mcp::metadata::is_pid_alive(pid) {
return DaemonState::StaleFiles;
}
if owner == "mcp" {
return DaemonState::LiveOwnerMcp { pid };
}
if has_sock {
match daemon_result(root, "ping", serde_json::json!({})).await {
DaemonResult::Ok(_) => DaemonState::LiveOwnerDaemon { pid },
DaemonResult::Unresponsive | DaemonResult::PermissionDenied => {
DaemonState::Unresponsive { pid }
}
DaemonResult::NotRunning | DaemonResult::StaleSocket => DaemonState::StaleFiles,
}
} else {
DaemonState::LiveOwnerDaemon { pid }
}
}
None => {
if !has_sock {
return DaemonState::StaleFiles;
}
match daemon_result(root, "ping", serde_json::json!({})).await {
DaemonResult::Ok(_) => {
let meta_pid = mati_core::mcp::metadata::read_metadata(root).map(|m| m.pid);
if meta_pid.is_some() {
return DaemonState::LiveOwnerUnknown {
pid: meta_pid,
from_metadata: true,
};
}
let lsof_pid = if force {
lsof_owning_pid(&sock_path)
} else {
None
};
DaemonState::LiveOwnerUnknown {
pid: lsof_pid,
from_metadata: false,
}
}
DaemonResult::StaleSocket | DaemonResult::NotRunning => DaemonState::StaleFiles,
DaemonResult::Unresponsive | DaemonResult::PermissionDenied => {
let pid = mati_core::mcp::metadata::read_metadata(root).map(|m| m.pid);
match pid {
Some(pid) => DaemonState::Unresponsive { pid },
None => DaemonState::StaleFiles,
}
}
}
}
}
}
pub(super) use mati_core::mcp::metadata::kill_and_wait;
#[cfg(unix)]
pub(super) fn send_sigterm_only(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))]
pub(super) fn send_sigterm_only(_pid: u32) -> bool {
false
}
#[cfg(unix)]
pub(super) fn send_sigkill_only(pid: u32) -> bool {
let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
if ret == 0 {
return true;
}
let errno = std::io::Error::last_os_error().raw_os_error();
matches!(errno, Some(libc::ESRCH))
}
#[cfg(not(unix))]
pub(super) fn send_sigkill_only(_pid: u32) -> bool {
false
}
pub(super) const RECORDED_SERVE_SCAN_MAX_BYTES: u64 = 2 * 1024 * 1024;
pub(super) fn read_tail(path: &Path, max_bytes: u64) -> std::io::Result<String> {
use std::io::{Read, Seek, SeekFrom};
let mut file = std::fs::File::open(path)?;
let len = file.metadata()?.len();
if len <= max_bytes {
let mut s = String::new();
file.read_to_string(&mut s)?;
return Ok(s);
}
file.seek(SeekFrom::Start(len - max_bytes))?;
let mut buf = Vec::with_capacity(max_bytes as usize);
file.read_to_end(&mut buf)?;
let text = String::from_utf8_lossy(&buf);
Ok(match text.find('\n') {
Some(i) => text[i + 1..].to_string(),
None => String::new(),
})
}
pub(super) fn recorded_serve_pids(root: &Path) -> std::collections::HashSet<u32> {
let mut live = std::collections::HashSet::new();
let Ok(contents) = read_tail(&root.join("lifecycle.log"), RECORDED_SERVE_SCAN_MAX_BYTES) else {
return live;
};
for line in contents.lines() {
let mut cols = line.split('\t');
let (Some(_ts), Some(pid), Some(event)) = (cols.next(), cols.next(), cols.next()) else {
continue;
};
let Ok(pid) = pid.trim().parse::<u32>() else {
continue;
};
let detail = cols.next().unwrap_or("");
match event {
"serve_start" if detail.contains("owner=proxy") => {
live.insert(pid);
}
"serve_shutdown" | "serve_failed" | "panic" => {
live.remove(&pid);
}
_ => {}
}
}
live
}
pub(super) fn serve_pids_to_kill(root: &Path, running: &[u32]) -> Vec<u32> {
let recorded = recorded_serve_pids(root);
let my_pid = std::process::id();
running
.iter()
.copied()
.filter(|pid| *pid != my_pid && recorded.contains(pid))
.collect()
}
pub(super) async fn kill_mati_serve_processes(root: &Path) {
let output = match std::process::Command::new("pgrep")
.arg("-f")
.arg("mati serve")
.output()
{
Ok(o) => o,
Err(e) => {
tracing::warn!(
"kill_mati_serve_processes: pgrep failed: {e} \
(is pgrep installed? skipping --include-mcp cleanup)"
);
eprintln!(
"[mati] warning: pgrep not available; could not locate `mati serve` processes"
);
return;
}
};
if !output.status.success() {
return;
}
let running: Vec<u32> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|l| l.trim().parse::<u32>().ok())
.collect();
let targets = serve_pids_to_kill(root, &running);
let spared = running.len().saturating_sub(targets.len());
let mut killed: Vec<u32> = Vec::new();
for pid in targets {
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
if ret == 0 {
killed.push(pid);
} else {
let errno = std::io::Error::last_os_error().raw_os_error();
if !matches!(errno, Some(libc::ESRCH)) {
tracing::warn!(pid, ?errno, "kill_mati_serve_processes: SIGKILL failed");
}
}
}
}
if spared > 0 {
println!(
"mati daemon: --include-mcp left {spared} `mati serve` process(es) alone \
(not recorded against this store)"
);
}
if !killed.is_empty() {
let pid_list = killed
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(",");
println!(
"mati daemon: --include-mcp killed {} serve proxy/proxies (pid={pid_list})",
killed.len()
);
mati_core::mcp::metadata::record_lifecycle_event(
root,
"stop_include_mcp",
&format!("killed={} pids={pid_list}", killed.len()),
);
}
}
pub(super) async fn wait_for_files_removed(root: &Path) -> bool {
const FILE_POLL_BUDGET: Duration = Duration::from_millis(500);
const FILE_POLL_INTERVAL: Duration = Duration::from_millis(20);
let sock = root.join("mati.sock");
let pid = root.join("mati.pid");
let starting = root.join("mati.starting");
let deadline = std::time::Instant::now() + FILE_POLL_BUDGET;
while std::time::Instant::now() < deadline {
if !sock.exists() && !pid.exists() {
let _ = std::fs::remove_file(&starting);
return true;
}
tokio::time::sleep(FILE_POLL_INTERVAL).await;
}
let _ = std::fs::remove_file(&sock);
let _ = std::fs::remove_file(&pid);
let _ = std::fs::remove_file(&starting);
false
}