initial side-by-side impl for linux temmp and process as a poc
This commit is contained in:
parent
5fe6b1182f
commit
1ca4a63044
|
@ -4,7 +4,14 @@
|
||||||
//! the battery crate.
|
//! the battery crate.
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "ios"))] {
|
if #[cfg(any(
|
||||||
|
target_os = "windows",
|
||||||
|
target_os = "macos",
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "dragonfly",
|
||||||
|
target_os = "ios",
|
||||||
|
))] {
|
||||||
pub mod battery;
|
pub mod battery;
|
||||||
pub use self::battery::*;
|
pub use self::battery::*;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
# Data Collection
|
# Data Collection
|
||||||
|
|
||||||
|
**Note:** This information is really only useful to _developers_ of bottom,
|
||||||
|
and can be ignored by users.
|
||||||
|
|
||||||
Data collection in bottom has two main components: **sources** and **collectors**.
|
Data collection in bottom has two main components: **sources** and **collectors**.
|
||||||
|
|
||||||
**Sources** are either libraries or system APIs that actually extract the data.
|
**Sources** are either libraries or system APIs that actually extract the data.
|
||||||
|
@ -8,7 +11,7 @@ or `libc` bindings, or Linux-specific code.
|
||||||
|
|
||||||
**Collectors** are _platform-specific_ (typically OS-specific), and can pull from
|
**Collectors** are _platform-specific_ (typically OS-specific), and can pull from
|
||||||
different sources to get all the data needed, with some glue code in between. As
|
different sources to get all the data needed, with some glue code in between. As
|
||||||
such, sources should be written to be per-"job", and be divisible such that
|
such, sources should be written to be per-"job", and be divided such that
|
||||||
collectors can import specific code as needed.
|
collectors can import specific code as needed.
|
||||||
|
|
||||||
We can kinda visualize this with a quick-and-dirty diagram (note this is not accurate or up-to-date):
|
We can kinda visualize this with a quick-and-dirty diagram (note this is not accurate or up-to-date):
|
||||||
|
@ -42,3 +45,11 @@ flowchart TB
|
||||||
freebsd -..-> FreeBSD
|
freebsd -..-> FreeBSD
|
||||||
sysinfo -..-> FreeBSD
|
sysinfo -..-> FreeBSD
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
As mentioned above, sources should be written in a way where collectors can easily pick the necessary code required.
|
||||||
|
|
||||||
|
## Collectors
|
||||||
|
|
||||||
|
Each platform should implement the `DataCollector` trait in `collectors/common.rs`. The trait has default implementations where essentially no work is done, which is used as fallback behaviour.
|
|
@ -0,0 +1,27 @@
|
||||||
|
//! Common code amongst all data collectors.
|
||||||
|
|
||||||
|
use crate::new_data_collection::{
|
||||||
|
error::CollectionResult,
|
||||||
|
sources::common::{processes::ProcessHarvest, temperature::TemperatureData},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The trait representing what a per-platform data collector should implement.
|
||||||
|
pub(crate) trait DataCollector {
|
||||||
|
/// Refresh inner data sources to prepare them for gathering data.
|
||||||
|
///
|
||||||
|
/// Note that depending on the implementation, this may
|
||||||
|
/// not actually need to do anything.
|
||||||
|
fn refresh_data(&mut self) -> CollectionResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return temperature data.
|
||||||
|
fn get_temperature_data(&mut self) -> CollectionResult<Vec<TemperatureData>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return process data.
|
||||||
|
fn get_process_data(&mut self) -> CollectionResult<Vec<ProcessHarvest>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
use super::common::DataCollector;
|
||||||
|
|
||||||
|
/// A fallback [`DataCollector`] for unsupported systems
|
||||||
|
/// that does nothing.
|
||||||
|
pub struct FallbackDataCollector {}
|
||||||
|
|
||||||
|
impl DataCollector for FallbackDataCollector {}
|
|
@ -0,0 +1,36 @@
|
||||||
|
//! The data collector for FreeBSD.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::filter::Filter,
|
||||||
|
new_data_collection::{
|
||||||
|
error::CollectionResult,
|
||||||
|
sources::{
|
||||||
|
common::temperature::{TemperatureData, TemperatureType},
|
||||||
|
sysinfo::temperature::get_temperature_data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::common::DataCollector;
|
||||||
|
|
||||||
|
/// The [`DataCollector`] for FreeBSD.
|
||||||
|
pub struct FreeBsdDataCollector {
|
||||||
|
temp_type: TemperatureType,
|
||||||
|
temp_filters: Option<Filter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataCollector for FreeBsdDataCollector {
|
||||||
|
fn refresh_data(&mut self) -> CollectionResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_temperature_data(&self) -> CollectionResult<Option<Vec<TemperatureData>>> {
|
||||||
|
let mut results = get_temperature_data(&self.temp_type, &self.temp_filters);
|
||||||
|
|
||||||
|
for entry in sysctl_temp_iter(&self.temp_type, &self.temp_filters) {
|
||||||
|
results.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(results))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
//! The data collector for Linux.
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use starship_battery::{Battery, Manager};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::filter::Filter,
|
||||||
|
new_data_collection::{
|
||||||
|
error::CollectionResult,
|
||||||
|
sources::{
|
||||||
|
common::{
|
||||||
|
processes::ProcessHarvest,
|
||||||
|
temperature::{TemperatureData, TemperatureType},
|
||||||
|
},
|
||||||
|
linux::{
|
||||||
|
processes::{linux_process_data, ProcessCollector},
|
||||||
|
temperature::get_temperature_data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::common::DataCollector;
|
||||||
|
|
||||||
|
/// The [`DataCollector`] for Linux.
|
||||||
|
pub struct LinuxDataCollector {
|
||||||
|
current_collection_time: Instant,
|
||||||
|
last_collection_time: Instant,
|
||||||
|
|
||||||
|
temp_type: TemperatureType,
|
||||||
|
temp_filters: Option<Filter>,
|
||||||
|
|
||||||
|
proc_collector: ProcessCollector,
|
||||||
|
|
||||||
|
system: sysinfo::System,
|
||||||
|
network: sysinfo::Networks,
|
||||||
|
|
||||||
|
#[cfg(feature = "battery")]
|
||||||
|
battery_manager: Option<Manager>,
|
||||||
|
#[cfg(feature = "battery")]
|
||||||
|
battery_list: Option<Vec<Battery>>,
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
gpus_total_mem: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataCollector for LinuxDataCollector {
|
||||||
|
fn refresh_data(&mut self) -> CollectionResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_temperature_data(&mut self) -> CollectionResult<Vec<TemperatureData>> {
|
||||||
|
Ok(get_temperature_data(&self.temp_type, &self.temp_filters))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_process_data(&mut self) -> CollectionResult<Vec<ProcessHarvest>> {
|
||||||
|
let time_diff = self
|
||||||
|
.current_collection_time
|
||||||
|
.duration_since(self.last_collection_time)
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
linux_process_data(
|
||||||
|
&self.system,
|
||||||
|
time_diff,
|
||||||
|
&mut self.proc_collector,
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
self.gpus_total_mem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
//! The data collector for macOS.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::filter::Filter,
|
||||||
|
new_data_collection::{
|
||||||
|
error::CollectionResult,
|
||||||
|
sources::{
|
||||||
|
common::temperature::{TemperatureData, TemperatureType},
|
||||||
|
sysinfo::temperature::get_temperature_data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::common::DataCollector;
|
||||||
|
|
||||||
|
/// The [`DataCollector`] for macOS.
|
||||||
|
pub struct MacOsDataCollector {
|
||||||
|
temp_type: TemperatureType,
|
||||||
|
temp_filters: Option<Filter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataCollector for MacOsDataCollector {
|
||||||
|
fn refresh_data(&mut self) -> CollectionResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_temperature_data(&self) -> CollectionResult<Option<Vec<TemperatureData>>> {
|
||||||
|
Ok(Some(get_temperature_data(
|
||||||
|
&self.temp_type,
|
||||||
|
&self.temp_filters,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
//! The data collector for Windows.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::filter::Filter,
|
||||||
|
new_data_collection::{
|
||||||
|
error::CollectionResult,
|
||||||
|
sources::{
|
||||||
|
common::temperature::{TemperatureData, TemperatureType},
|
||||||
|
sysinfo::temperature::get_temperature_data,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::common::DataCollector;
|
||||||
|
|
||||||
|
/// The [`DataCollector`] for Windows.
|
||||||
|
pub struct WindowsDataCollector {
|
||||||
|
temp_type: TemperatureType,
|
||||||
|
temp_filters: Option<Filter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataCollector for WindowsDataCollector {
|
||||||
|
fn refresh_data(&mut self) -> CollectionResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_temperature_data(&self) -> CollectionResult<Option<Vec<TemperatureData>>> {
|
||||||
|
Ok(Some(get_temperature_data(
|
||||||
|
&self.temp_type,
|
||||||
|
&self.temp_filters,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
/// An error to do with data collection.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CollectionError {
|
||||||
|
/// A general error to propagate back up. A wrapper around [`anyhow::Error`].
|
||||||
|
General(anyhow::Error),
|
||||||
|
|
||||||
|
/// The collection is unsupported.
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for CollectionError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
CollectionError::General(err) => err.fmt(f),
|
||||||
|
CollectionError::Unsupported => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"bottom does not support this type of data collection for this platform."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CollectionError {}
|
||||||
|
|
||||||
|
/// A [`Result`] with the error type being a [`DataCollectionError`].
|
||||||
|
pub(crate) type CollectionResult<T> = Result<T, CollectionError>;
|
||||||
|
|
||||||
|
impl From<std::io::Error> for CollectionError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
Self::General(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for CollectionError {
|
||||||
|
fn from(msg: &'static str) -> Self {
|
||||||
|
Self::General(anyhow!(msg))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
//! Module that just re-exports the right data collector for a given platform.
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
mod collectors {
|
||||||
|
pub mod common;
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(target_os = "linux")] {
|
||||||
|
pub mod linux;
|
||||||
|
pub use linux::LinuxDataCollector as DataCollectorImpl;
|
||||||
|
} else if #[cfg(target_os = "macos")] {
|
||||||
|
pub mod macos;
|
||||||
|
pub use macos::MacOsDataCollector as DataCollectorImpl;
|
||||||
|
} else if #[cfg(target_os = "windows")] {
|
||||||
|
pub mod windows;
|
||||||
|
pub use windows::WindowsDataCollector as DataCollectorImpl;
|
||||||
|
} else if #[cfg(target_os = "freebsd")] {
|
||||||
|
pub mod freebsd;
|
||||||
|
pub use freebsd::FreeBsdDataCollector as DataCollectorImpl;
|
||||||
|
} else {
|
||||||
|
pub mod fallback;
|
||||||
|
pub use fallback::FallbackDataCollector as DataCollectorImpl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod sources;
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod processes;
|
||||||
|
pub mod temperature;
|
|
@ -0,0 +1,87 @@
|
||||||
|
use std::{borrow::Cow, time::Duration};
|
||||||
|
|
||||||
|
use crate::new_data_collection::sources::Pid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ProcessHarvest {
|
||||||
|
/// The pid of the process.
|
||||||
|
pub pid: Pid,
|
||||||
|
|
||||||
|
/// The parent PID of the process. A `parent_pid` of 0 is usually the root.
|
||||||
|
pub parent_pid: Option<Pid>,
|
||||||
|
|
||||||
|
/// CPU usage as a percentage.
|
||||||
|
pub cpu_usage_percent: f32,
|
||||||
|
|
||||||
|
/// Memory usage as a percentage.
|
||||||
|
pub mem_usage_percent: f32,
|
||||||
|
|
||||||
|
/// Memory usage as bytes.
|
||||||
|
pub mem_usage_bytes: u64,
|
||||||
|
|
||||||
|
/// The name of the process.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// The exact command for the process.
|
||||||
|
pub command: String,
|
||||||
|
|
||||||
|
/// Bytes read per second.
|
||||||
|
pub read_bytes_per_sec: u64,
|
||||||
|
|
||||||
|
/// Bytes written per second.
|
||||||
|
pub write_bytes_per_sec: u64,
|
||||||
|
|
||||||
|
/// The total number of bytes read by the process.
|
||||||
|
pub total_read_bytes: u64,
|
||||||
|
|
||||||
|
/// The total number of bytes written by the process.
|
||||||
|
pub total_write_bytes: u64,
|
||||||
|
|
||||||
|
/// The current state of the process (e.g. zombie, asleep).
|
||||||
|
pub process_state: (String, char),
|
||||||
|
|
||||||
|
/// Cumulative process uptime.
|
||||||
|
pub time: Duration,
|
||||||
|
|
||||||
|
/// This is the *effective* user ID of the process. This is only used on
|
||||||
|
/// Unix platforms.
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
pub uid: Option<libc::uid_t>,
|
||||||
|
|
||||||
|
/// This is the process' user.
|
||||||
|
pub user: Cow<'static, str>,
|
||||||
|
|
||||||
|
/// GPU memory usage as bytes.
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
pub gpu_mem: u64,
|
||||||
|
|
||||||
|
/// GPU memory usage as percentage.
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
pub gpu_mem_percent: f32,
|
||||||
|
|
||||||
|
/// GPU utilization as a percentage.
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
pub gpu_util: u32,
|
||||||
|
// TODO: Additional fields
|
||||||
|
// pub rss_kb: u64,
|
||||||
|
// pub virt_kb: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessHarvest {
|
||||||
|
pub(crate) fn add(&mut self, rhs: &ProcessHarvest) {
|
||||||
|
self.cpu_usage_percent += rhs.cpu_usage_percent;
|
||||||
|
self.mem_usage_bytes += rhs.mem_usage_bytes;
|
||||||
|
self.mem_usage_percent += rhs.mem_usage_percent;
|
||||||
|
self.read_bytes_per_sec += rhs.read_bytes_per_sec;
|
||||||
|
self.write_bytes_per_sec += rhs.write_bytes_per_sec;
|
||||||
|
self.total_read_bytes += rhs.total_read_bytes;
|
||||||
|
self.total_write_bytes += rhs.total_write_bytes;
|
||||||
|
self.time = self.time.max(rhs.time);
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
{
|
||||||
|
self.gpu_mem += rhs.gpu_mem;
|
||||||
|
self.gpu_util += rhs.gpu_util;
|
||||||
|
self.gpu_mem_percent += rhs.gpu_mem_percent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct TemperatureData {
|
||||||
|
pub name: String,
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum TemperatureType {
|
||||||
|
#[default]
|
||||||
|
Celsius,
|
||||||
|
Kelvin,
|
||||||
|
Fahrenheit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TemperatureType {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"fahrenheit" | "f" => Ok(TemperatureType::Fahrenheit),
|
||||||
|
"kelvin" | "k" => Ok(TemperatureType::Kelvin),
|
||||||
|
"celsius" | "c" => Ok(TemperatureType::Celsius),
|
||||||
|
_ => Err(format!(
|
||||||
|
"'{s}' is an invalid temperature type, use one of: [kelvin, k, celsius, c, fahrenheit, f]."
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemperatureType {
|
||||||
|
/// Given a temperature in Celsius, covert it if necessary for a different
|
||||||
|
/// unit.
|
||||||
|
pub fn convert_temp_unit(&self, temp_celsius: f32) -> f32 {
|
||||||
|
fn convert_celsius_to_kelvin(celsius: f32) -> f32 {
|
||||||
|
celsius + 273.15
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_celsius_to_fahrenheit(celsius: f32) -> f32 {
|
||||||
|
(celsius * (9.0 / 5.0)) + 32.0
|
||||||
|
}
|
||||||
|
|
||||||
|
match self {
|
||||||
|
TemperatureType::Celsius => temp_celsius,
|
||||||
|
TemperatureType::Kelvin => convert_celsius_to_kelvin(temp_celsius),
|
||||||
|
TemperatureType::Fahrenheit => convert_celsius_to_fahrenheit(temp_celsius),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::new_data_collection::sources::common::temperature::TemperatureType;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn temp_conversions() {
|
||||||
|
const TEMP: f32 = 100.0;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
TemperatureType::Celsius.convert_temp_unit(TEMP),
|
||||||
|
TEMP,
|
||||||
|
"celsius to celsius is the same"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(TemperatureType::Kelvin.convert_temp_unit(TEMP), 373.15);
|
||||||
|
|
||||||
|
assert_eq!(TemperatureType::Fahrenheit.convert_temp_unit(TEMP), 212.0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
mod temperature;
|
|
@ -0,0 +1,35 @@
|
||||||
|
//! FreeBSD-specific temperature extraction code.
|
||||||
|
|
||||||
|
// For RockPro64 boards on FreeBSD, they apparently use "hw.temperature" for
|
||||||
|
// sensors.
|
||||||
|
use sysctl::Sysctl;
|
||||||
|
|
||||||
|
/// Return an iterator of temperature data pulled from sysctl.
|
||||||
|
pub(crate) fn sysctl_temp_iter(
|
||||||
|
temp_type: &TemperatureType, filter: &Option<Filter>,
|
||||||
|
) -> impl Iterator<Item = TemperatureData> {
|
||||||
|
const KEY: &str = "hw.temperature";
|
||||||
|
|
||||||
|
if let Ok(root) = sysctl::Ctl::new(KEY) {
|
||||||
|
sysctl::CtlIter::below(root).flatten().filter_map(|ctl| {
|
||||||
|
if let (Ok(name), Ok(temp)) = (ctl.name(), ctl.value()) {
|
||||||
|
if let Some(temp) = temp.as_temperature() {
|
||||||
|
if Filter::optional_should_keep(filter, &name) {
|
||||||
|
return Some(TemperatureData {
|
||||||
|
name,
|
||||||
|
temperature: Some(match temp_type {
|
||||||
|
TemperatureType::Celsius => temp.celsius(),
|
||||||
|
TemperatureType::Kelvin => temp.kelvin(),
|
||||||
|
TemperatureType::Fahrenheit => temp.fahrenheit(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
std::iter::empty()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod processes;
|
||||||
|
pub mod temperature;
|
|
@ -0,0 +1,450 @@
|
||||||
|
//! Process data collection for Linux.
|
||||||
|
|
||||||
|
mod process;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use hashbrown::{HashMap, HashSet};
|
||||||
|
use process::*;
|
||||||
|
use sysinfo::ProcessStatus;
|
||||||
|
|
||||||
|
use crate::new_data_collection::{
|
||||||
|
error::CollectionResult,
|
||||||
|
sources::{common::processes::ProcessHarvest, unix::processes::user_table::UserTable, Pid},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Maximum character length of a `/proc/<PID>/stat`` process name.
|
||||||
|
/// If it's equal or greater, then we instead refer to the command for the name.
|
||||||
|
const MAX_STAT_NAME_LEN: usize = 15;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct PrevProcDetails {
|
||||||
|
total_read_bytes: u64,
|
||||||
|
total_write_bytes: u64,
|
||||||
|
cpu_time: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given `/proc/stat` file contents, determine the idle and non-idle values of
|
||||||
|
/// the CPU used to calculate CPU usage.
|
||||||
|
fn fetch_cpu_usage(line: &str) -> (f64, f64) {
|
||||||
|
/// Converts a `Option<&str>` value to an f64. If it fails to parse or is
|
||||||
|
/// `None`, it will return `0_f64`.
|
||||||
|
fn str_to_f64(val: Option<&str>) -> f64 {
|
||||||
|
val.and_then(|v| v.parse::<f64>().ok()).unwrap_or(0_f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut val = line.split_whitespace();
|
||||||
|
let user = str_to_f64(val.next());
|
||||||
|
let nice: f64 = str_to_f64(val.next());
|
||||||
|
let system: f64 = str_to_f64(val.next());
|
||||||
|
let idle: f64 = str_to_f64(val.next());
|
||||||
|
let iowait: f64 = str_to_f64(val.next());
|
||||||
|
let irq: f64 = str_to_f64(val.next());
|
||||||
|
let softirq: f64 = str_to_f64(val.next());
|
||||||
|
let steal: f64 = str_to_f64(val.next());
|
||||||
|
|
||||||
|
// Note we do not get guest/guest_nice, as they are calculated as part of
|
||||||
|
// user/nice respectively See https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c
|
||||||
|
let idle = idle + iowait;
|
||||||
|
let non_idle = user + nice + system + irq + softirq + steal;
|
||||||
|
|
||||||
|
(idle, non_idle)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CpuUsage {
|
||||||
|
/// Difference between the total delta and the idle delta.
|
||||||
|
cpu_usage: f64,
|
||||||
|
|
||||||
|
/// Overall CPU usage as a fraction.
|
||||||
|
cpu_fraction: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cpu_usage_calculation(
|
||||||
|
prev_idle: &mut f64, prev_non_idle: &mut f64,
|
||||||
|
) -> CollectionResult<CpuUsage> {
|
||||||
|
let (idle, non_idle) = {
|
||||||
|
// From SO answer: https://stackoverflow.com/a/23376195
|
||||||
|
let first_line = {
|
||||||
|
// We just need a single line from this file. Read it and return it.
|
||||||
|
let mut reader = BufReader::new(File::open("/proc/stat")?);
|
||||||
|
let mut buffer = String::new();
|
||||||
|
reader.read_line(&mut buffer)?;
|
||||||
|
|
||||||
|
buffer
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch_cpu_usage(&first_line)
|
||||||
|
};
|
||||||
|
|
||||||
|
let total = idle + non_idle;
|
||||||
|
let prev_total = *prev_idle + *prev_non_idle;
|
||||||
|
|
||||||
|
let total_delta = total - prev_total;
|
||||||
|
let idle_delta = idle - *prev_idle;
|
||||||
|
|
||||||
|
*prev_idle = idle;
|
||||||
|
*prev_non_idle = non_idle;
|
||||||
|
|
||||||
|
// TODO: Should these return errors instead?
|
||||||
|
let cpu_usage = if total_delta - idle_delta != 0.0 {
|
||||||
|
total_delta - idle_delta
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let cpu_fraction = if total_delta != 0.0 {
|
||||||
|
cpu_usage / total_delta
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CpuUsage {
|
||||||
|
cpu_usage,
|
||||||
|
cpu_fraction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the usage and a new set of process times.
|
||||||
|
///
|
||||||
|
/// NB: cpu_fraction should be represented WITHOUT the x100 factor!
|
||||||
|
fn get_linux_cpu_usage(
|
||||||
|
stat: &Stat, cpu_usage: f64, cpu_fraction: f64, prev_proc_times: u64,
|
||||||
|
use_current_cpu_total: bool,
|
||||||
|
) -> (f32, u64) {
|
||||||
|
// Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556
|
||||||
|
let new_proc_times = stat.utime + stat.stime;
|
||||||
|
let diff = (new_proc_times - prev_proc_times) as f64; // No try_from for u64 -> f64... oh well.
|
||||||
|
|
||||||
|
if cpu_usage == 0.0 {
|
||||||
|
(0.0, new_proc_times)
|
||||||
|
} else if use_current_cpu_total {
|
||||||
|
(((diff / cpu_usage) * 100.0) as f32, new_proc_times)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
((diff / cpu_usage) * 100.0 * cpu_fraction) as f32,
|
||||||
|
new_proc_times,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_proc(
|
||||||
|
prev_proc: &PrevProcDetails, process: Process, args: ReadProcArgs, user_table: &mut UserTable,
|
||||||
|
) -> CollectionResult<(ProcessHarvest, u64)> {
|
||||||
|
let Process {
|
||||||
|
pid: _,
|
||||||
|
uid,
|
||||||
|
stat,
|
||||||
|
io,
|
||||||
|
cmdline,
|
||||||
|
} = process;
|
||||||
|
|
||||||
|
let ReadProcArgs {
|
||||||
|
use_current_cpu_total,
|
||||||
|
cpu_usage,
|
||||||
|
cpu_fraction,
|
||||||
|
total_memory,
|
||||||
|
time_difference_in_secs,
|
||||||
|
uptime,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
let (command, name) = {
|
||||||
|
let truncated_name = stat.comm.as_str();
|
||||||
|
if let Ok(cmdline) = cmdline {
|
||||||
|
if cmdline.is_empty() {
|
||||||
|
(format!("[{truncated_name}]"), truncated_name.to_string())
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
cmdline.join(" "),
|
||||||
|
if truncated_name.len() >= MAX_STAT_NAME_LEN {
|
||||||
|
if let Some(first_part) = cmdline.first() {
|
||||||
|
// We're only interested in the executable part... not the file path.
|
||||||
|
// That's for command.
|
||||||
|
first_part
|
||||||
|
.rsplit_once('/')
|
||||||
|
.map(|(_prefix, suffix)| suffix)
|
||||||
|
.unwrap_or(truncated_name)
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
truncated_name.to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
truncated_name.to_string()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(truncated_name.to_string(), truncated_name.to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let process_state_char = stat.state;
|
||||||
|
let process_state = (
|
||||||
|
ProcessStatus::from(process_state_char).to_string(),
|
||||||
|
process_state_char,
|
||||||
|
);
|
||||||
|
let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage(
|
||||||
|
&stat,
|
||||||
|
cpu_usage,
|
||||||
|
cpu_fraction,
|
||||||
|
prev_proc.cpu_time,
|
||||||
|
use_current_cpu_total,
|
||||||
|
);
|
||||||
|
let parent_pid = Some(stat.ppid);
|
||||||
|
let mem_usage_bytes = stat.rss_bytes();
|
||||||
|
let mem_usage_percent = (mem_usage_bytes as f64 / total_memory as f64 * 100.0) as f32;
|
||||||
|
|
||||||
|
// This can fail if permission is denied!
|
||||||
|
let (total_read_bytes, total_write_bytes, read_bytes_per_sec, write_bytes_per_sec) =
|
||||||
|
if let Ok(io) = io {
|
||||||
|
let total_read_bytes = io.read_bytes;
|
||||||
|
let total_write_bytes = io.write_bytes;
|
||||||
|
let prev_total_read_bytes = prev_proc.total_read_bytes;
|
||||||
|
let prev_total_write_bytes = prev_proc.total_write_bytes;
|
||||||
|
|
||||||
|
let read_bytes_per_sec = total_read_bytes
|
||||||
|
.saturating_sub(prev_total_read_bytes)
|
||||||
|
.checked_div(time_difference_in_secs)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let write_bytes_per_sec = total_write_bytes
|
||||||
|
.saturating_sub(prev_total_write_bytes)
|
||||||
|
.checked_div(time_difference_in_secs)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
(
|
||||||
|
total_read_bytes,
|
||||||
|
total_write_bytes,
|
||||||
|
read_bytes_per_sec,
|
||||||
|
write_bytes_per_sec,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(0, 0, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = uid
|
||||||
|
.and_then(|uid| {
|
||||||
|
user_table
|
||||||
|
.get_uid_to_username_mapping(uid)
|
||||||
|
.map(Into::into)
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "N/A".into());
|
||||||
|
|
||||||
|
let time = if let Ok(ticks_per_sec) = u32::try_from(rustix::param::clock_ticks_per_second()) {
|
||||||
|
if ticks_per_sec == 0 {
|
||||||
|
Duration::ZERO
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(uptime.saturating_sub(stat.start_time / ticks_per_sec as u64))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
ProcessHarvest {
|
||||||
|
pid: process.pid,
|
||||||
|
parent_pid,
|
||||||
|
cpu_usage_percent,
|
||||||
|
mem_usage_percent,
|
||||||
|
mem_usage_bytes,
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
read_bytes_per_sec,
|
||||||
|
write_bytes_per_sec,
|
||||||
|
total_read_bytes,
|
||||||
|
total_write_bytes,
|
||||||
|
process_state,
|
||||||
|
uid,
|
||||||
|
user,
|
||||||
|
time,
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
gpu_mem: 0,
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
gpu_mem_percent: 0.0,
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
gpu_util: 0,
|
||||||
|
},
|
||||||
|
new_process_times,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PrevProc {
|
||||||
|
pub prev_idle: f64,
|
||||||
|
pub prev_non_idle: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct ProcHarvestOptions {
|
||||||
|
pub use_current_cpu_total: bool,
|
||||||
|
pub unnormalized_cpu: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_str_numeric(s: &str) -> bool {
|
||||||
|
s.chars().all(|c| c.is_ascii_digit())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// General args to keep around for reading proc data.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub(crate) struct ReadProcArgs {
|
||||||
|
pub(crate) use_current_cpu_total: bool,
|
||||||
|
pub(crate) cpu_usage: f64,
|
||||||
|
pub(crate) cpu_fraction: f64,
|
||||||
|
pub(crate) total_memory: u64,
|
||||||
|
pub(crate) time_difference_in_secs: u64,
|
||||||
|
pub(crate) uptime: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProcessCollector {
|
||||||
|
pub options: ProcHarvestOptions,
|
||||||
|
pub prev_proc: PrevProc,
|
||||||
|
pub pid_mapping: HashMap<Pid, PrevProcDetails>,
|
||||||
|
pub user_table: UserTable,
|
||||||
|
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
pub gpu_pids: Option<Vec<HashMap<u32, (u64, u32)>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn linux_process_data(
|
||||||
|
system: &sysinfo::System, time_difference_in_secs: u64, collector: &mut ProcessCollector,
|
||||||
|
#[cfg(feature = "gpu")] gpus_total_mem: Option<u64>,
|
||||||
|
) -> CollectionResult<Vec<ProcessHarvest>> {
|
||||||
|
let total_memory = system.total_memory();
|
||||||
|
|
||||||
|
let ProcHarvestOptions {
|
||||||
|
use_current_cpu_total,
|
||||||
|
unnormalized_cpu,
|
||||||
|
} = collector.options;
|
||||||
|
|
||||||
|
let PrevProc {
|
||||||
|
prev_idle,
|
||||||
|
prev_non_idle,
|
||||||
|
} = &mut collector.prev_proc;
|
||||||
|
|
||||||
|
// TODO: [PROC THREADS] Add threads
|
||||||
|
|
||||||
|
let CpuUsage {
|
||||||
|
mut cpu_usage,
|
||||||
|
cpu_fraction,
|
||||||
|
} = cpu_usage_calculation(prev_idle, prev_non_idle)?;
|
||||||
|
|
||||||
|
if unnormalized_cpu {
|
||||||
|
let num_processors = system.cpus().len() as f64;
|
||||||
|
|
||||||
|
// Note we *divide* here because the later calculation divides `cpu_usage` - in
|
||||||
|
// effect, multiplying over the number of cores.
|
||||||
|
cpu_usage /= num_processors;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pids_to_clear: HashSet<Pid> = collector.pid_mapping.keys().cloned().collect();
|
||||||
|
|
||||||
|
let pids = fs::read_dir("/proc")?.flatten().filter_map(|dir| {
|
||||||
|
if is_str_numeric(dir.file_name().to_string_lossy().trim()) {
|
||||||
|
Some(dir.path())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let args = ReadProcArgs {
|
||||||
|
use_current_cpu_total,
|
||||||
|
cpu_usage,
|
||||||
|
cpu_fraction,
|
||||||
|
total_memory,
|
||||||
|
time_difference_in_secs,
|
||||||
|
uptime: sysinfo::System::uptime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let process_vector: Vec<ProcessHarvest> = pids
|
||||||
|
.filter_map(|pid_path| {
|
||||||
|
if let Ok(process) = Process::from_path(pid_path) {
|
||||||
|
let pid = process.pid;
|
||||||
|
let prev_proc_details = collector.pid_mapping.entry(pid).or_default();
|
||||||
|
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
if let Ok((mut process_harvest, new_process_times)) =
|
||||||
|
read_proc(prev_proc_details, process, args, &mut collector.user_table)
|
||||||
|
{
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
if let Some(gpus) = &collector.gpu_pids {
|
||||||
|
gpus.iter().for_each(|gpu| {
|
||||||
|
// add mem/util for all gpus to pid
|
||||||
|
if let Some((mem, util)) = gpu.get(&(pid as u32)) {
|
||||||
|
process_harvest.gpu_mem += mem;
|
||||||
|
process_harvest.gpu_util += util;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(gpu_total_mem) = gpus_total_mem {
|
||||||
|
process_harvest.gpu_mem_percent =
|
||||||
|
(process_harvest.gpu_mem as f64 / gpu_total_mem as f64 * 100.0)
|
||||||
|
as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_proc_details.cpu_time = new_process_times;
|
||||||
|
prev_proc_details.total_read_bytes = process_harvest.total_read_bytes;
|
||||||
|
prev_proc_details.total_write_bytes = process_harvest.total_write_bytes;
|
||||||
|
|
||||||
|
pids_to_clear.remove(&pid);
|
||||||
|
return Some(process_harvest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
pids_to_clear.iter().for_each(|pid| {
|
||||||
|
collector.pid_mapping.remove(pid);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(process_vector)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_proc_cpu_parse() {
|
||||||
|
assert_eq!(
|
||||||
|
(100_f64, 200_f64),
|
||||||
|
fetch_cpu_usage("100 0 100 100"),
|
||||||
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 4 values"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(120_f64, 200_f64),
|
||||||
|
fetch_cpu_usage("100 0 100 100 20"),
|
||||||
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 5 values"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(120_f64, 230_f64),
|
||||||
|
fetch_cpu_usage("100 0 100 100 20 30"),
|
||||||
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 6 values"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(120_f64, 270_f64),
|
||||||
|
fetch_cpu_usage("100 0 100 100 20 30 40"),
|
||||||
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 7 values"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(120_f64, 320_f64),
|
||||||
|
fetch_cpu_usage("100 0 100 100 20 30 40 50"),
|
||||||
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 8 values"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(120_f64, 320_f64),
|
||||||
|
fetch_cpu_usage("100 0 100 100 20 30 40 50 100"),
|
||||||
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 9 values"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
(120_f64, 320_f64),
|
||||||
|
fetch_cpu_usage("100 0 100 100 20 30 40 50 100 200"),
|
||||||
|
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 10 values"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,308 @@
|
||||||
|
//! Linux process code for getting process data via `/proc/`.
|
||||||
|
//! Based on the [procfs](https://github.com/eminence/procfs) crate.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{self, BufRead, BufReader, Read},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::OnceLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use libc::uid_t;
|
||||||
|
use rustix::{
|
||||||
|
fd::OwnedFd,
|
||||||
|
fs::{Mode, OFlags},
|
||||||
|
path::Arg,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::data_collection::processes::Pid;
|
||||||
|
|
||||||
|
static PAGESIZE: OnceLock<u64> = OnceLock::new();
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next_part<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Result<&'a str, io::Error> {
|
||||||
|
iter.next()
|
||||||
|
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see
|
||||||
|
/// [here](https://man7.org/linux/man-pages/man5/proc.5.html).
|
||||||
|
///
|
||||||
|
/// Note this does not necessarily get all fields, only the ones we use in
|
||||||
|
/// bottom.
|
||||||
|
pub(crate) struct Stat {
|
||||||
|
/// The filename of the executable without parentheses.
|
||||||
|
pub comm: String,
|
||||||
|
|
||||||
|
/// The current process state, represented by a char.
|
||||||
|
pub state: char,
|
||||||
|
|
||||||
|
/// The parent process PID.
|
||||||
|
pub ppid: Pid,
|
||||||
|
|
||||||
|
/// The amount of time this process has been scheduled in user mode in clock
|
||||||
|
/// ticks.
|
||||||
|
pub utime: u64,
|
||||||
|
|
||||||
|
/// The amount of time this process has been scheduled in kernel mode in
|
||||||
|
/// clock ticks.
|
||||||
|
pub stime: u64,
|
||||||
|
|
||||||
|
/// The resident set size, or the number of pages the process has in real
|
||||||
|
/// memory.
|
||||||
|
pub rss: u64,
|
||||||
|
|
||||||
|
/// The start time of the process, represented in clock ticks.
|
||||||
|
pub start_time: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stat {
|
||||||
|
#[inline]
|
||||||
|
fn from_file(mut f: File, buffer: &mut String) -> anyhow::Result<Stat> {
|
||||||
|
// Since this is just one line, we can read it all at once. However, since it
|
||||||
|
// might have non-utf8 characters, we can't just use read_to_string.
|
||||||
|
f.read_to_end(unsafe { buffer.as_mut_vec() })?;
|
||||||
|
|
||||||
|
let line = buffer.to_string_lossy();
|
||||||
|
let line = line.trim();
|
||||||
|
|
||||||
|
let (comm, rest) = {
|
||||||
|
let start_paren = line
|
||||||
|
.find('(')
|
||||||
|
.ok_or_else(|| anyhow!("start paren missing"))?;
|
||||||
|
let end_paren = line.find(')').ok_or_else(|| anyhow!("end paren missing"))?;
|
||||||
|
|
||||||
|
(
|
||||||
|
line[start_paren + 1..end_paren].to_string(),
|
||||||
|
&line[end_paren + 2..],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rest = rest.split(' ');
|
||||||
|
let state = next_part(&mut rest)?
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("missing state"))?;
|
||||||
|
let ppid: Pid = next_part(&mut rest)?.parse()?;
|
||||||
|
|
||||||
|
// Skip 9 fields until utime (pgrp, session, tty_nr, tpgid, flags, minflt,
|
||||||
|
// cminflt, majflt, cmajflt).
|
||||||
|
let mut rest = rest.skip(9);
|
||||||
|
let utime: u64 = next_part(&mut rest)?.parse()?;
|
||||||
|
let stime: u64 = next_part(&mut rest)?.parse()?;
|
||||||
|
|
||||||
|
// Skip 6 fields until starttime (cutime, cstime, priority, nice, num_threads,
|
||||||
|
// itrealvalue).
|
||||||
|
let mut rest = rest.skip(6);
|
||||||
|
let start_time: u64 = next_part(&mut rest)?.parse()?;
|
||||||
|
|
||||||
|
// Skip one field until rss (vsize)
|
||||||
|
let mut rest = rest.skip(1);
|
||||||
|
let rss: u64 = next_part(&mut rest)?.parse()?;
|
||||||
|
|
||||||
|
Ok(Stat {
|
||||||
|
comm,
|
||||||
|
state,
|
||||||
|
ppid,
|
||||||
|
utime,
|
||||||
|
stime,
|
||||||
|
rss,
|
||||||
|
start_time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the Resident Set Size in bytes.
|
||||||
|
#[inline]
|
||||||
|
pub fn rss_bytes(&self) -> u64 {
|
||||||
|
self.rss * PAGESIZE.get_or_init(|| rustix::param::page_size() as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around the data in `/proc/<PID>/io`.
|
||||||
|
///
|
||||||
|
/// Note this does not necessarily get all fields, only the ones we use in
|
||||||
|
/// bottom.
|
||||||
|
pub(crate) struct Io {
|
||||||
|
pub read_bytes: u64,
|
||||||
|
pub write_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Io {
|
||||||
|
#[inline]
|
||||||
|
fn from_file(f: File, buffer: &mut String) -> anyhow::Result<Io> {
|
||||||
|
const NUM_FIELDS: u16 = 0; // Make sure to update this if you want more fields!
|
||||||
|
enum Fields {
|
||||||
|
ReadBytes,
|
||||||
|
WriteBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut read_fields = 0;
|
||||||
|
let mut reader = BufReader::new(f);
|
||||||
|
|
||||||
|
let mut read_bytes = 0;
|
||||||
|
let mut write_bytes = 0;
|
||||||
|
|
||||||
|
// This saves us from doing a string allocation on each iteration compared to
|
||||||
|
// `lines()`.
|
||||||
|
while let Ok(bytes) = reader.read_line(buffer) {
|
||||||
|
if bytes > 0 {
|
||||||
|
if buffer.is_empty() {
|
||||||
|
// Empty, no need to clear.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parts = buffer.split_whitespace();
|
||||||
|
|
||||||
|
if let Some(field) = parts.next() {
|
||||||
|
let curr_field = match field {
|
||||||
|
"read_bytes:" => Fields::ReadBytes,
|
||||||
|
"write_bytes:" => Fields::WriteBytes,
|
||||||
|
_ => {
|
||||||
|
buffer.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = parts.next() {
|
||||||
|
let value = value.parse::<u64>()?;
|
||||||
|
match curr_field {
|
||||||
|
Fields::ReadBytes => {
|
||||||
|
read_bytes = value;
|
||||||
|
read_fields += 1;
|
||||||
|
}
|
||||||
|
Fields::WriteBytes => {
|
||||||
|
write_bytes = value;
|
||||||
|
read_fields += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick short circuit if we have already read all the required fields.
|
||||||
|
if read_fields == NUM_FIELDS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.clear();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Io {
|
||||||
|
read_bytes,
|
||||||
|
write_bytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around a Linux process operations in `/proc/<PID>`.
|
||||||
|
///
|
||||||
|
/// Core documentation based on [proc's manpages](https://man7.org/linux/man-pages/man5/proc.5.html).
|
||||||
|
pub(crate) struct Process {
|
||||||
|
pub pid: Pid,
|
||||||
|
pub uid: Option<uid_t>,
|
||||||
|
pub stat: Stat,
|
||||||
|
pub io: anyhow::Result<Io>,
|
||||||
|
pub cmdline: anyhow::Result<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn reset(root: &mut PathBuf, buffer: &mut String) {
|
||||||
|
root.pop();
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Process {
|
||||||
|
/// Creates a new [`Process`] given a `/proc/<PID>` path. This may fail if
|
||||||
|
/// the process no longer exists or there are permissions issues.
|
||||||
|
///
|
||||||
|
/// Note that this pre-allocates fields on **creation**! As such, some data
|
||||||
|
/// might end up "outdated" depending on when you call some of the
|
||||||
|
/// methods. Therefore, this struct is only useful for either fields
|
||||||
|
/// that are unlikely to change, or are short-lived and
|
||||||
|
/// will be discarded quickly.
|
||||||
|
pub(crate) fn from_path(pid_path: PathBuf) -> anyhow::Result<Process> {
|
||||||
|
// TODO: Pass in a buffer vec/string to share?
|
||||||
|
|
||||||
|
let fd = rustix::fs::openat(
|
||||||
|
rustix::fs::CWD,
|
||||||
|
&pid_path,
|
||||||
|
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||||
|
Mode::empty(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let pid = pid_path
|
||||||
|
.as_path()
|
||||||
|
.components()
|
||||||
|
.last()
|
||||||
|
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
|
||||||
|
.or_else(|| {
|
||||||
|
rustix::fs::readlinkat(rustix::fs::CWD, &pid_path, vec![])
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow!("PID for {pid_path:?} was not found"))?;
|
||||||
|
|
||||||
|
let uid = {
|
||||||
|
let metadata = rustix::fs::fstat(&fd);
|
||||||
|
match metadata {
|
||||||
|
Ok(md) => Some(md.st_uid),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut root = pid_path;
|
||||||
|
let mut buffer = String::new();
|
||||||
|
|
||||||
|
// NB: Whenever you add a new stat, make sure to pop the root and clear the
|
||||||
|
// buffer!
|
||||||
|
let stat =
|
||||||
|
open_at(&mut root, "stat", &fd).and_then(|file| Stat::from_file(file, &mut buffer))?;
|
||||||
|
reset(&mut root, &mut buffer);
|
||||||
|
|
||||||
|
let cmdline = cmdline(&mut root, &fd, &mut buffer);
|
||||||
|
reset(&mut root, &mut buffer);
|
||||||
|
|
||||||
|
let io = open_at(&mut root, "io", &fd).and_then(|file| Io::from_file(file, &mut buffer));
|
||||||
|
|
||||||
|
Ok(Process {
|
||||||
|
pid,
|
||||||
|
uid,
|
||||||
|
stat,
|
||||||
|
io,
|
||||||
|
cmdline,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn cmdline(root: &mut PathBuf, fd: &OwnedFd, buffer: &mut String) -> anyhow::Result<Vec<String>> {
|
||||||
|
open_at(root, "cmdline", fd)
|
||||||
|
.map(|mut file| file.read_to_string(buffer))
|
||||||
|
.map(|_| {
|
||||||
|
buffer
|
||||||
|
.split('\0')
|
||||||
|
.filter_map(|s| {
|
||||||
|
if !s.is_empty() {
|
||||||
|
Some(s.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a path. Note that this function takes in a mutable root - this will
|
||||||
|
/// mutate it to avoid allocations. You probably will want to pop the most
|
||||||
|
/// recent child after if you need to use the buffer again.
|
||||||
|
#[inline]
|
||||||
|
fn open_at(root: &mut PathBuf, child: &str, fd: &OwnedFd) -> anyhow::Result<File> {
|
||||||
|
root.push(child);
|
||||||
|
let new_fd = rustix::fs::openat(fd, &*root, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?;
|
||||||
|
|
||||||
|
Ok(File::from(new_fd))
|
||||||
|
}
|
|
@ -8,15 +8,17 @@ use std::{
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
|
|
||||||
use super::{TempHarvest, TemperatureType};
|
use crate::{
|
||||||
use crate::app::filter::Filter;
|
app::filter::Filter,
|
||||||
|
new_data_collection::sources::common::temperature::{TemperatureData, TemperatureType},
|
||||||
|
};
|
||||||
|
|
||||||
const EMPTY_NAME: &str = "Unknown";
|
const EMPTY_NAME: &str = "Unknown";
|
||||||
|
|
||||||
/// Returned results from grabbing hwmon/coretemp temperature sensor
|
/// Returned results from grabbing hwmon/coretemp temperature sensor
|
||||||
/// values/names.
|
/// values/names.
|
||||||
struct HwmonResults {
|
struct HwmonResults {
|
||||||
temperatures: Vec<TempHarvest>,
|
temperatures: Vec<TemperatureData>,
|
||||||
num_hwmon: usize,
|
num_hwmon: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,7 +226,7 @@ fn is_device_awake(path: &Path) -> bool {
|
||||||
/// once this happens, the device will be *kept* on through the sensor
|
/// once this happens, the device will be *kept* on through the sensor
|
||||||
/// reading, and not be able to re-enter ACPI D3cold.
|
/// reading, and not be able to re-enter ACPI D3cold.
|
||||||
fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> HwmonResults {
|
fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> HwmonResults {
|
||||||
let mut temperatures: Vec<TempHarvest> = vec![];
|
let mut temperatures: Vec<TemperatureData> = vec![];
|
||||||
let mut seen_names: HashMap<String, u32> = HashMap::new();
|
let mut seen_names: HashMap<String, u32> = HashMap::new();
|
||||||
|
|
||||||
let (dirs, num_hwmon) = get_hwmon_candidates();
|
let (dirs, num_hwmon) = get_hwmon_candidates();
|
||||||
|
@ -246,7 +248,7 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
|
|
||||||
if !is_device_awake(&file_path) {
|
if !is_device_awake(&file_path) {
|
||||||
let name = finalize_name(None, None, &sensor_name, &mut seen_names);
|
let name = finalize_name(None, None, &sensor_name, &mut seen_names);
|
||||||
temperatures.push(TempHarvest {
|
temperatures.push(TemperatureData {
|
||||||
name,
|
name,
|
||||||
temperature: None,
|
temperature: None,
|
||||||
});
|
});
|
||||||
|
@ -329,7 +331,7 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
// probing hwmon if not needed?
|
// probing hwmon if not needed?
|
||||||
if Filter::optional_should_keep(filter, &name) {
|
if Filter::optional_should_keep(filter, &name) {
|
||||||
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
||||||
temperatures.push(TempHarvest {
|
temperatures.push(TemperatureData {
|
||||||
name,
|
name,
|
||||||
temperature: Some(temp_type.convert_temp_unit(temp_celsius)),
|
temperature: Some(temp_type.convert_temp_unit(temp_celsius)),
|
||||||
});
|
});
|
||||||
|
@ -352,7 +354,7 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
/// See [the Linux kernel documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal)
|
/// See [the Linux kernel documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal)
|
||||||
/// for more details.
|
/// for more details.
|
||||||
fn add_thermal_zone_temperatures(
|
fn add_thermal_zone_temperatures(
|
||||||
temperatures: &mut Vec<TempHarvest>, temp_type: &TemperatureType, filter: &Option<Filter>,
|
temperatures: &mut Vec<TemperatureData>, temp_type: &TemperatureType, filter: &Option<Filter>,
|
||||||
) {
|
) {
|
||||||
let path = Path::new("/sys/class/thermal");
|
let path = Path::new("/sys/class/thermal");
|
||||||
let Ok(read_dir) = path.read_dir() else {
|
let Ok(read_dir) = path.read_dir() else {
|
||||||
|
@ -382,7 +384,7 @@ fn add_thermal_zone_temperatures(
|
||||||
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
||||||
let name = counted_name(&mut seen_names, name);
|
let name = counted_name(&mut seen_names, name);
|
||||||
|
|
||||||
temperatures.push(TempHarvest {
|
temperatures.push(TemperatureData {
|
||||||
name,
|
name,
|
||||||
temperature: Some(temp_type.convert_temp_unit(temp_celsius)),
|
temperature: Some(temp_type.convert_temp_unit(temp_celsius)),
|
||||||
});
|
});
|
||||||
|
@ -396,14 +398,14 @@ fn add_thermal_zone_temperatures(
|
||||||
/// Gets temperature sensors and data.
|
/// Gets temperature sensors and data.
|
||||||
pub fn get_temperature_data(
|
pub fn get_temperature_data(
|
||||||
temp_type: &TemperatureType, filter: &Option<Filter>,
|
temp_type: &TemperatureType, filter: &Option<Filter>,
|
||||||
) -> Result<Option<Vec<TempHarvest>>> {
|
) -> Vec<TemperatureData> {
|
||||||
let mut results = hwmon_temperatures(temp_type, filter);
|
let mut results = hwmon_temperatures(temp_type, filter);
|
||||||
|
|
||||||
if results.num_hwmon == 0 {
|
if results.num_hwmon == 0 {
|
||||||
add_thermal_zone_temperatures(&mut results.temperatures, temp_type, filter);
|
add_thermal_zone_temperatures(&mut results.temperatures, temp_type, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(results.temperatures))
|
results.temperatures
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
//! Re-exports all of the sources.
|
pub mod common;
|
||||||
|
pub mod linux;
|
||||||
|
pub mod macos;
|
||||||
|
#[cfg(feature = "gpu")]
|
||||||
|
pub mod nvidia;
|
||||||
|
pub mod sysinfo;
|
||||||
|
pub mod unix;
|
||||||
|
pub mod windows;
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(any(
|
if #[cfg(target_family = "windows")] {
|
||||||
target_os = "windows",
|
pub use windows::processes::Pid as Pid;
|
||||||
target_os = "macos",
|
} else if #[cfg(target_family = "unix")] {
|
||||||
target_os = "linux",
|
pub use unix::processes::Pid as Pid;
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "dragonfly",
|
|
||||||
target_os = "ios",
|
|
||||||
))] {
|
|
||||||
pub mod starship_battery;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod temperature;
|
|
@ -1,51 +1,26 @@
|
||||||
//! Gets temperature data via sysinfo.
|
//! Gets temperature data via sysinfo.
|
||||||
|
|
||||||
use anyhow::Result;
|
use crate::{
|
||||||
|
app::filter::Filter,
|
||||||
use super::{TempHarvest, TemperatureType};
|
new_data_collection::sources::common::temperature::{TemperatureData, TemperatureType},
|
||||||
use crate::app::filter::Filter;
|
};
|
||||||
|
|
||||||
pub fn get_temperature_data(
|
pub fn get_temperature_data(
|
||||||
components: &sysinfo::Components, temp_type: &TemperatureType, filter: &Option<Filter>,
|
components: &sysinfo::Components, temp_type: &TemperatureType, filter: &Option<Filter>,
|
||||||
) -> Result<Option<Vec<TempHarvest>>> {
|
) -> Vec<TemperatureData> {
|
||||||
let mut temperature_vec: Vec<TempHarvest> = Vec::new();
|
let mut temperature_vec: Vec<TemperatureData> = Vec::new();
|
||||||
|
|
||||||
for component in components {
|
for component in components {
|
||||||
let name = component.label().to_string();
|
let name = component.label().to_string();
|
||||||
|
|
||||||
if Filter::optional_should_keep(filter, &name) {
|
if Filter::optional_should_keep(filter, &name) {
|
||||||
temperature_vec.push(TempHarvest {
|
temperature_vec.push(TemperatureData {
|
||||||
name,
|
name,
|
||||||
temperature: Some(temp_type.convert_temp_unit(component.temperature())),
|
temperature: Some(temp_type.convert_temp_unit(component.temperature())),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For RockPro64 boards on FreeBSD, they apparently use "hw.temperature" for
|
|
||||||
// sensors.
|
|
||||||
#[cfg(target_os = "freebsd")]
|
|
||||||
{
|
|
||||||
use sysctl::Sysctl;
|
|
||||||
|
|
||||||
const KEY: &str = "hw.temperature";
|
|
||||||
if let Ok(root) = sysctl::Ctl::new(KEY) {
|
|
||||||
for ctl in sysctl::CtlIter::below(root).flatten() {
|
|
||||||
if let (Ok(name), Ok(temp)) = (ctl.name(), ctl.value()) {
|
|
||||||
if let Some(temp) = temp.as_temperature() {
|
|
||||||
temperature_vec.push(TempHarvest {
|
|
||||||
name,
|
|
||||||
temperature: Some(match temp_type {
|
|
||||||
TemperatureType::Celsius => temp.celsius(),
|
|
||||||
TemperatureType::Kelvin => temp.kelvin(),
|
|
||||||
TemperatureType::Fahrenheit => temp.fahrenheit(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Should we instead use a hashmap -> vec to skip dupes?
|
// TODO: Should we instead use a hashmap -> vec to skip dupes?
|
||||||
Ok(Some(temperature_vec))
|
temperature_vec
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod processes;
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod user_table;
|
||||||
|
|
||||||
|
/// A UNIX process ID.
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
pub type Pid = libc::pid_t;
|
|
@ -0,0 +1,33 @@
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
|
||||||
|
use crate::data_collection::error::{CollectionError, CollectionResult};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct UserTable {
|
||||||
|
pub uid_user_mapping: HashMap<libc::uid_t, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserTable {
|
||||||
|
pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> CollectionResult<String> {
|
||||||
|
if let Some(user) = self.uid_user_mapping.get(&uid) {
|
||||||
|
Ok(user.clone())
|
||||||
|
} else {
|
||||||
|
// SAFETY: getpwuid returns a null pointer if no passwd entry is found for the
|
||||||
|
// uid
|
||||||
|
let passwd = unsafe { libc::getpwuid(uid) };
|
||||||
|
|
||||||
|
if passwd.is_null() {
|
||||||
|
Err("passwd is inaccessible".into())
|
||||||
|
} else {
|
||||||
|
// SAFETY: We return early if passwd is null.
|
||||||
|
let username = unsafe { std::ffi::CStr::from_ptr((*passwd).pw_name) }
|
||||||
|
.to_str()
|
||||||
|
.map_err(|err| CollectionError::General(err.into()))?
|
||||||
|
.to_string();
|
||||||
|
self.uid_user_mapping.insert(uid, username.clone());
|
||||||
|
|
||||||
|
Ok(username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod processes;
|
|
@ -0,0 +1,3 @@
|
||||||
|
/// A Windows process ID.
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
pub type Pid = usize;
|
Loading…
Reference in New Issue