use super::*;
#[allow(clippy::too_many_arguments)]
pub(super) async fn serve_loop_graceful(
graph: Arc<tokio::sync::RwLock<Graph>>,
policy_matcher: Arc<tokio::sync::RwLock<PolicyMatcherSet>>,
repo_root: &Path,
listener: &UnixListener,
last_wall: &AtomicU64,
active_connections: &Arc<AtomicU64>,
shutdown: &mati_core::mcp::server::Shutdown,
daemon_euid: u32,
daemon_session: uuid::Uuid,
) {
let semaphore = Arc::new(tokio::sync::Semaphore::new(MAX_DAEMON_CONNECTIONS));
let mut in_flight: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
let repo_root_arc: Arc<PathBuf> = Arc::new(repo_root.to_path_buf());
struct ConnGuard(Arc<AtomicU64>);
impl Drop for ConnGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::Relaxed);
}
}
'accept: loop {
while let Some(res) = in_flight.try_join_next() {
if let Err(e) = res {
if e.is_panic() {
tracing::error!(error = ?e, "daemon: handler panicked");
break 'accept;
}
}
}
let permit = tokio::select! {
biased;
_ = shutdown.wait() => break 'accept,
res = Arc::clone(&semaphore).acquire_owned() => match res {
Ok(p) => p,
Err(_) => break 'accept,
},
};
let stream = tokio::select! {
biased;
_ = shutdown.wait() => break 'accept,
res = listener.accept() => match res {
Ok((s, _)) => s,
Err(e) => {
tracing::warn!(error = %e, "daemon: accept error");
drop(permit);
continue 'accept;
}
},
};
last_wall.store(wall_secs(), Ordering::Relaxed);
let peer = match mati_core::mcp::metadata::check_peer_cred(&stream, daemon_euid) {
Some(p) => p,
None => {
drop(permit);
continue;
}
};
let graph_clone = Arc::clone(&graph);
let policy_matcher_clone = Arc::clone(&policy_matcher);
let repo_root_clone = Arc::clone(&repo_root_arc);
active_connections.fetch_add(1, Ordering::Relaxed);
let conn_guard = ConnGuard(Arc::clone(active_connections));
in_flight.spawn(async move {
let _permit = permit;
let _conn_guard = conn_guard;
if let Err(e) = mati_core::mcp::server::socket_handle_connection(
graph_clone,
policy_matcher_clone,
&repo_root_clone,
stream,
peer,
daemon_session,
)
.await
{
tracing::warn!(error = %e, "daemon: connection error");
}
});
}
let drained = in_flight.len();
if drained > 0 {
tracing::debug!("daemon: draining {drained} in-flight handler(s)");
}
const DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
let drain = tokio::time::timeout(DRAIN_TIMEOUT, async {
while in_flight.join_next().await.is_some() {}
})
.await;
if drain.is_err() {
tracing::warn!(
remaining = in_flight.len(),
"daemon: drain timed out after {DRAIN_TIMEOUT:?} — aborting handlers"
);
in_flight.abort_all();
let _ = tokio::time::timeout(Duration::from_secs(1), async {
while in_flight.join_next().await.is_some() {}
})
.await;
}
shutdown.signal();
}
const PING_RESPONSE_TIMEOUT: Duration = Duration::from_secs(2);
const REQUEST_RESPONSE_TIMEOUT: Duration = Duration::from_secs(10);
pub async fn daemon_v2(root: &Path, cmd: mati_core::mcp::protocol::Command) -> DaemonResult {
let v2_cmd = match serde_json::to_value(&cmd) {
Ok(v) => v,
Err(_) => return DaemonResult::Unresponsive,
};
send_v2_raw(root, v2_cmd, REQUEST_RESPONSE_TIMEOUT).await
}
pub async fn daemon_result(root: &Path, cmd: &str, args: serde_json::Value) -> DaemonResult {
let v2_cmd = mati_core::mcp::protocol::v1_to_v2_command(cmd, &args);
let timeout = if cmd == "ping" {
PING_RESPONSE_TIMEOUT
} else {
REQUEST_RESPONSE_TIMEOUT
};
send_v2_raw(root, v2_cmd, timeout).await
}
async fn send_v2_raw(
root: &Path,
v2_cmd: serde_json::Value,
response_timeout: Duration,
) -> DaemonResult {
let sock_path = root.join("mati.sock");
if sock_path.as_os_str().len() > UNIX_SOCK_PATH_MAX {
tracing::warn!(
path = %sock_path.display(),
"daemon: socket path exceeds Unix limit — daemon unavailable"
);
return DaemonResult::NotRunning;
}
if !sock_path.exists() {
return DaemonResult::NotRunning;
}
let stream = match UnixStream::connect(&sock_path).await {
Ok(s) => s,
Err(e) => {
if e.kind() == std::io::ErrorKind::PermissionDenied {
return DaemonResult::PermissionDenied;
}
let is_refused = e.kind() == std::io::ErrorKind::ConnectionRefused;
if is_refused {
use mati_core::mcp::metadata::{self as meta, StaleCheckResult};
match meta::check_and_cleanup_stale(root) {
StaleCheckResult::StaleRemoved | StaleCheckResult::Clean => {
tracing::debug!("daemon: removed stale socket");
return DaemonResult::StaleSocket;
}
StaleCheckResult::OrphanSocket => {
let _ = std::fs::remove_file(&sock_path);
tracing::debug!("daemon: removed orphan socket");
return DaemonResult::StaleSocket;
}
StaleCheckResult::LiveDaemon { .. } => {
tracing::warn!("daemon: socket refused but PID alive — unresponsive");
return DaemonResult::Unresponsive;
}
}
}
tracing::debug!(error = %e, "daemon: connect failed, treating as not running");
return DaemonResult::NotRunning;
}
};
let daemon_session = mati_core::mcp::metadata::read_metadata(root)
.map(|m| m.session)
.unwrap_or_else(uuid::Uuid::nil);
let v2_request = serde_json::json!({
"v": mati_core::mcp::protocol::PROTOCOL_VERSION,
"id": uuid::Uuid::new_v4(),
"session": daemon_session,
"cmd": v2_cmd,
});
let (reader, mut writer) = stream.into_split();
let mut bytes = match serde_json::to_vec(&v2_request) {
Ok(b) => b,
Err(_) => return DaemonResult::Unresponsive,
};
bytes.push(b'\n');
if writer.write_all(&bytes).await.is_err() {
return DaemonResult::Unresponsive;
}
if writer.shutdown().await.is_err() {
return DaemonResult::Unresponsive;
}
let mut buf_reader = BufReader::new(reader);
let mut line = String::new();
match tokio::time::timeout(response_timeout, buf_reader.read_line(&mut line)).await {
Ok(Ok(n)) if n > 0 => {}
_ => return DaemonResult::Unresponsive,
}
let resp: serde_json::Value = match serde_json::from_str(line.trim()) {
Ok(v) => v,
Err(_) => return DaemonResult::Unresponsive,
};
match resp.get("status").and_then(|s| s.as_str()) {
Some("ok") => {
let data = resp.get("data").cloned().unwrap_or(serde_json::Value::Null);
DaemonResult::Ok(serde_json::json!({"ok": true, "v": 2, "data": data}))
}
Some("err") => {
let code = resp
.get("code")
.and_then(|c| c.as_str())
.unwrap_or("internal");
let message = resp
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error");
if code == "session_mismatch" {
tracing::debug!("daemon: session mismatch — daemon may have restarted");
}
DaemonResult::Ok(
serde_json::json!({"ok": false, "v": 2, "error": message, "code": code}),
)
}
_ => DaemonResult::Unresponsive,
}
}
#[allow(dead_code)]
pub async fn daemon_get(root: &Path, key: &str) -> Option<String> {
match daemon_result(root, "get", serde_json::json!({ "key": key })).await {
DaemonResult::Ok(resp) => {
if resp.get("ok") != Some(&serde_json::Value::Bool(true)) {
return None;
}
match resp.get("data") {
Some(d) if d.is_null() => Some("null".to_string()),
Some(d) => Some(d.to_string()),
None => None,
}
}
DaemonResult::NotRunning | DaemonResult::StaleSocket => None,
DaemonResult::Unresponsive | DaemonResult::PermissionDenied => None,
}
}