From 8472f1f3589a3c1cdacb0b67be0776decb18f054 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Wed, 24 Jan 2024 21:19:18 +0100 Subject: [PATCH] battery: Add gpu indicator and process list --- Cargo.lock | 23 + cosmic-applet-battery/Cargo.toml | 3 +- .../i18n/de/cosmic_applet_battery.ftl | 2 + .../i18n/en/cosmic_applet_battery.ftl | 2 + cosmic-applet-battery/src/app.rs | 439 ++++++++++++----- cosmic-applet-battery/src/dgpu.rs | 457 ++++++++++++++++++ cosmic-applet-battery/src/main.rs | 2 +- debian/control | 1 + 8 files changed, 798 insertions(+), 131 deletions(-) create mode 100644 cosmic-applet-battery/src/dgpu.rs diff --git a/Cargo.lock b/Cargo.lock index 854f8496..2c5a3911 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,6 +855,7 @@ dependencies = [ "tracing", "tracing-log", "tracing-subscriber", + "udev", "zbus", ] @@ -3218,6 +3219,16 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -5327,6 +5338,18 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "udev" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50051c6e22be28ee6f217d50014f3bc29e81c20dc66ff7ca0d5c5226e1dcc5a1" +dependencies = [ + "io-lifetimes 1.0.11", + "libc", + "libudev-sys", + "pkg-config", +] + [[package]] name = "uds_windows" version = "1.1.0" diff --git a/cosmic-applet-battery/Cargo.toml b/cosmic-applet-battery/Cargo.toml index b386d470..1d1413e7 100644 --- a/cosmic-applet-battery/Cargo.toml +++ b/cosmic-applet-battery/Cargo.toml @@ -17,4 +17,5 @@ tracing-log.workspace = true i18n-embed = { version = "0.13.4", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.6.4" rust-embed = "6.3.0" -tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "fs"] } +tokio = { version = "1.17.0", features = ["sync", "rt", "rt-multi-thread", "fs", "macros"] } +udev = "0.8" \ No newline at end of file diff --git a/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl b/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl index 3b47d70d..a3f516a5 100644 --- a/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl +++ b/cosmic-applet-battery/i18n/de/cosmic_applet_battery.ftl @@ -11,3 +11,5 @@ minutes = m hours = h until-empty = bis leer power-settings = Energie- und Batterieeinstellungen... +dgpu-running = Dedizierte GPU ist aktiv und kann die Batterielaufzeit reduzieren +dgpu-applications = Anwendungen, die die dedizierte GPU {$gpu_name} nutzen \ No newline at end of file diff --git a/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl b/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl index 57d327ed..3214e58b 100644 --- a/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl +++ b/cosmic-applet-battery/i18n/en/cosmic_applet_battery.ftl @@ -11,3 +11,5 @@ minutes = m hours = h until-empty = until empty power-settings = Power and Battery Settings... +dgpu-running = Discrete GPU is active and can reduce battery life +dgpu-applications = Applications using {$gpu_name} discrete GPU \ No newline at end of file diff --git a/cosmic-applet-battery/src/app.rs b/cosmic-applet-battery/src/app.rs index 0e61f0c2..fbd39573 100644 --- a/cosmic-applet-battery/src/app.rs +++ b/cosmic-applet-battery/src/app.rs @@ -2,6 +2,7 @@ use crate::backlight::{ screen_backlight_subscription, ScreenBacklightRequest, ScreenBacklightUpdate, }; use crate::config; +use crate::dgpu::{dgpu_subscription, App, GpuUpdate}; use crate::fl; use crate::power_daemon::{ power_profile_subscription, Power, PowerProfileRequest, PowerProfileUpdate, @@ -10,6 +11,7 @@ use crate::upower_device::{device_subscription, DeviceDbusEvent}; use crate::upower_kbdbacklight::{ kbd_backlight_subscription, KeyboardBacklightRequest, KeyboardBacklightUpdate, }; +use cosmic::applet::cosmic_panel_config::PanelAnchor; use cosmic::applet::token::subscription::{ activation_token_subscription, TokenRequest, TokenUpdate, }; @@ -21,14 +23,19 @@ use cosmic::iced::{ widget::{column, container, row, slider, text}, window, Alignment, Length, Subscription, }; +use cosmic::iced_core::alignment::Vertical; +use cosmic::iced_core::{Background, Color}; use cosmic::iced_runtime::core::layout::Limits; use cosmic::iced_style::application; -use cosmic::widget::{divider, horizontal_space, icon}; +use cosmic::iced_widget::{Column, Row}; +use cosmic::widget::{divider, horizontal_space, icon, scrollable, vertical_space}; use cosmic::Command; use cosmic::{Element, Theme}; use cosmic_time::{anim, chain, id, once_cell::sync::Lazy, Instant, Timeline}; use log::error; +use std::collections::HashMap; +use std::path::PathBuf; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; @@ -49,11 +56,18 @@ fn format_duration(duration: Duration) -> String { } pub fn run() -> cosmic::iced::Result { - cosmic::applet::run::(false, ()) + cosmic::applet::run::(true, ()) } static MAX_CHARGE: Lazy = Lazy::new(id::Toggler::unique); +#[derive(Clone, Default)] +struct GPUData { + name: String, + toggled: bool, + app_list: Option>, +} + #[derive(Clone, Default)] struct CosmicBatteryApplet { core: cosmic::app::Core, @@ -62,6 +76,7 @@ struct CosmicBatteryApplet { charging_limit: bool, battery_percent: f64, on_battery: bool, + gpus: HashMap, time_remaining: Duration, kbd_brightness: f64, screen_brightness: f64, @@ -144,6 +159,9 @@ enum Message { UpdateScreenBrightness(f64), InitKbdBacklight(UnboundedSender, f64), InitScreenBacklight(UnboundedSender, f64), + GpuOn(PathBuf, String, Option>), + GpuOff(PathBuf), + ToggleGpuApps(PathBuf), Errored(String), InitProfile(UnboundedSender, Power), Profile(Power), @@ -319,16 +337,67 @@ impl cosmic::Application for CosmicBatteryApplet { cosmic::process::spawn(cmd); } }, + Message::GpuOn(path, name, app_list) => { + let toggled = self + .gpus + .get(&path) + .map(|data| data.toggled) + .unwrap_or_default(); + self.gpus.insert( + path, + GPUData { + name, + app_list, + toggled, + }, + ); + } + Message::GpuOff(path) => { + self.gpus.remove(&path); + } + Message::ToggleGpuApps(path) => { + if let Some(data) = self.gpus.get_mut(&path) { + data.toggled = !data.toggled; + } + } } Command::none() } fn view(&self) -> Element { - self.core + let btn = self + .core .applet .icon_button(&self.icon_name) .on_press(Message::TogglePopup) - .into() + .into(); + + if !self.gpus.is_empty() { + let dot = container(vertical_space(Length::Fixed(0.0))) + .padding(2.0) + .style(::Style::Custom(Box::new( + |theme| container::Appearance { + text_color: Some(Color::TRANSPARENT), + background: Some(Background::Color(theme.cosmic().accent_color().into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: Some(Color::TRANSPARENT), + }, + ))) + .into(); + + match self.core.applet.anchor { + PanelAnchor::Left | PanelAnchor::Right => Column::with_children(vec![btn, dot]) + .align_items(Alignment::Center) + .into(), + PanelAnchor::Top | PanelAnchor::Bottom => Row::with_children(vec![btn, dot]) + .align_items(Alignment::Center) + .into(), + } + } else { + btn + } } fn view_window(&self, _id: window::Id) -> Element { @@ -344,133 +413,241 @@ impl cosmic::Application for CosmicBatteryApplet { ) }) .size(10); + + let mut content = vec![ + padded_control( + row![ + icon::from_name(&*self.icon_name).size(24).symbolic(true), + column![name, description] + ] + .spacing(8) + .align_items(Alignment::Center), + ) + .into(), + padded_control(divider::horizontal::default()).into(), + menu_button( + row![ + column![ + text(fl!("battery")).size(14), + text(fl!("battery-desc")).size(10) + ] + .width(Length::Fill), + if matches!(self.power_profile, Power::Battery) { + container( + icon::from_name("emblem-ok-symbolic") + .size(12) + .symbolic(true), + ) + } else { + container(horizontal_space(1.0)) + } + ] + .align_items(Alignment::Center), + ) + .on_press(Message::SelectProfile(Power::Battery)) + .into(), + menu_button( + row![ + column![ + text(fl!("balanced")).size(14), + text(fl!("balanced-desc")).size(10) + ] + .width(Length::Fill), + if matches!(self.power_profile, Power::Balanced) { + container( + icon::from_name("emblem-ok-symbolic") + .size(12) + .symbolic(true), + ) + } else { + container(horizontal_space(1.0)) + } + ] + .align_items(Alignment::Center), + ) + .on_press(Message::SelectProfile(Power::Balanced)) + .into(), + menu_button( + row![ + column![ + text(fl!("performance")).size(14), + text(fl!("performance-desc")).size(10) + ] + .width(Length::Fill), + if matches!(self.power_profile, Power::Performance) { + container( + icon::from_name("emblem-ok-symbolic") + .size(12) + .symbolic(true), + ) + } else { + container(horizontal_space(1.0)) + } + ] + .align_items(Alignment::Center), + ) + .on_press(Message::SelectProfile(Power::Performance)) + .into(), + padded_control(divider::horizontal::default()).into(), + padded_control( + anim!( + //toggler + MAX_CHARGE, + &self.timeline, + fl!("max-charge"), + self.charging_limit, + Message::SetChargingLimit, + ) + .text_size(14) + .width(Length::Fill), + ) + .into(), + padded_control(divider::horizontal::default()).into(), + padded_control( + row![ + icon::from_name(self.display_icon_name.as_str()) + .size(24) + .symbolic(true), + slider( + 1..=100, + (self.screen_brightness * 100.0) as i32, + Message::SetScreenBrightness + ), + text(format!("{:.0}%", self.screen_brightness * 100.0)) + .size(16) + .width(Length::Fixed(40.0)) + .horizontal_alignment(Horizontal::Right) + ] + .spacing(12), + ) + .into(), + padded_control( + row![ + icon::from_name("keyboard-brightness-symbolic") + .size(24) + .symbolic(true), + slider( + 0..=100, + (self.kbd_brightness * 100.0) as i32, + Message::SetKbdBrightness + ), + text(format!("{:.0}%", self.kbd_brightness * 100.0)) + .size(16) + .width(Length::Fixed(40.0)) + .horizontal_alignment(Horizontal::Right) + ] + .spacing(12), + ) + .into(), + padded_control(divider::horizontal::default()).into(), + ]; + + if !self.gpus.is_empty() { + content.push( + padded_control( + row![ + text(fl!("dgpu-running")) + .size(16) + .width(Length::Fill) + .horizontal_alignment(Horizontal::Left), + container(vertical_space(Length::Fixed(0.0))) + .padding(4) + .style(::Style::Custom(Box::new( + |theme| container::Appearance { + text_color: Some(Color::TRANSPARENT), + background: Some(Background::Color( + theme.cosmic().accent_color().into(), + )), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: Some(Color::TRANSPARENT), + }, + ))), + ] + .align_items(Alignment::Center), + ) + .into(), + ); + content.push(padded_control(divider::horizontal::default()).into()); + } + + for (key, gpu) in &self.gpus { + if gpu.app_list.is_none() { + continue; + } + + content.push( + menu_button(row![ + text(fl!( + "dgpu-applications", + gpu_name = if self.gpus.len() == 1 { + String::new() + } else { + format!("\"{}\"", gpu.name) + } + )) + .size(14) + .width(Length::Fill) + .height(Length::Fixed(24.0)) + .vertical_alignment(Vertical::Center), + container( + icon::from_name(if gpu.toggled { + "go-down-symbolic" + } else { + "go-up-symbolic" + }) + .size(14) + .symbolic(true) + ) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .width(Length::Fixed(24.0)) + .height(Length::Fixed(24.0)), + ]) + .on_press(Message::ToggleGpuApps(key.clone())) + .into(), + ); + + if gpu.toggled { + let app_list = gpu.app_list.as_ref().unwrap(); + let mut list_apps = Vec::with_capacity(app_list.len()); + for app in app_list { + list_apps.push( + padded_control( + row![ + if let Some(icon) = &app.icon { + container(icon::from_name(&**icon).size(12).symbolic(true)) + } else { + container(horizontal_space(12.0)) + }, + column![text(&app.name).size(14), text(&app.secondary).size(10)] + .width(Length::Fill), + ] + .spacing(8) + .align_items(Alignment::Center), + ) + .into(), + ); + } + content.push( + scrollable(Column::with_children(list_apps)) + .height(Length::Fixed(300.0)) + .into(), + ); + } + content.push(padded_control(divider::horizontal::default()).into()); + } + + content.push( + menu_button(text(fl!("power-settings")).size(14).width(Length::Fill)) + .on_press(Message::OpenSettings) + .into(), + ); + self.core .applet - .popup_container( - column![ - padded_control( - row![ - icon::from_name(&*self.icon_name).size(24).symbolic(true), - column![name, description] - ] - .spacing(8) - .align_items(Alignment::Center) - ), - padded_control(divider::horizontal::default()), - menu_button( - row![ - column![ - text(fl!("battery")).size(14), - text(fl!("battery-desc")).size(10) - ] - .width(Length::Fill), - if matches!(self.power_profile, Power::Battery) { - container( - icon::from_name("emblem-ok-symbolic") - .size(12) - .symbolic(true), - ) - } else { - container(horizontal_space(1.0)) - } - ] - .align_items(Alignment::Center) - ) - .on_press(Message::SelectProfile(Power::Battery)), - menu_button( - row![ - column![ - text(fl!("balanced")).size(14), - text(fl!("balanced-desc")).size(10) - ] - .width(Length::Fill), - if matches!(self.power_profile, Power::Balanced) { - container( - icon::from_name("emblem-ok-symbolic") - .size(12) - .symbolic(true), - ) - } else { - container(horizontal_space(1.0)) - } - ] - .align_items(Alignment::Center) - ) - .on_press(Message::SelectProfile(Power::Balanced)), - menu_button( - row![ - column![ - text(fl!("performance")).size(14), - text(fl!("performance-desc")).size(10) - ] - .width(Length::Fill), - if matches!(self.power_profile, Power::Performance) { - container( - icon::from_name("emblem-ok-symbolic") - .size(12) - .symbolic(true), - ) - } else { - container(horizontal_space(1.0)) - } - ] - .align_items(Alignment::Center) - ) - .on_press(Message::SelectProfile(Power::Performance)), - padded_control(divider::horizontal::default()), - padded_control( - anim!( - //toggler - MAX_CHARGE, - &self.timeline, - fl!("max-charge"), - self.charging_limit, - Message::SetChargingLimit, - ) - .text_size(14) - .width(Length::Fill) - ), - padded_control(divider::horizontal::default()), - padded_control( - row![ - icon::from_name(self.display_icon_name.as_str()) - .size(24) - .symbolic(true), - slider( - 1..=100, - (self.screen_brightness * 100.0) as i32, - Message::SetScreenBrightness - ), - text(format!("{:.0}%", self.screen_brightness * 100.0)) - .size(16) - .width(Length::Fixed(40.0)) - .horizontal_alignment(Horizontal::Right) - ] - .spacing(12) - ), - padded_control( - row![ - icon::from_name("keyboard-brightness-symbolic") - .size(24) - .symbolic(true), - slider( - 0..=100, - (self.kbd_brightness * 100.0) as i32, - Message::SetKbdBrightness - ), - text(format!("{:.0}%", self.kbd_brightness * 100.0)) - .size(16) - .width(Length::Fixed(40.0)) - .horizontal_alignment(Horizontal::Right) - ] - .spacing(12) - ), - padded_control(divider::horizontal::default()), - menu_button(text(fl!("power-settings")).size(14).width(Length::Fill)) - .on_press(Message::OpenSettings) - ] - .padding([8, 0]), - ) + .popup_container(Column::with_children(content).padding([8, 0])) .into() } @@ -500,6 +677,10 @@ impl cosmic::Application for CosmicBatteryApplet { PowerProfileUpdate::Init(tx, p) => Message::InitProfile(p, tx), PowerProfileUpdate::Error(e) => Message::Errored(e), // TODO: handle error }), + dgpu_subscription(0).map(|event| match event { + GpuUpdate::On(path, name, list) => Message::GpuOn(path, name, list), + GpuUpdate::Off(path) => Message::GpuOff(path), + }), self.timeline .as_subscription() .map(|(_, now)| Message::Frame(now)), diff --git a/cosmic-applet-battery/src/dgpu.rs b/cosmic-applet-battery/src/dgpu.rs new file mode 100644 index 00000000..405d5016 --- /dev/null +++ b/cosmic-applet-battery/src/dgpu.rs @@ -0,0 +1,457 @@ +use std::{ + ffi::{OsStr, OsString}, + fmt::{self, Debug}, + hash::Hash, + io, + os::fd::AsRawFd, + path::{Path, PathBuf}, + time::Duration, +}; + +use cosmic::iced::{self, subscription}; +use futures::{FutureExt, SinkExt}; +use tokio::{ + io::unix::AsyncFd, + task::spawn_blocking, + time::{self, Interval}, +}; +use tracing::{debug, info, trace}; +use udev::EventType; + +pub struct GpuMonitor { + primary_gpu: PathBuf, + gpus: Vec, + monitor: AsyncFd, + seat: String, +} + +struct WrappedSocket(udev::MonitorSocket); +impl AsRawFd for WrappedSocket { + fn as_raw_fd(&self) -> std::os::unix::io::RawFd { + self.0.as_raw_fd() + } +} +unsafe impl Send for WrappedSocket {} +unsafe impl Sync for WrappedSocket {} + +impl Debug for GpuMonitor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GpuMonitor") + .field("primary_gpu", &self.primary_gpu) + .field("gpus", &self.gpus) + .field("monitor", &"...") + .field("seat", &self.seat) + .finish() + } +} + +#[derive(Debug)] +struct Gpu { + path: PathBuf, + name: String, + primary: bool, + enabled: bool, + driver: Option, + interval: Interval, +} + +async fn is_desktop() -> bool { + let chassis = tokio::fs::read_to_string("/sys/class/dmi/id/chassis_type") + .await + .unwrap_or_default(); + + chassis.trim() == "3" +} + +async fn powered_on(path: impl AsRef) -> bool { + let Some(component) = path.as_ref().components().last() else { + return true; + }; + let name_str = component.as_os_str(); + let Some(name) = name_str.to_str() else { + return true; + }; + let Ok(state) = + tokio::fs::read_to_string(format!("/sys/class/drm/{}/device/power_state", name)).await + else { + return true; + }; + + match state.trim() { + "D0" => true, + "D3cold" | "D3hot" => false, + x => { + debug!( + "Unknown power state {} for node {}", + x, + path.as_ref().display() + ); + true + } + } +} + +impl GpuMonitor { + async fn new() -> Option { + if is_desktop().await { + info!("Desktop, skipping dGPU code"); + return None; + } + + let seat = std::env::var("XDG_SEAT").unwrap_or_else(|_| String::from("seat0")); + let seat_clone = seat.clone(); + let gpus = spawn_blocking(move || all_gpus(seat)).await.ok()?.ok()?; + + let monitor = AsyncFd::new(WrappedSocket( + udev::MonitorBuilder::new() + .ok()? + .match_subsystem("drm") + .ok()? + .listen() + .ok()?, + )) + .ok()?; + + let primary_gpu = gpus + .iter() + .find_map(|gpu| gpu.primary.then(|| gpu.path.clone()))?; + + Some(GpuMonitor { + primary_gpu, + gpus, + monitor, + seat: seat_clone, + }) + } +} + +#[derive(Debug, Clone)] +pub struct App { + pub name: String, + pub icon: Option, + pub secondary: String, +} + +#[derive(Debug)] +pub struct RunningApp { + name: String, + icon: Option, + executable_name: String, +} + +impl Gpu { + async fn app_list(&self, running_apps: &[RunningApp]) -> Option> { + match self.driver.as_ref().and_then(|s| s.to_str()) { + Some("nvidia") => { + // figure out bus path for calling nvidia-smi + let mut sys_path = PathBuf::from("/sys/class/drm"); + sys_path.push(self.path.components().last()?.as_os_str()); + let buslink = std::fs::read_link(sys_path) + .ok()? + .components() + .rev() + .nth(2)? + .as_os_str() + .to_string_lossy() + .into_owned(); + + let smi_output = match tokio::process::Command::new("nvidia-smi") + .args(["pmon", "--id", &buslink, "--count", "1"]) + .output() + .await + { + Ok(output) if output.status.success() => { + String::from_utf8_lossy(&output.stdout).into_owned() + } + Ok(output) => { + debug!( + "smi returned error code {}: {}", + output.status, + String::from_utf8_lossy(&output.stdout) + ); + return None; + } + Err(err) => { + debug!("smi returned error code: {}", err); + return None; + } + }; + + Some( + smi_output + .lines() + .filter(|line| !line.starts_with('#')) + .map(|line| { + let components = line.split_whitespace().collect::>(); + let pid = components[1].trim(); + let process_name = components.last().unwrap().trim(); + + if let Some(application) = running_apps + .iter() + .find(|running_app| running_app.executable_name == process_name) + { + App { + name: application.name.clone(), + icon: application.icon.clone(), + secondary: String::new(), + } + } else { + App { + name: process_name.to_string(), + icon: None, + secondary: pid.to_string(), + } + } + }) + .collect(), + ) + } + _ => { + let lsof_output = match tokio::process::Command::new("lsof") + .args([OsStr::new("-t"), self.path.as_os_str()]) + .output() + .await + { + Ok(output) => String::from_utf8_lossy(&output.stdout).into_owned(), + Err(err) => { + debug!("lsof returned error code: {}", err); + return None; + } + }; + + Some( + lsof_output + .lines() + .filter_map(|pid| { + let executable = std::fs::read_link(format!("/proc/{}/exe", pid)) + .ok()? + .components() + .last()? + .as_os_str() + .to_string_lossy() + .into_owned(); + + if let Some(application) = running_apps + .iter() + .find(|running_app| running_app.executable_name == executable) + { + Some(App { + name: application.name.clone(), + icon: application.icon.clone(), + secondary: String::new(), + }) + } else { + Some(App { + name: executable, + icon: None, + secondary: pid.to_string(), + }) + } + }) + .collect(), + ) + } + } + } +} + +fn all_gpus>(seat: S) -> io::Result> { + let mut enumerator = udev::Enumerator::new()?; + enumerator.match_subsystem("drm")?; + enumerator.match_sysname("card[0-9]*")?; + Ok(enumerator + .scan_devices()? + .filter(|device| { + device + .property_value("ID_SEAT") + .map(|x| x.to_os_string()) + .unwrap_or_else(|| OsString::from("seat0")) + == *seat.as_ref() + }) + .flat_map(|device| { + let path = device.devnode().map(PathBuf::from)?; + let boot_vga = if let Ok(Some(pci)) = device.parent_with_subsystem(Path::new("pci")) { + if let Some(value) = pci.attribute_value("boot_vga") { + value == "1" + } else { + false + } + } else { + false + }; + + let name = if let Some(parent) = device.parent() { + let vendor = parent + .property_value("SWITCHEROO_CONTROL_VENDOR_NAME") + .or_else(|| parent.property_value("ID_VENDOR_FROM_DATABASE")); + let name = parent + .property_value("SWITCHEROO_CONTROL_PRODUCT_NAME") + .or_else(|| parent.property_value("ID_MODEL_FROM_DATABASE")); + + if vendor.is_none() && name.is_none() { + String::from("Unknown GPU") + } else { + format!( + "{} {}", + vendor.map(|s| s.to_string_lossy()).unwrap_or_default(), + name.map(|s| s.to_string_lossy()).unwrap_or_default() + ) + } + } else { + String::from("Unknown GPU") + }; + + let mut device = Some(device); + let driver = loop { + if let Some(dev) = device { + if dev.driver().is_some() { + break dev.driver().map(std::ffi::OsStr::to_os_string); + } else { + device = dev.parent(); + } + } else { + break None; + } + }; + + let mut interval = time::interval(Duration::from_secs(3)); + interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); + + Some(Gpu { + path, + name, + primary: boot_vga, + enabled: false, + driver, + interval, + }) + }) + .collect()) +} + +pub fn dgpu_subscription( + id: I, +) -> iced::Subscription { + subscription::channel(id, 50, move |mut output| async move { + let mut state = State::Ready; + + loop { + state = start_listening(state, &mut output).await; + } + }) +} + +#[derive(Debug)] +pub enum State { + Ready, + Waiting(GpuMonitor), + Finished, +} + +#[derive(Debug)] +pub enum GpuUpdate { + Off(PathBuf), + On(PathBuf, String, Option>), +} + +async fn start_listening( + state: State, + output: &mut futures::channel::mpsc::Sender, +) -> State { + match state { + State::Ready => match GpuMonitor::new().await { + Some(monitor) => State::Waiting(monitor), + None => State::Finished, + }, + State::Waiting(mut monitor) => { + let select_all = futures::future::select_all( + monitor + .gpus + .iter_mut() + .map(|gpu| Box::pin(gpu.interval.tick())), + ) + .map(|(_, i, _)| i); + + tokio::select! { + _guard = monitor.monitor.readable() => { + for event in monitor.monitor.get_ref().0.iter() { + match event.event_type() { + // New device + EventType::Add => { + if let Some(path) = event.devnode() { + let name = if let Ok(Some(pci)) = event.device().parent_with_subsystem(Path::new("pci")) { + if let Some(value) = pci.attribute_value("ID_MODEL_FROM_DATABASE") { + value.to_string_lossy().into_owned() + } else { + String::from("Unknown") + } + } else { + String::from("Unknown") + }; + + let mut device = Some(event.device()); + let driver = loop { + if let Some(dev) = device { + if dev.driver().is_some() { + break dev.driver().map(std::ffi::OsStr::to_os_string); + } else { + device = dev.parent(); + } + } else { + break None; + } + }; + + let mut interval = time::interval(Duration::from_secs(3)); + interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); + monitor.gpus.push(Gpu { + path: path.to_path_buf(), + name, + primary: false, + enabled: false, + driver, + interval, + }); + } + }, + EventType::Remove => { + if let Some(path) = event.devnode() { + monitor.gpus.retain(|gpu| gpu.path != path); + } + } + _ => {}, + } + } + } + i = select_all => { + let gpu = &mut monitor.gpus[i]; + if gpu.path == monitor.primary_gpu { + return State::Waiting(monitor); + } + + trace!("Polling gpu {}", gpu.path.display()); + let enabled = powered_on(&gpu.path).await; + + if enabled != gpu.enabled { + let mut new_interval = time::interval(Duration::from_secs(if enabled { 30 } else { 3 })); + new_interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); + gpu.interval = new_interval; + gpu.enabled = enabled; + } + + if enabled { + let app_list = gpu.app_list(&[]).await; + if output.send(GpuUpdate::On(gpu.path.clone(), gpu.name.clone(), app_list)).await.is_err() { + return State::Finished; + } + } else if output.send(GpuUpdate::Off(gpu.path.clone())).await.is_err() { + return State::Finished; + } + } + }; + + State::Waiting(monitor) + } + State::Finished => iced::futures::future::pending().await, + } +} diff --git a/cosmic-applet-battery/src/main.rs b/cosmic-applet-battery/src/main.rs index 25e3ad04..f19df1de 100644 --- a/cosmic-applet-battery/src/main.rs +++ b/cosmic-applet-battery/src/main.rs @@ -2,10 +2,10 @@ mod backlight; mod app; mod config; +mod dgpu; mod localize; mod power_daemon; mod upower; - mod upower_device; mod upower_kbdbacklight; use config::APP_ID; diff --git a/debian/control b/debian/control index 99068127..388b59ca 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,7 @@ Build-Depends: libdbus-1-dev, libegl-dev, libpulse-dev, + libudev-dev, libxkbcommon-dev, libwayland-dev, just,