use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunningDaemon {
pub pid: u32,
pub version: String,
pub endpoint: String,
}
#[must_use]
pub fn pidfile_path(state_dir: &Path) -> PathBuf {
state_dir.join("terminal-commanderd.pid")
}
pub fn write_pidfile(state_dir: &Path, rec: &RunningDaemon) -> std::io::Result<()> {
std::fs::create_dir_all(state_dir)?;
let path = pidfile_path(state_dir);
let tmp = path.with_extension(format!("pid.tmp-{}", std::process::id()));
let bytes = serde_json::to_vec_pretty(rec).map_err(std::io::Error::other)?;
std::fs::write(&tmp, bytes)?;
std::fs::rename(&tmp, &path)
}
pub fn remove_pidfile(state_dir: &Path) {
let _ = std::fs::remove_file(pidfile_path(state_dir));
}
#[must_use]
pub fn read_pidfile(state_dir: &Path) -> Option<RunningDaemon> {
let bytes = std::fs::read(pidfile_path(state_dir)).ok()?;
let rec: RunningDaemon = serde_json::from_slice(&bytes).ok()?;
if pid_alive(rec.pid) { Some(rec) } else { None }
}
#[must_use]
pub fn pid_alive(pid: u32) -> bool {
#[cfg(unix)]
{
std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(windows)]
{
std::process::Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"])
.output()
.map(|o| {
let stdout = String::from_utf8_lossy(&o.stdout);
csv_row_has_exact_pid(&stdout, pid)
})
.unwrap_or(false)
}
}
#[cfg(windows)]
fn csv_row_has_exact_pid(stdout: &str, pid: u32) -> bool {
let want = pid.to_string();
stdout.lines().any(|line| {
line.split(',')
.nth(1)
.map(|f| f.trim().trim_matches('"') == want)
.unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(windows)]
#[test]
fn csv_pid_match_is_exact_not_substring() {
let row = "\"terminal-commanderd.exe\",\"1234\",\"Console\",\"1\",\"12,345 K\"";
assert!(csv_row_has_exact_pid(row, 1234), "exact pid must match");
assert!(
!csv_row_has_exact_pid(row, 123),
"substring of the pid/mem column must NOT match"
);
assert!(
!csv_row_has_exact_pid(row, 12),
"digits in the mem-usage column must NOT match"
);
assert!(!csv_row_has_exact_pid("INFO: No tasks are running.", 1234));
}
#[test]
fn write_read_roundtrip() {
let dir = std::env::temp_dir().join(format!("tc-pidfile-{}", std::process::id()));
let rec = RunningDaemon {
pid: std::process::id(),
version: "0.1.14".into(),
endpoint: "/tmp/x.sock".into(),
};
write_pidfile(&dir, &rec).unwrap();
let got = read_pidfile(&dir).unwrap();
assert_eq!(got, rec);
remove_pidfile(&dir);
assert!(read_pidfile(&dir).is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn dead_pid_reads_as_absent() {
let dir = std::env::temp_dir().join(format!("tc-pidfile-dead-{}", std::process::id()));
let rec = RunningDaemon {
pid: 999_999_999,
version: "0.1.0".into(),
endpoint: "x".into(),
};
write_pidfile(&dir, &rec).unwrap();
assert!(
read_pidfile(&dir).is_none(),
"a pidfile with a dead pid must read as absent"
);
remove_pidfile(&dir);
let _ = std::fs::remove_dir_all(&dir);
}
}