use std::collections::HashMap;
use tokio::sync::Mutex;
use serde::{Deserialize, Serialize};
use synwire_core::BoxFuture;
use synwire_core::vfs::error::VfsError;
use synwire_core::vfs::types::{ExecuteResponse, JobInfo, ProcessInfo};
use tokio::process::Command;
use uuid::Uuid;
#[derive(Debug)]
pub struct ProcessManager {
jobs: Mutex<HashMap<String, JobInfo>>,
bg_pids: Mutex<HashMap<String, u32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessList {
pub processes: Vec<ProcessInfo>,
}
impl Default for ProcessManager {
fn default() -> Self {
Self::new()
}
}
impl ProcessManager {
#[must_use]
pub fn new() -> Self {
Self {
jobs: Mutex::new(HashMap::new()),
bg_pids: Mutex::new(HashMap::new()),
}
}
pub fn list_processes(&self) -> BoxFuture<'_, Result<ProcessList, VfsError>> {
Box::pin(async move {
let output = Command::new("ps")
.args(["-eo", "pid,ppid,comm,pcpu,rss,stat"])
.output()
.await
.map_err(VfsError::Io)?;
let text = String::from_utf8_lossy(&output.stdout);
let mut processes = Vec::new();
for line in text.lines().skip(1) {
let cols: Vec<&str> = line.split_whitespace().collect();
if cols.len() < 6 {
continue;
}
let pid = cols[0].parse::<u32>().unwrap_or(0);
let parent_pid = cols[1].parse::<u32>().ok();
let command = cols[2].to_string();
let cpu_pct = cols[3].parse::<f32>().ok();
let mem_kb = cols[4].parse::<u64>().ok();
let state = cols[5].to_string();
processes.push(ProcessInfo {
pid,
command,
cpu_pct,
mem_bytes: mem_kb.map(|k| k * 1024),
parent_pid,
state,
});
}
Ok(ProcessList { processes })
})
}
pub fn kill_process(&self, pid: u32) -> BoxFuture<'_, Result<(), VfsError>> {
Box::pin(async move {
let output = Command::new("kill")
.arg(pid.to_string())
.output()
.await
.map_err(VfsError::Io)?;
if !output.status.success() {
return Err(VfsError::NotFound(format!("process {pid}")));
}
Ok(())
})
}
pub fn spawn_background<'a>(
&'a self,
cmd: &'a str,
args: &'a [String],
) -> BoxFuture<'a, Result<String, VfsError>> {
Box::pin(async move {
let job_id = Uuid::new_v4().to_string();
let child = Command::new(cmd).args(args).spawn().map_err(VfsError::Io)?;
let pid = child.id();
let _ = self.jobs.lock().await.insert(
job_id.clone(),
JobInfo {
id: job_id.clone(),
pid,
command: format!("{cmd} {}", args.join(" ")),
status: "running".to_string(),
},
);
if let Some(pid) = pid {
let _ = self.bg_pids.lock().await.insert(job_id.clone(), pid);
}
Ok(job_id)
})
}
pub async fn list_jobs(&self) -> Vec<JobInfo> {
self.jobs.lock().await.values().cloned().collect()
}
pub fn execute<'a>(
&'a self,
cmd: &'a str,
args: &'a [String],
) -> BoxFuture<'a, Result<ExecuteResponse, VfsError>> {
Box::pin(async move {
let output = Command::new(cmd)
.args(args)
.output()
.await
.map_err(VfsError::Io)?;
Ok(ExecuteResponse {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[tokio::test]
async fn test_list_processes_returns_data() {
let backend = ProcessManager::new();
let list = backend.list_processes().await.expect("list_processes");
assert!(!list.processes.is_empty());
}
#[tokio::test]
async fn test_spawn_background_job() {
let backend = ProcessManager::new();
let job_id = backend
.spawn_background("sleep", &["60".to_string()])
.await
.expect("spawn");
let jobs = backend.list_jobs().await;
assert!(jobs.iter().any(|j| j.id == job_id));
let pid = backend.bg_pids.lock().await.get(&job_id).copied();
if let Some(pid) = pid {
let _ = backend.kill_process(pid).await;
}
}
#[tokio::test]
async fn test_execute_returns_output() {
let backend = ProcessManager::new();
let resp = backend
.execute("echo", &["hello".to_string()])
.await
.expect("execute");
assert_eq!(resp.exit_code, 0);
assert!(resp.stdout.contains("hello"));
}
}