1use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9
10use crate::config::Config;
11use crate::gpu::NvmlCollector;
12use crate::smart::SmartReader;
13#[cfg(target_os = "linux")]
14use crate::snapshot::device_matches_disk;
15use crate::snapshot::{PowerStats, Snapshot};
16use crate::system::SystemCollector;
17
18#[cfg(target_os = "linux")]
19use crate::linux::{
20 self, SysRoots, diskstats::DiskStatsMeter, net::NetLinkReader, rapl::RaplMeter,
21};
22
23pub const MIN_POLL_INTERVAL: Duration = sysinfo::MINIMUM_CPU_UPDATE_INTERVAL;
26
27#[derive(Debug, thiserror::Error)]
29pub enum SourceError {
30 #[error("I/O error: {0}")]
32 Io(#[from] std::io::Error),
33 #[error("source disconnected: {0}")]
35 Disconnected(String),
36 #[error("remote: {0}")]
38 Remote(String),
39 #[error("cannot signal process {pid}: {source}")]
41 Signal {
42 pid: u32,
44 source: std::io::Error,
46 },
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
51pub enum ProcessSignal {
52 Terminate,
54 Kill,
56}
57
58pub trait DataSource {
60 fn poll(&mut self) -> Result<Snapshot, SourceError>;
66
67 fn signal_process(&mut self, pid: u32, signal: ProcessSignal) -> Result<(), SourceError>;
70
71 fn set_interval(&mut self, _interval: Duration) {}
75
76 fn set_process_limit(&mut self, _limit: Option<usize>) {}
79
80 fn watch_processes(&mut self, _pids: Vec<u32>) {}
84}
85
86impl<T: DataSource + ?Sized> DataSource for Box<T> {
88 fn poll(&mut self) -> Result<Snapshot, SourceError> {
89 (**self).poll()
90 }
91
92 fn signal_process(&mut self, pid: u32, signal: ProcessSignal) -> Result<(), SourceError> {
93 (**self).signal_process(pid, signal)
94 }
95
96 fn set_interval(&mut self, interval: Duration) {
97 (**self).set_interval(interval);
98 }
99
100 fn set_process_limit(&mut self, limit: Option<usize>) {
101 (**self).set_process_limit(limit);
102 }
103
104 fn watch_processes(&mut self, pids: Vec<u32>) {
105 (**self).watch_processes(pids);
106 }
107}
108
109pub struct LocalSource {
111 system: SystemCollector,
112 nvml: NvmlCollector,
113 smart: SmartReader,
114 last_poll: Option<Instant>,
115 watched_pids: Vec<u32>,
117 fill: crate::fill::FillTracker,
118 #[cfg(target_os = "linux")]
119 roots: SysRoots,
120 #[cfg(target_os = "linux")]
121 rapl: RaplMeter,
122 #[cfg(target_os = "linux")]
123 diskstats: DiskStatsMeter,
124 #[cfg(target_os = "linux")]
125 net_links: NetLinkReader,
126 #[cfg(target_os = "linux")]
127 failed_units: linux::systemd::FailedUnitsReader,
128 #[cfg(target_os = "linux")]
129 containers: linux::containers::ContainerTracker,
130 #[cfg(target_os = "linux")]
131 oom: linux::oom::OomTracker,
132 #[cfg(target_os = "linux")]
134 scrub_dirs: Vec<std::path::PathBuf>,
135 #[cfg(target_os = "linux")]
137 drm_gpus: linux::drm::DrmGpuReader,
138 #[cfg(windows)]
140 lhm: crate::lhm::LhmReader,
141}
142
143impl LocalSource {
144 pub fn new() -> Self {
146 Self::with_config(&Config::default())
147 }
148
149 pub fn with_config(config: &Config) -> Self {
156 let mut source = Self {
157 system: SystemCollector::new(),
158 nvml: NvmlCollector::new(),
159 smart: SmartReader::new(&config.smart.dir),
160 last_poll: None,
161 watched_pids: Vec::new(),
162 fill: crate::fill::FillTracker::new(),
163 #[cfg(target_os = "linux")]
164 roots: SysRoots::default(),
165 #[cfg(target_os = "linux")]
166 rapl: RaplMeter::new(),
167 #[cfg(target_os = "linux")]
168 diskstats: DiskStatsMeter::new(),
169 #[cfg(target_os = "linux")]
170 net_links: NetLinkReader::new(),
171 #[cfg(target_os = "linux")]
172 failed_units: linux::systemd::FailedUnitsReader::new(),
173 #[cfg(target_os = "linux")]
174 containers: linux::containers::ContainerTracker::new(),
175 #[cfg(target_os = "linux")]
176 oom: linux::oom::OomTracker::new(),
177 #[cfg(target_os = "linux")]
178 drm_gpus: linux::drm::DrmGpuReader::new(),
179 #[cfg(windows)]
182 lhm: crate::lhm::LhmReader::new(&config.lhm.url),
183 #[cfg(target_os = "linux")]
184 scrub_dirs: vec![
185 config.btrfs.scrub_dir.clone(),
186 linux::btrfs::BTRFS_PROGS_STATUS_DIR.into(),
187 ],
188 };
189 let _ = source.collect();
191 source
192 }
193
194 pub fn gpu_unavailable_reason(&self) -> Option<&str> {
196 self.nvml.init_error()
197 }
198
199 fn collect(&mut self) -> Snapshot {
200 let now = Instant::now();
201 let elapsed = self
202 .last_poll
203 .map(|prev| now.saturating_duration_since(prev).as_secs_f64());
204 self.last_poll = Some(now);
205
206 self.system.refresh();
207 let processes = self.system.processes(elapsed);
208 let gpus = self.nvml.collect();
209 let gpus_w = sum_present(gpus.iter().map(|g| g.power_w));
210
211 let mut snapshot = Snapshot {
212 timestamp_ms: SystemTime::now()
213 .duration_since(UNIX_EPOCH)
214 .map(|d| d.as_millis() as u64)
215 .unwrap_or(0),
216 host: self.system.host(),
217 cpu: self.system.cpu(),
218 memory: self.system.memory(),
219 networks: self.system.networks(elapsed),
220 filesystems: self.system.filesystems(),
221 process_count: processes.len(),
222 processes,
223 watched_pids: self.watched_pids.clone(),
224 gpus,
225 smart: self.smart.read(),
226 power: PowerStats {
227 gpus_w,
228 ..Default::default()
229 },
230 ..Default::default()
231 };
232
233 #[cfg(target_os = "linux")]
234 self.collect_linux(&mut snapshot, now);
235 #[cfg(not(target_os = "linux"))]
236 self.collect_portable(&mut snapshot, elapsed);
237
238 self.fill.annotate(&mut snapshot.filesystems, now);
239 let power = &mut snapshot.power;
240 power.total_w = sum_present([power.cpu_or_soc_w().1, power.gpus_w]);
241 snapshot
242 }
243
244 #[cfg(not(target_os = "linux"))]
247 fn collect_portable(&mut self, snapshot: &mut Snapshot, elapsed: Option<f64>) {
248 snapshot.disks = self.system.volume_io(elapsed);
249 let (chips, cpu_temperatures) = self.system.components();
250 snapshot.sensors = chips;
251 snapshot.cpu.temperatures = cpu_temperatures;
252 #[cfg(windows)]
253 {
254 crate::windows::annotate_links(&mut snapshot.networks);
255 match self.lhm.read(Instant::now()) {
258 Some(lhm) => {
259 if !lhm.chips.is_empty() {
260 snapshot.sensors = lhm.chips;
261 }
262 if !lhm.cpu_temperatures.is_empty() {
263 snapshot.cpu.temperatures = lhm.cpu_temperatures;
264 }
265 snapshot.power.cpu_package_w = lhm.cpu_package_w;
266 if !lhm.smart.is_empty() {
267 snapshot.smart = lhm.smart;
268 }
269 }
270 None => snapshot.lhm_error = self.lhm.problem().map(str::to_owned),
271 }
272 }
273 }
274
275 #[cfg(target_os = "linux")]
277 fn collect_linux(&mut self, snapshot: &mut Snapshot, now: Instant) {
278 let chips = linux::hwmon::read_chips(&self.roots);
279
280 snapshot.cpu.temperatures = chips
281 .iter()
282 .filter(|c| linux::hwmon::is_cpu_chip(&c.chip))
283 .flat_map(|c| c.chip.temperatures.iter().cloned())
284 .collect();
285 snapshot.memory.cached_bytes = linux::meminfo::read_cached_bytes(&self.roots);
286 snapshot.memory.oom_kills =
287 self.oom
288 .sample(&self.roots, &snapshot.processes, snapshot.timestamp_ms);
289 snapshot.power.cpu_package_w = self.rapl.sample(&self.roots, now);
290 snapshot.power.igpu_w = sum_present(
293 chips
294 .iter()
295 .filter(|c| c.is_apu)
296 .flat_map(|c| c.power_w.iter().map(|(_, w)| Some(*w))),
297 );
298 snapshot.power.battery = linux::battery::read_battery(&self.roots);
299
300 let first = snapshot.gpus.len() as u32;
302 snapshot.gpus.extend(self.drm_gpus.read(&self.roots, first));
303 snapshot.power.gpus_w = sum_present(snapshot.gpus.iter().map(|g| g.power_w));
304
305 let mut disks = self.diskstats.sample(&self.roots, now);
306 for disk in &mut disks {
307 disk.temperature_c = nvme_temperature(&chips, &disk.name);
308 }
309 snapshot.disks = disks;
310
311 let mounts = linux::mounts::read_mounts(&self.roots);
312 snapshot.raid.btrfs = linux::btrfs::read_filesystems(&self.roots, &mounts);
313 for fs in &mut snapshot.raid.btrfs {
314 fs.scrub = linux::btrfs::read_scrub(&self.scrub_dirs, &fs.fsid);
315 }
316 self.net_links
317 .annotate(&self.roots, &mut snapshot.networks, now);
318 snapshot.raid.md = linux::mdstat::read_arrays(&self.roots);
319 snapshot.raid.zfs = linux::zfs::read_pools(&self.roots);
320 snapshot.failed_units = self.failed_units.read(now);
321 let release = std::fs::read_to_string(self.roots.proc.join("sys/kernel/osrelease"))
322 .unwrap_or_default();
323 snapshot.host.reboot_required = linux::reboot::reboot_required(
324 std::path::Path::new("/run"),
325 std::path::Path::new("/usr/lib/modules"),
326 &release,
327 );
328 for process in &mut snapshot.processes {
330 process.threads = linux::procstat::thread_count(&self.roots, process.pid);
331 }
332 self.containers
333 .annotate(&self.roots, &mut snapshot.processes, now);
334
335 snapshot.sensors = chips.into_iter().map(|c| c.chip).collect();
336 }
337}
338
339impl Default for LocalSource {
340 fn default() -> Self {
341 Self::new()
342 }
343}
344
345impl DataSource for LocalSource {
346 fn poll(&mut self) -> Result<Snapshot, SourceError> {
347 Ok(self.collect())
348 }
349
350 fn signal_process(&mut self, pid: u32, signal: ProcessSignal) -> Result<(), SourceError> {
351 send_signal(pid, signal).map_err(|source| SourceError::Signal { pid, source })
352 }
353
354 fn set_process_limit(&mut self, limit: Option<usize>) {
357 self.nvml.set_process_utilization(limit.is_none());
358 }
359
360 fn watch_processes(&mut self, pids: Vec<u32>) {
361 self.watched_pids = pids;
362 }
363}
364
365#[cfg(unix)]
367fn send_signal(pid: u32, signal: ProcessSignal) -> std::io::Result<()> {
368 let pid = libc::pid_t::try_from(pid)
371 .ok()
372 .filter(|p| *p > 0)
373 .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::InvalidInput))?;
374 let sig = match signal {
375 ProcessSignal::Terminate => libc::SIGTERM,
376 ProcessSignal::Kill => libc::SIGKILL,
377 };
378 if unsafe { libc::kill(pid, sig) } == 0 {
380 Ok(())
381 } else {
382 Err(std::io::Error::last_os_error())
383 }
384}
385
386#[cfg(windows)]
387use crate::windows::send_signal;
388
389#[cfg(not(any(unix, windows)))]
391fn send_signal(_pid: u32, _signal: ProcessSignal) -> std::io::Result<()> {
392 Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
393}
394
395fn sum_present(values: impl IntoIterator<Item = Option<f32>>) -> Option<f32> {
397 values
398 .into_iter()
399 .flatten()
400 .fold(None, |acc, v| Some(acc.unwrap_or(0.0) + v))
401}
402
403#[cfg(target_os = "linux")]
406fn nvme_temperature(chips: &[linux::hwmon::HwmonChip], disk: &str) -> Option<f32> {
407 chips.iter().find_map(|c| {
408 let controller = c.chip.key.strip_prefix("nvme:")?;
409 device_matches_disk(controller, disk).then_some(())?;
410 c.chip.temperatures.first().map(|t| t.celsius)
412 })
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[cfg(unix)]
420 #[test]
421 fn signal_rejects_group_pids_and_reports_missing_process() {
422 assert_eq!(
423 send_signal(0, ProcessSignal::Terminate).unwrap_err().kind(),
424 std::io::ErrorKind::InvalidInput
425 );
426 assert_eq!(
427 send_signal(u32::MAX, ProcessSignal::Kill)
428 .unwrap_err()
429 .kind(),
430 std::io::ErrorKind::InvalidInput
431 );
432 let mut child = std::process::Command::new("true").spawn().unwrap();
434 let pid = child.id();
435 child.wait().unwrap();
436 let err = send_signal(pid, ProcessSignal::Terminate).unwrap_err();
437 assert_eq!(err.raw_os_error(), Some(libc::ESRCH));
438 }
439
440 #[cfg(unix)]
441 #[test]
442 fn signal_terminates_a_child_process() {
443 let mut child = std::process::Command::new("sleep")
444 .arg("30")
445 .spawn()
446 .unwrap();
447 send_signal(child.id(), ProcessSignal::Kill).unwrap();
448 let status = child.wait().unwrap();
449 assert!(!status.success());
450 }
451
452 #[test]
453 fn sum_present_ignores_missing_values() {
454 assert_eq!(sum_present([None, None]), None);
455 assert_eq!(sum_present([Some(1.5), None, Some(2.0)]), Some(3.5));
456 }
457
458 #[cfg(target_os = "linux")]
459 #[test]
460 fn nvme_temperature_matches_controller_exactly() {
461 use crate::linux::hwmon::HwmonChip;
462 use crate::snapshot::{SensorChip, Temperature};
463
464 let chip = |key: &str, celsius: f32| HwmonChip {
465 chip: SensorChip {
466 key: key.to_owned(),
467 name: "nvme".to_owned(),
468 temperatures: vec![Temperature {
469 label: "Composite".to_owned(),
470 celsius,
471 ..Default::default()
472 }],
473 fans: Vec::new(),
474 },
475 power_w: Vec::new(),
476 is_apu: false,
477 };
478 let chips = vec![chip("nvme:nvme1", 40.0), chip("nvme:nvme10", 50.0)];
479 assert_eq!(nvme_temperature(&chips, "nvme10n1"), Some(50.0));
480 assert_eq!(nvme_temperature(&chips, "nvme1n1"), Some(40.0));
481 assert_eq!(nvme_temperature(&chips, "sda"), None);
482 }
483}