diff --git a/docs/modules/Sys-Info.md b/docs/modules/Sys-Info.md index d26c2719..67cf162b 100644 --- a/docs/modules/Sys-Info.md +++ b/docs/modules/Sys-Info.md @@ -153,6 +153,12 @@ More information about each of these and the additional options can be found fur | `{swap_used}` | N/A | GB | `0<4.1` | | `{swap_total}` | N/A | GB | `0<4.1` | | `{swap_percent}` | N/A | GB | `0<4.1` | +| **GPU** | | | | +| `{gpu_percent}` | N/A | % | `0<2` | +| `{gpu_memory_free}` | N/A | GB | `0<4.1` | +| `{gpu_memory_total}` | N/A | GB | `0<4.1` | +| `{gpu_memory_used}` | N/A | GB | `0<4.1` | +| `{gpu_memory_percent}` | N/A | % | `0<2` | | **Temperature** | | | | | `{temp_c[@sensor]}` | `max` | °C | | | `{temp_f[@sensor]}` | `max` | °F | | @@ -196,6 +202,7 @@ It is also possible to get only a single value from the set by specifying a name | Token category | Valid name | |----------------|------------------------------------------| | CPU | A CPU thread, eg `cpu0`, `cpu1`, ... | +| GPU | A PCI bus address, eg `0000:07:00.0` | | Temperature | A sensor name, eg `CPUTIN`. | | Disk | A disk mountpoint, eg `/`, `/home`, ... | | Network | An adapter name, eg `eth0` or `enp30s0`. | @@ -357,6 +364,7 @@ Possible types: - `Memory` - `Cpu` +- `Gpu` - `Temps` - `Disks` - `Network` diff --git a/src/clients/libinput/mod.rs b/src/clients/libinput/mod.rs index 52fe393b..f124655b 100644 --- a/src/clients/libinput/mod.rs +++ b/src/clients/libinput/mod.rs @@ -298,36 +298,42 @@ impl super::Client for Client { let tx = channel.clone(); let known_devices = known_devices.clone(); - std::thread::spawn(move || { - let local = LocalSet::new(); - - local.spawn_local(async move { - let stream = match Self::connect(&seat) { - Ok(stream) => { - ready_tx.send(Ok(())).expect(crate::error::ERR_CHANNEL_SEND); - stream - } - Err(err) => { - ready_tx - .send(Err(err)) - .expect(crate::error::ERR_CHANNEL_SEND); - return; - } - }; + let thread = std::thread::Builder::new() + .name("ironbar-libinput".to_string()) + .spawn(move || { + let local = LocalSet::new(); + + local.spawn_local(async move { + let stream = match Self::connect(&seat) { + Ok(stream) => { + ready_tx.send(Ok(())).expect(crate::error::ERR_CHANNEL_SEND); + stream + } + Err(err) => { + ready_tx + .send(Err(err)) + .expect(crate::error::ERR_CHANNEL_SEND); + return; + } + }; - tokio::select! { - res = Self::run(stream, &tx, &known_devices) => { - if let Err(err) = res { - error!("{err:?}"); + tokio::select! { + res = Self::run(stream, &tx, &known_devices) => { + if let Err(err) = res { + error!("{err:?}"); + } } + // resolves on explicit stop, or when the client is dropped + _ = stop_rx => debug!("stopping libinput client for seat '{seat}'"), } - // resolves on explicit stop, or when the client is dropped - _ = stop_rx => debug!("stopping libinput client for seat '{seat}'"), - } + }); + + Ironbar::runtime().block_on(local); }); - Ironbar::runtime().block_on(local); - }); + if let Err(err) = thread { + error!("{err:?}"); + } } ready_rx.await.map_err(|_| Error::ThreadExited)??; diff --git a/src/clients/sysinfo/ironvar.rs b/src/clients/sysinfo/ironvar.rs index 5a20665d..f86616fd 100644 --- a/src/clients/sysinfo/ironvar.rs +++ b/src/clients/sysinfo/ironvar.rs @@ -15,28 +15,41 @@ impl Namespace for Client { match token { TokenType::CpuFrequency => None, TokenType::CpuPercent => None, + TokenType::MemoryFree => get(data.memory_free()), TokenType::MemoryAvailable => get(data.memory_available()), TokenType::MemoryTotal => get(data.memory_total()), TokenType::MemoryUsed => get(data.memory_used()), TokenType::MemoryPercent => get(data.memory_percent()), + TokenType::SwapFree => get(data.swap_free()), TokenType::SwapTotal => get(data.swap_total()), TokenType::SwapUsed => get(data.swap_used()), TokenType::SwapPercent => get(data.swap_percent()), + + TokenType::GpuPercent => None, + TokenType::GpuMemoryFree => None, + TokenType::GpuMemoryTotal => None, + TokenType::GpuMemoryUsed => None, + TokenType::GpuMemoryPercent => None, + TokenType::TempC => None, TokenType::TempF => None, + TokenType::DiskFree => None, TokenType::DiskTotal => None, TokenType::DiskUsed => None, TokenType::DiskPercent => None, TokenType::DiskRead => None, TokenType::DiskWrite => None, + TokenType::NetDown => None, TokenType::NetUp => None, + TokenType::LoadAverage1 => get(data.load_average_1()), TokenType::LoadAverage5 => get(data.load_average_5()), TokenType::LoadAverage15 => get(data.load_average_15()), + TokenType::Uptime => Client::uptime(), } } @@ -66,6 +79,10 @@ impl Namespace for Client { vec![ "cpu_frequency", "cpu_percent", + "gpu_percent", + "gpu_memory_total", + "gpu_memory_used", + "gpu_memory_percent", "temp_c", "temp_f", "disk_free", @@ -90,28 +107,41 @@ impl Namespace for Client { match token { TokenType::CpuFrequency => Some(Arc::new(data.cpu_frequency())), TokenType::CpuPercent => Some(Arc::new(data.cpu_percent())), + TokenType::MemoryFree => None, TokenType::MemoryAvailable => None, TokenType::MemoryTotal => None, TokenType::MemoryUsed => None, TokenType::MemoryPercent => None, + TokenType::SwapFree => None, TokenType::SwapTotal => None, TokenType::SwapUsed => None, TokenType::SwapPercent => None, + + TokenType::GpuPercent => Some(Arc::new(data.gpu_percent())), + TokenType::GpuMemoryFree => Some(Arc::new(data.gpu_memory_free())), + TokenType::GpuMemoryTotal => Some(Arc::new(data.gpu_memory_total())), + TokenType::GpuMemoryUsed => Some(Arc::new(data.gpu_memory_used())), + TokenType::GpuMemoryPercent => Some(Arc::new(data.gpu_memory_percent())), + TokenType::TempC => Some(Arc::new(data.temp_c())), TokenType::TempF => Some(Arc::new(data.temp_f())), + TokenType::DiskFree => Some(Arc::new(data.disk_free())), TokenType::DiskTotal => Some(Arc::new(data.disk_total())), TokenType::DiskUsed => Some(Arc::new(data.disk_used())), TokenType::DiskPercent => Some(Arc::new(data.disk_percent())), TokenType::DiskRead => Some(Arc::new(data.disk_read())), TokenType::DiskWrite => Some(Arc::new(data.disk_write())), + TokenType::NetDown => Some(Arc::new(data.net_down())), TokenType::NetUp => Some(Arc::new(data.net_up())), + TokenType::LoadAverage1 => None, TokenType::LoadAverage5 => None, TokenType::LoadAverage15 => None, + TokenType::Uptime => None, } } diff --git a/src/clients/sysinfo/lua.rs b/src/clients/sysinfo/lua.rs index 09d7b225..44f70aee 100644 --- a/src/clients/sysinfo/lua.rs +++ b/src/clients/sysinfo/lua.rs @@ -28,6 +28,11 @@ pub(super) fn add_methods>>(methods: &mut M swap_total => SwapTotal, swap_used => SwapUsed, swap_percent => SwapPercent, + gpu_percent => GpuPercent, + gpu_memory_free => GpuMemoryFree, + gpu_memory_total => GpuMemoryTotal, + gpu_memory_used => GpuMemoryUsed, + gpu_memory_percent => GpuMemoryPercent, temp_c => TempC, temp_f => TempF, disk_free => DiskFree, diff --git a/src/clients/sysinfo/mod.rs b/src/clients/sysinfo/mod.rs index f8a3bd43..04568d50 100644 --- a/src/clients/sysinfo/mod.rs +++ b/src/clients/sysinfo/mod.rs @@ -12,9 +12,11 @@ use std::collections::HashMap; use std::fmt::Debug; use std::str::FromStr; use std::sync::{Arc, Mutex}; +use std::thread; use std::time::{Duration, Instant}; use sysinfo::{ - Component, Components, Cpu, Disk, Disks, LoadAvg, NetworkData, Networks, RefreshKind, System, + Component, Components, Cpu, Disk, Disks, Gpus, LoadAvg, NetworkData, Networks, RefreshKind, + System, }; use thiserror::Error; use tokio::sync::{broadcast, mpsc}; @@ -35,6 +37,7 @@ const MIN_REFRESH_GAP: Duration = Duration::from_millis(500); pub enum RefreshType { Memory, Cpu, + Gpu, Temps, Disks, Network, @@ -42,9 +45,10 @@ pub enum RefreshType { } impl RefreshType { - pub const ALL: [Self; 6] = [ + pub const ALL: [Self; 7] = [ Self::Memory, Self::Cpu, + Self::Gpu, Self::Temps, Self::Disks, Self::Network, @@ -68,6 +72,12 @@ pub enum TokenType { SwapUsed, SwapPercent, + GpuPercent, + GpuMemoryFree, + GpuMemoryTotal, + GpuMemoryUsed, + GpuMemoryPercent, + TempC, TempF, @@ -112,6 +122,12 @@ impl FromStr for TokenType { "swap_used" => Ok(Self::SwapUsed), "swap_percent" => Ok(Self::SwapPercent), + "gpu_percent" => Ok(Self::GpuPercent), + "gpu_memory_free" => Ok(Self::GpuMemoryFree), + "gpu_memory_total" => Ok(Self::GpuMemoryTotal), + "gpu_memory_used" => Ok(Self::GpuMemoryUsed), + "gpu_memory_percent" => Ok(Self::GpuMemoryPercent), + "temp_c" => Ok(Self::TempC), "temp_f" => Ok(Self::TempF), @@ -150,6 +166,11 @@ impl TokenType { | Self::SwapTotal | Self::SwapUsed | Self::SwapPercent => RefreshType::Memory, + Self::GpuPercent + | Self::GpuMemoryFree + | Self::GpuMemoryTotal + | Self::GpuMemoryUsed + | Self::GpuMemoryPercent => RefreshType::Gpu, Self::TempC | Self::TempF => RefreshType::Temps, Self::DiskFree | Self::DiskTotal @@ -210,6 +231,17 @@ pub struct State { #[cfg_attr(feature = "lua", lua(get))] pub memory_percent: Value, + #[cfg_attr(feature = "lua", lua(get))] + pub gpu_percent: ValueSet, + #[cfg_attr(feature = "lua", lua(get))] + pub gpu_memory_free: ValueSet, + #[cfg_attr(feature = "lua", lua(get))] + pub gpu_memory_total: ValueSet, + #[cfg_attr(feature = "lua", lua(get))] + pub gpu_memory_used: ValueSet, + #[cfg_attr(feature = "lua", lua(get))] + pub gpu_memory_percent: ValueSet, + #[cfg_attr(feature = "lua", lua(get))] pub swap_free: Value, #[cfg_attr(feature = "lua", lua(get))] @@ -347,6 +379,11 @@ impl super::Client for Client { swap_total: data.swap_total(), swap_used: data.swap_used(), swap_percent: data.swap_percent(), + gpu_percent: data.gpu_percent(), + gpu_memory_free: data.gpu_memory_free(), + gpu_memory_total: data.gpu_memory_total(), + gpu_memory_used: data.gpu_memory_used(), + gpu_memory_percent: data.gpu_memory_percent(), temp_c: data.temp_c(), temp_f: data.temp_f(), disk_free: data.disk_free(), @@ -380,6 +417,8 @@ impl super::Client for Client { #[derive(Debug)] pub struct Data { system: Mutex, + gpu_worker: GpuWorker, + gpus: Mutex>, disks: Mutex>, components: Mutex, networks: Mutex>, @@ -394,6 +433,7 @@ impl Data { let refresh_kind = RefreshKind::everything().without_processes(); let system = System::new_with_specifics(refresh_kind)?; + let (gpu_worker, gpus) = GpuWorker::spawn()?; let disks = Disks::new_with_refreshed_list()?; let components = Components::new_with_refreshed_list()?; let networks = Networks::new_with_refreshed_list()?; @@ -403,6 +443,8 @@ impl Data { Ok(Self { system: Mutex::new(system), + gpu_worker, + gpus: Mutex::new(gpus), disks: Mutex::new(Delta::new(disks)), components: Mutex::new(components), networks: Mutex::new(Delta::new(networks)), @@ -525,6 +567,10 @@ impl Data { match kind { RefreshType::Memory => lock!(self.system).refresh_memory(), RefreshType::Cpu => lock!(self.system).refresh_cpu_all(), + RefreshType::Gpu => match self.gpu_worker.refresh() { + Some(gpus) => *lock!(self.gpus) = gpus, + None => warn!("failed to refresh gpus: worker thread is not running"), + }, RefreshType::Temps => lock!(self.components).refresh(true), RefreshType::Disks => lock!(self.disks).refresh_with(|disks| disks.refresh(true)), RefreshType::Network => { @@ -589,6 +635,41 @@ impl Data { Value::new(used / total * 100.0) } + pub fn gpu_percent(&self) -> ValueSet { + self.map_gpus(|gpu| Value::new(gpu.usage.unwrap_or_default() as f64)) + } + + pub fn gpu_memory_free(&self) -> ValueSet { + self.map_gpus(|gpu| { + Value::new( + gpu.total_memory + .unwrap_or_default() + .saturating_sub(gpu.used_memory.unwrap_or_default()) as f64, + ) + }) + } + + pub fn gpu_memory_total(&self) -> ValueSet { + self.map_gpus(|gpu| Value::new(gpu.total_memory.unwrap_or_default() as f64)) + } + + pub fn gpu_memory_used(&self) -> ValueSet { + self.map_gpus(|gpu| Value::new(gpu.used_memory.unwrap_or_default() as f64)) + } + + pub fn gpu_memory_percent(&self) -> ValueSet { + self.map_gpus(|gpu| { + let total = gpu.total_memory.unwrap_or_default() as f64; + let used = gpu.used_memory.unwrap_or_default() as f64; + + Value::new(if total > 0.0 { + used / total * 100.0 + } else { + 0.0 + }) + }) + } + pub fn temp_c(&self) -> ValueSet { self.map_components(|comp| comp.temperature().unwrap_or_default() as f64) } @@ -656,6 +737,14 @@ impl Data { .collect() } + #[inline] + fn map_gpus(&self, value: impl Fn(&GpuStats) -> Value) -> ValueSet { + lock!(self.gpus) + .iter() + .map(|gpu| (gpu.pci.as_str().into(), value(gpu))) + .collect() + } + #[inline] fn map_components(&self, value: impl Fn(&Component) -> f64) -> ValueSet { lock!(self.components) @@ -693,6 +782,88 @@ impl Data { } } +/// A `Send` snapshot of a single GPU's stats. +/// +/// [`Gpus`] holds raw handles to dynamically loaded libraries (NVML, Vulkan) so is `!Send`. +#[derive(Debug, Clone)] +struct GpuStats { + pci: String, + usage: Option, + total_memory: Option, + used_memory: Option, +} + +impl GpuStats { + fn snapshot(gpus: &Gpus) -> Vec { + gpus.iter() + .map(|gpu| Self { + pci: gpu.pci().to_string(), + usage: gpu.usage(), + total_memory: gpu.total_memory(), + used_memory: gpu.used_memory(), + }) + .collect() + } +} + +/// Handle to a dedicated thread which owns the `!Send` [`Gpus`]. +#[derive(Debug)] +struct GpuWorker { + tx: std::sync::mpsc::Sender>>, +} + +impl GpuWorker { + fn spawn() -> Result<(Self, Vec), sysinfo::Error> { + use std::sync::mpsc; + + let (init_tx, init_rx) = mpsc::sync_channel(1); + let (tx, rx) = mpsc::channel::>>(); + + let spawned = thread::Builder::new() + .name("ironbar-sysinfo-gpu".to_string()) + .spawn(move || { + let mut gpus = match Gpus::new_with_refreshed_list() { + Ok(gpus) => { + let _ = init_tx.send(Ok(GpuStats::snapshot(&gpus))); + gpus + } + Err(err) => { + let _ = init_tx.send(Err(err)); + return; + } + }; + + for reply in rx { + gpus.refresh(true); + let _ = reply.send(GpuStats::snapshot(&gpus)); + } + }); + + if let Err(err) = spawned { + warn!("failed to spawn gpu worker thread: {err}"); + return Ok((Self { tx }, Vec::new())); + } + + match init_rx.recv() { + Ok(Ok(gpus)) => Ok((Self { tx }, gpus)), + Ok(Err(err)) => Err(err), + Err(_) => { + warn!("gpu worker thread exited during initialisation"); + Ok((Self { tx }, Vec::new())) + } + } + } + + /// Refreshes the GPU data. + /// + /// Returns `None` if the worker thread is not running. + fn refresh(&self) -> Option> { + let (reply_tx, reply_rx) = std::sync::mpsc::sync_channel(1); + self.tx.send(reply_tx).ok()?; + reply_rx.recv().ok() + } +} + register_fallible_client!(Client, sys_info); #[cfg(test)] diff --git a/src/modules/sysinfo/mod.rs b/src/modules/sysinfo/mod.rs index 32bbeba1..7e198287 100644 --- a/src/modules/sysinfo/mod.rs +++ b/src/modules/sysinfo/mod.rs @@ -76,6 +76,11 @@ pub struct Intervals { /// **Default**: `5` cpu: u64, + /// The number of seconds between refreshing GPU data. + /// + /// **Default**: `5` + gpu: u64, + /// The number of seconds between refreshing temperature data. /// /// **Default**: `5` @@ -102,6 +107,7 @@ impl Default for Intervals { Self { memory: 5, cpu: 5, + gpu: 5, temps: 5, disks: 5, networks: 5, @@ -131,6 +137,7 @@ impl Interval { Self::Individual(intervals) => match kind { RefreshType::Memory => intervals.memory, RefreshType::Cpu => intervals.cpu, + RefreshType::Gpu => intervals.gpu, RefreshType::Temps => intervals.temps, RefreshType::Disks => intervals.disks, RefreshType::Network => intervals.networks, diff --git a/src/modules/sysinfo/parser.rs b/src/modules/sysinfo/parser.rs index bf4b0262..36944195 100644 --- a/src/modules/sysinfo/parser.rs +++ b/src/modules/sysinfo/parser.rs @@ -11,7 +11,12 @@ impl Function { | TokenType::CpuPercent | TokenType::TempC | TokenType::DiskPercent => Self::Mean, - TokenType::DiskFree + TokenType::GpuPercent + | TokenType::GpuMemoryFree + | TokenType::GpuMemoryTotal + | TokenType::GpuMemoryUsed + | TokenType::GpuMemoryPercent + | TokenType::DiskFree | TokenType::DiskTotal | TokenType::DiskUsed | TokenType::DiskRead @@ -34,6 +39,9 @@ impl Prefix { | TokenType::SwapFree | TokenType::SwapTotal | TokenType::SwapUsed + | TokenType::GpuMemoryFree + | TokenType::GpuMemoryTotal + | TokenType::GpuMemoryUsed | TokenType::DiskFree | TokenType::DiskTotal | TokenType::DiskUsed => Self::Giga, @@ -95,7 +103,7 @@ impl Formatting { align: Alignment::default(), precision: 2, }, - TokenType::CpuPercent => Self { + TokenType::CpuPercent | TokenType::GpuPercent | TokenType::GpuMemoryPercent => Self { width: 2, fill: '0', align: Alignment::default(), @@ -109,7 +117,10 @@ impl Formatting { | TokenType::SwapFree | TokenType::SwapTotal | TokenType::SwapUsed - | TokenType::SwapPercent => Self { + | TokenType::SwapPercent + | TokenType::GpuMemoryFree + | TokenType::GpuMemoryTotal + | TokenType::GpuMemoryUsed => Self { width: 4, fill: '0', align: Alignment::default(), diff --git a/src/modules/sysinfo/renderer.rs b/src/modules/sysinfo/renderer.rs index c3a6c118..3bdbf5be 100644 --- a/src/modules/sysinfo/renderer.rs +++ b/src/modules/sysinfo/renderer.rs @@ -53,25 +53,37 @@ impl Token { // Number tokens TokenType::CpuFrequency => apply(client.cpu_frequency()), TokenType::CpuPercent => apply(client.cpu_percent()), + TokenType::MemoryFree => get(client.memory_free()), TokenType::MemoryAvailable => get(client.memory_available()), TokenType::MemoryTotal => get(client.memory_total()), TokenType::MemoryUsed => get(client.memory_used()), TokenType::MemoryPercent => get(client.memory_percent()), + TokenType::SwapFree => get(client.swap_free()), TokenType::SwapTotal => get(client.swap_total()), TokenType::SwapUsed => get(client.swap_used()), TokenType::SwapPercent => get(client.swap_percent()), + + TokenType::GpuPercent => apply(client.gpu_percent()), + TokenType::GpuMemoryFree => apply(client.gpu_memory_free()), + TokenType::GpuMemoryTotal => apply(client.gpu_memory_total()), + TokenType::GpuMemoryUsed => apply(client.gpu_memory_used()), + TokenType::GpuMemoryPercent => apply(client.gpu_memory_percent()), + TokenType::TempC => apply(client.temp_c()), TokenType::TempF => apply(client.temp_f()), + TokenType::DiskFree => apply(client.disk_free()), TokenType::DiskTotal => apply(client.disk_total()), TokenType::DiskUsed => apply(client.disk_used()), TokenType::DiskPercent => apply(client.disk_percent()), TokenType::DiskRead => apply(client.disk_read()), TokenType::DiskWrite => apply(client.disk_write()), + TokenType::NetDown => apply(client.net_down()), TokenType::NetUp => apply(client.net_up()), + TokenType::LoadAverage1 => get(client.load_average_1()), TokenType::LoadAverage5 => get(client.load_average_5()), TokenType::LoadAverage15 => get(client.load_average_15()),