use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, broadcast};
use crate::acp::client::AcpClient;
#[derive(Clone, Debug)]
pub struct AcpProcessEvent {
pub session_id: String,
pub alive: bool,
}
#[derive(Clone)]
pub struct AcpSupervisor {
clients: Arc<Mutex<HashMap<String, Arc<AcpClient>>>>,
events: broadcast::Sender<AcpProcessEvent>,
}
impl Default for AcpSupervisor {
fn default() -> Self {
let (events, _) = broadcast::channel(64);
Self { clients: Arc::new(Mutex::new(HashMap::new())), events }
}
}
impl AcpSupervisor {
pub async fn insert(&self, session_id: String, client: Arc<AcpClient>) {
self.clients.lock().await.insert(session_id.clone(), client);
let _ = self.events.send(AcpProcessEvent { session_id, alive: true });
}
pub async fn get(&self, session_id: &str) -> Option<Arc<AcpClient>> {
self.clients.lock().await.get(session_id).cloned()
}
pub async fn snapshot(&self) -> Vec<(String, Arc<AcpClient>)> {
self.clients.lock().await.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
}
pub async fn dispose(&self, session_id: &str) -> Option<Arc<AcpClient>> {
let removed = self.clients.lock().await.remove(session_id);
if removed.is_some() {
let _ = self
.events
.send(AcpProcessEvent { session_id: session_id.to_string(), alive: false });
}
removed
}
pub fn process_event_subscribe(&self) -> broadcast::Receiver<AcpProcessEvent> {
self.events.subscribe()
}
pub async fn shutdown_all(&self) {
let clients: Vec<_> = self.clients.lock().await.drain().collect();
for (_, client) in clients {
client.shutdown().await;
}
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
use crate::acp::agent_proc::spawn_test_lock_async;
use crate::acp::test_support::{
KILL_TIMEOUT, agent_for, proc_dead, proc_reaped, spawn_connect, unique_dir, wait_until,
write_fake_agent,
};
#[tokio::test]
async fn insert_get_snapshot_dispose_broadcasts_lifecycle_events() {
let _guard = spawn_test_lock_async().await;
let dir = unique_dir("sup-lifecycle");
let workspace = dir.join("ws");
std::fs::create_dir_all(&workspace).expect("create workspace");
let script = write_fake_agent(&dir);
let client = Arc::new(spawn_connect(agent_for(&script, "live", &dir), workspace).await);
let sup = AcpSupervisor::default();
let mut events = sup.process_event_subscribe();
sup.insert("s1".to_string(), client.clone()).await;
let got = sup.get("s1").await.expect("insert 后 get 应命中");
assert!(Arc::ptr_eq(&got, &client), "get 应返回同一 client 实例");
let snap = sup.snapshot().await;
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].0, "s1");
let ev = events.try_recv().expect("insert 应广播 alive=true 事件");
assert_eq!(ev.session_id, "s1");
assert!(ev.alive);
let removed = sup.dispose("s1").await;
assert!(removed.is_some(), "dispose 应返回被移除的 client");
assert!(sup.get("s1").await.is_none(), "dispose 后 get 应为 None");
assert!(sup.snapshot().await.is_empty(), "dispose 后快照应为空");
let ev = events.try_recv().expect("dispose 应广播 alive=false 事件");
assert_eq!(ev.session_id, "s1");
assert!(!ev.alive);
assert!(sup.dispose("s1").await.is_none());
assert!(events.try_recv().is_err(), "dispose 未命中不应广播事件");
client.shutdown().await;
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn shutdown_all_drains_clients_and_kills_agents() {
let _guard = spawn_test_lock_async().await;
let dir = unique_dir("sup-shutdown");
let workspace = dir.join("ws");
std::fs::create_dir_all(&workspace).expect("create workspace");
let script = write_fake_agent(&dir);
let sup = AcpSupervisor::default();
let mut pids = Vec::new();
for id in ["a", "b"] {
let client =
Arc::new(spawn_connect(agent_for(&script, "live", &dir), workspace.clone()).await);
pids.push(client.agent_pid().expect("D1:live 模式必须捕获 pid"));
sup.insert(id.to_string(), client).await;
}
assert_eq!(sup.snapshot().await.len(), 2);
sup.shutdown_all().await;
assert!(sup.snapshot().await.is_empty(), "shutdown_all 后注册表应排空");
for pid in pids {
assert!(
wait_until(|| proc_dead(pid), KILL_TIMEOUT).await,
"shutdown_all 后 pid {pid} 未在 {KILL_TIMEOUT:?} 内死亡"
);
assert!(
wait_until(|| proc_reaped(pid), crate::acp::test_support::REAP_TIMEOUT).await,
"shutdown_all 后 pid {pid} 未被回收(僵尸残留)"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
}