1use std::fs;
13use std::io::Write;
14use std::path::{Path, PathBuf};
15use std::time::Duration;
16
17pub(crate) const RUNTIME_DIR_ENV: &str = "ASC_RUNTIME_DIR";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct ProcessUsage {
24 pub pid: u32,
26 pub bytes: u64,
28 pub idle: Duration,
30 pub busy: Option<bool>,
33}
34
35impl ProcessUsage {
36 pub fn new(pid: u32, bytes: u64, idle: Duration, busy: Option<bool>) -> ProcessUsage {
39 ProcessUsage { pid, bytes, idle, busy }
40 }
41}
42
43pub struct ProcessRegistry {
45 dir: PathBuf,
46 pid: u32,
47 start: u64,
48}
49
50impl ProcessRegistry {
51 pub fn default_dir() -> Option<PathBuf> {
54 if let Some(d) = std::env::var_os(RUNTIME_DIR_ENV) {
55 return Some(PathBuf::from(d));
56 }
57 if let Some(d) = std::env::var_os("XDG_RUNTIME_DIR") {
58 return Some(PathBuf::from(d).join("asc"));
59 }
60 Some(std::env::temp_dir().join(format!("asc-{}", uid()?)).join("procs"))
61 }
62
63 pub fn open(dir: impl Into<PathBuf>) -> Option<ProcessRegistry> {
67 let dir = dir.into();
68 let pid = std::process::id();
69 let start = start_time(pid)?;
70 create_private_dir(&dir)?;
71 let reg = ProcessRegistry { dir, pid, start };
72 reg.update(0, false).ok()?;
73 Some(reg)
74 }
75
76 pub fn dir(&self) -> &Path {
78 &self.dir
79 }
80
81 pub fn update(&self, bytes: u64, busy: bool) -> std::io::Result<()> {
88 let path = self.dir.join(self.pid.to_string());
89 let tmp = self.dir.join(format!(".{}.tmp", self.pid));
90 fs::File::create(&tmp)?.write_all(format!("{} {bytes} {}\n", self.start, u8::from(busy)).as_bytes())?;
91 fs::rename(&tmp, path)
92 }
93
94 pub fn others(&self) -> Vec<ProcessUsage> {
96 Self::others_in(&self.dir)
97 }
98
99 pub fn others_in(dir: &Path) -> Vec<ProcessUsage> {
102 let mut out = Vec::new();
103 let Ok(rd) = fs::read_dir(dir) else { return out };
104 let me = std::process::id();
105 for f in rd.flatten() {
106 let Some(pid) = f.file_name().to_str().and_then(|n| n.parse::<u32>().ok()) else { continue };
107 if pid == me {
108 continue;
109 }
110 let Ok(text) = fs::read_to_string(f.path()) else { continue };
111 let mut fields = text.split_whitespace().map(|v| v.parse::<u64>().ok());
112 let (Some(Some(start)), Some(Some(bytes))) = (fields.next(), fields.next()) else { continue };
113 let busy = fields.next().flatten().map(|b| b != 0);
114 if start_time(pid) == Some(start) {
115 let idle = f.metadata().and_then(|m| m.modified()).ok().and_then(|t| t.elapsed().ok()).unwrap_or_default();
116 out.push(ProcessUsage { pid, bytes, idle, busy });
117 } else {
118 let _ = fs::remove_file(f.path());
119 }
120 }
121 out
122 }
123}
124
125impl Drop for ProcessRegistry {
126 fn drop(&mut self) {
127 let _ = fs::remove_file(self.dir.join(self.pid.to_string()));
128 }
129}
130
131#[cfg(target_os = "linux")]
134fn start_time(pid: u32) -> Option<u64> {
135 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
136 stat.rsplit_once(')')?.1.split_whitespace().nth(19)?.parse().ok()
139}
140
141#[cfg(target_os = "macos")]
143fn start_time(pid: u32) -> Option<u64> {
144 let pid = libc::c_int::try_from(pid).ok()?;
145 let size = libc::c_int::try_from(std::mem::size_of::<libc::proc_bsdinfo>()).ok()?;
146 let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
147 let written = unsafe { libc::proc_pidinfo(pid, libc::PROC_PIDTBSDINFO, 0, info.as_mut_ptr().cast(), size) };
150 if written != size {
151 return None;
152 }
153 let info = unsafe { info.assume_init() };
156 Some(info.pbi_start_tvsec * 1_000_000 + info.pbi_start_tvusec)
157}
158
159#[cfg(not(any(target_os = "linux", target_os = "macos")))]
160fn start_time(_pid: u32) -> Option<u64> {
161 None
162}
163
164#[cfg(unix)]
165fn uid() -> Option<u32> {
166 Some(unsafe { libc::getuid() })
168}
169
170#[cfg(not(unix))]
171fn uid() -> Option<u32> {
172 None
173}
174
175#[cfg(unix)]
178fn create_private_dir(dir: &Path) -> Option<()> {
179 use std::os::unix::fs::{DirBuilderExt, MetadataExt};
180 let _ = fs::DirBuilder::new().recursive(true).mode(0o700).create(dir);
181 let meta = fs::symlink_metadata(dir).ok()?;
182 (meta.is_dir() && Some(meta.uid()) == uid()).then_some(())
183}
184
185#[cfg(not(unix))]
186fn create_private_dir(dir: &Path) -> Option<()> {
187 fs::create_dir_all(dir).ok()
188}
189
190#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
191mod tests {
192 use super::*;
193
194 #[cfg(target_os = "linux")]
196 #[test]
197 fn processes_see_each_other_and_stale_entries_go() {
198 let dir = tempfile::tempdir().unwrap();
199 let me = ProcessRegistry::open(dir.path()).unwrap();
200 me.update(123, true).unwrap();
201 assert!(me.others().is_empty(), "never lists itself");
202 let init_start = start_time(1).unwrap();
204 fs::write(dir.path().join("1"), format!("{init_start} 456 1\n")).unwrap();
205 fs::write(dir.path().join("2"), "1 789\n").unwrap();
206 fs::write(dir.path().join("notes"), "x").unwrap();
207 let others: Vec<_> = me.others().iter().map(|o| (o.pid, o.bytes, o.busy)).collect();
209 assert_eq!(others, vec![(1, 456, Some(true))]);
210 fs::write(dir.path().join("1"), format!("{init_start} 456\n")).unwrap();
212 assert_eq!(me.others()[0].busy, None);
213 assert_eq!(ProcessRegistry::others_in(dir.path()).len(), 1, "readable without registering");
214 assert!(me.others()[0].idle < Duration::from_secs(60), "just written");
215 assert!(!dir.path().join("2").exists(), "dead entry removed");
216 assert!(dir.path().join(std::process::id().to_string()).exists());
217 drop(me);
218 assert!(!dir.path().join(std::process::id().to_string()).exists(), "unregistered on drop");
219 }
220
221 #[test]
222 fn start_time_survives_odd_command_names() {
223 assert!(start_time(std::process::id()).is_some());
224 assert!(start_time(u32::MAX).is_none());
225 }
226}