use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use tokio::process::Child;
use anyhow::{Result, Context};
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum TaskStatus {
Running,
Finished { exit_code: i32 },
Failed { error: String },
Killed,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TaskInfo {
pub id: String,
pub command: String,
pub status: TaskStatus,
pub log_path: String,
pub started_at: DateTime<Utc>,
}
pub struct BackgroundTask {
pub info: TaskInfo,
pub child: Arc<Mutex<Option<Child>>>,
}
pub struct TaskManager {
tasks: Mutex<HashMap<String, BackgroundTask>>,
}
impl TaskManager {
pub fn global() -> &'static Self {
static INSTANCE: OnceLock<TaskManager> = OnceLock::new();
INSTANCE.get_or_init(|| Self {
tasks: Mutex::new(HashMap::new()),
})
}
pub fn spawn_task(&self, command: String, session_dir: &Path) -> Result<String> {
let task_id = format!("task_{}", &uuid::Uuid::new_v4().to_string()[..8]);
let logs_dir = session_dir.join("logs");
std::fs::create_dir_all(&logs_dir)?;
let log_path = logs_dir.join(format!("{}.log", task_id));
let log_file = std::fs::File::create(&log_path)?;
let (shell, flag) = if cfg!(target_os = "windows") {
("cmd", "/C")
} else {
("sh", "-c")
};
let child = tokio::process::Command::new(shell)
.arg(flag)
.arg(&command)
.stdin(std::process::Stdio::null())
.stdout(log_file.try_clone()?)
.stderr(log_file)
.spawn()
.context("Failed to spawn background task")?;
let info = TaskInfo {
id: task_id.clone(),
command: command.clone(),
status: TaskStatus::Running,
log_path: log_path.to_string_lossy().to_string(),
started_at: Utc::now(),
};
let mut tasks = self.tasks.lock().unwrap();
tasks.insert(task_id.clone(), BackgroundTask {
info,
child: Arc::new(Mutex::new(Some(child))),
});
Ok(task_id)
}
pub fn refresh_tasks(&self) {
let mut tasks = self.tasks.lock().unwrap();
for task in tasks.values_mut() {
if let TaskStatus::Running = task.info.status {
let mut child_guard = task.child.lock().unwrap();
if let Some(child) = child_guard.as_mut() {
match child.try_wait() {
Ok(Some(status)) => {
task.info.status = TaskStatus::Finished {
exit_code: status.code().unwrap_or(0),
};
*child_guard = None;
}
Ok(None) => {
}
Err(e) => {
task.info.status = TaskStatus::Failed {
error: e.to_string(),
};
*child_guard = None;
}
}
} else {
task.info.status = TaskStatus::Finished { exit_code: 0 };
}
}
}
}
pub fn list_tasks(&self) -> Vec<TaskInfo> {
self.refresh_tasks();
let tasks = self.tasks.lock().unwrap();
let mut list: Vec<TaskInfo> = tasks.values().map(|t| t.info.clone()).collect();
list.sort_by_key(|t| t.started_at);
list
}
pub fn kill_task(&self, task_id: &str) -> Result<bool> {
self.refresh_tasks();
let mut tasks = self.tasks.lock().unwrap();
if let Some(task) = tasks.get_mut(task_id) {
if let TaskStatus::Running = task.info.status {
let mut child_guard = task.child.lock().unwrap();
if let Some(mut child) = child_guard.take() {
let _ = child.start_kill();
task.info.status = TaskStatus::Killed;
return Ok(true);
}
}
}
Ok(false)
}
}