use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ProcInfo {
pub pid: u32,
pub owner: bool,
pub command: String,
pub started_at: DateTime<Utc>,
pub cwd: PathBuf,
}
pub fn procs_dir(session_id: &str) -> PathBuf {
let home = dirs::home_dir().expect("home directory");
home.join(".secafs")
.join("run")
.join(session_id)
.join("procs")
}
pub fn proc_file(session_id: &str, pid: u32) -> PathBuf {
procs_dir(session_id).join(format!("{}.json", pid))
}
pub fn write_proc_file(session_id: &str, owner: bool, command: &str, cwd: &Path) -> Result<()> {
let pid = std::process::id();
let procs_dir = procs_dir(session_id);
std::fs::create_dir_all(&procs_dir)?;
let info = ProcInfo {
pid,
owner,
command: command.to_string(),
started_at: Utc::now(),
cwd: cwd.to_path_buf(),
};
let path = proc_file(session_id, pid);
let json = serde_json::to_string_pretty(&info)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn remove_proc_file(session_id: &str) {
let pid = std::process::id();
let path = proc_file(session_id, pid);
let _ = std::fs::remove_file(path);
}
fn is_process_alive(pid: u32) -> bool {
PathBuf::from(format!("/proc/{}", pid)).exists()
}
struct SessionInfo {
session_id: String,
procs: Vec<ProcInfo>,
}
pub fn active_session_ids() -> std::collections::HashSet<String> {
list_sessions().into_iter().map(|s| s.session_id).collect()
}
fn read_proc_file_if_alive(path: &Path) -> Option<ProcInfo> {
if path.extension() != Some(std::ffi::OsStr::new("json")) {
return None;
}
let content = std::fs::read_to_string(path).ok()?;
let info: ProcInfo = serde_json::from_str(&content).ok()?;
if is_process_alive(info.pid) {
Some(info)
} else {
let _ = std::fs::remove_file(path);
None
}
}
fn collect_session_procs(procs_dir: &Path) -> Vec<ProcInfo> {
let proc_entries = match std::fs::read_dir(procs_dir) {
Ok(e) => e,
Err(_) => return vec![],
};
let mut procs: Vec<ProcInfo> = proc_entries
.flatten()
.filter_map(|entry| read_proc_file_if_alive(&entry.path()))
.collect();
procs.sort_by(|a, b| {
b.owner
.cmp(&a.owner)
.then_with(|| a.started_at.cmp(&b.started_at))
});
procs
}
fn list_sessions() -> Vec<SessionInfo> {
let home = match dirs::home_dir() {
Some(h) => h,
None => return vec![],
};
let run_dir = home.join(".secafs").join("run");
let entries = match std::fs::read_dir(&run_dir) {
Ok(e) => e,
Err(_) => return vec![],
};
let mut sessions: Vec<SessionInfo> = entries
.flatten()
.filter_map(|entry| {
let session_id = entry.file_name().to_string_lossy().to_string();
let procs_dir = entry.path().join("procs");
if !procs_dir.exists() {
return None;
}
let procs = collect_session_procs(&procs_dir);
if procs.is_empty() {
return None;
}
Some(SessionInfo { session_id, procs })
})
.collect();
sessions.sort_by_key(|s| s.procs.first().map(|p| p.started_at));
sessions
}
fn format_duration(duration: chrono::Duration) -> String {
let secs = duration.num_seconds();
if secs < 60 {
format!("{}s ago", secs)
} else if secs < 3600 {
format!("{}m ago", secs / 60)
} else if secs < 86400 {
format!("{}h ago", secs / 3600)
} else {
format!("{}d ago", secs / 86400)
}
}
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
const COL_SESSION: usize = 36;
const COL_PID: usize = 8;
const COL_OWNER: usize = 5;
const COL_COMMAND: usize = 15;
const COL_STARTED: usize = 10;
pub fn list_ps<W: Write>(out: &mut W) -> Result<()> {
let sessions = list_sessions();
if sessions.is_empty() {
writeln!(out, "No active secafs run sessions.")?;
return Ok(());
}
writeln!(
out,
"{:<COL_SESSION$} {:>COL_PID$} {:^COL_OWNER$} {:<COL_COMMAND$} {:>COL_STARTED$}",
"SESSION", "PID", "OWNER", "COMMAND", "STARTED",
)?;
let now = Utc::now();
for session in &sessions {
for proc in &session.procs {
let owner_marker = if proc.owner { "*" } else { "" };
let duration = now.signed_duration_since(proc.started_at);
writeln!(
out,
"{:<COL_SESSION$} {:>COL_PID$} {:^COL_OWNER$} {:<COL_COMMAND$} {:>COL_STARTED$}",
&session.session_id,
proc.pid,
owner_marker,
truncate(&proc.command, COL_COMMAND),
format_duration(duration),
)?;
}
}
Ok(())
}