diff --git a/LICENSE b/LICENSE index 658021b..0cf8237 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ Copyright © 2023 Collabora Ltd. Copyright © 2024 Valve Software +Copyright © 2024 Igalia S.L. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in diff --git a/bin/steamos-disable-wifidebug b/bin/steamos-disable-wifidebug index 0ef839b..a7fed70 100644 --- a/bin/steamos-disable-wifidebug +++ b/bin/steamos-disable-wifidebug @@ -5,5 +5,5 @@ # buffer_size: the size in kilobytes to use for the trace (if on galileo device). # Use 0 for buffer_size since it's only used when mode is 1 -busctl call com.steampowered.SteamOSManager1 /com/steampowered/SteamOSManager1 com.steampowered.SteamOSManager1 SetWifiDebugMode uu 0 0 +busctl call com.steampowered.SteamOSManager1.Manager /com/steampowered/SteamOSManager1 com.steampowered.SteamOSManager1.Manager SetWifiDebugMode uu 0 0 diff --git a/bin/steamos-enable-wifidebug b/bin/steamos-enable-wifidebug index d65b538..c415866 100644 --- a/bin/steamos-enable-wifidebug +++ b/bin/steamos-enable-wifidebug @@ -4,5 +4,5 @@ # mode: 1 for on, 0 for off # buffer_size: the size in kilobytes to use for the trace (if on galileo device). -busctl call com.steampowered.SteamOSManager1 /com/steampowered/SteamOSManager1 com.steampowered.SteamOSManager1 SetWifiDebugMode uu 1 20000 +busctl call com.steampowered.SteamOSManager1.Manager /com/steampowered/SteamOSManager1 com.steampowered.SteamOSManager1.Manager SetWifiDebugMode uu 1 20000 diff --git a/bin/steamos-get-wifidebug b/bin/steamos-get-wifidebug index 0d51abf..72e0c7d 100644 --- a/bin/steamos-get-wifidebug +++ b/bin/steamos-get-wifidebug @@ -1,5 +1,5 @@ #!/bin/bash # Get wifi debugmode from steamos-manager -busctl call com.steampowered.SteamOSManager1 /com/steampowered/SteamOSManager1 com.steampowered.SteamOSManager1 GetWifiDebugMode +busctl call com.steampowered.SteamOSManager1.Manager /com/steampowered/SteamOSManager1 com.steampowered.SteamOSManager1.Manager GetWifiDebugMode diff --git a/com.steampowered.SteamOSManager1.xml b/com.steampowered.SteamOSManager1.xml index 58bfac0..395ff6c 100644 --- a/com.steampowered.SteamOSManager1.xml +++ b/com.steampowered.SteamOSManager1.xml @@ -47,7 +47,7 @@ Controls the wifi chip's power management features. - Valid states: 0 = Unsupported Feature, 1 = Disabled, 2 = Enabled + Valid states: 0 = Disabled, 1 = Enabled Version available: 7 --> @@ -58,7 +58,7 @@ Controls whether the OS or the BIOS should manage fan speed - Valid states: 0 = Unsupported Feature, 1 = BIOS, 2 = OS + Valid states: 0 = BIOS, 1 = OS Version available: 7 --> @@ -69,7 +69,7 @@ Reports whether the current hardware is supported or not. - Valid states: 0 = Unsupported Feature, 1 = Unsupported, 2 = Supported + Valid states: 0 = Unsupported, 1 = Supported Version available: 7 --> @@ -100,17 +100,6 @@ - - - - - - - - - - - - - - - - @@ -238,7 +190,7 @@ Controls the system TDP limit. - Valid states: 0 = Feature Unsupported, or in range of [ TDPLimitMin, TDPLimitMax ] + Valid states: In range of [ TDPLimitMin, TDPLimitMax ] Version available: 7 --> @@ -268,7 +220,7 @@ Whether wifi debug mode is currently enabled. - Valid states: 0 = Feature Unsupported, 1 = Disabled, 2 = Enabled + Valid states: 0 = Disabled, 1 = Enabled Version available: 7 --> @@ -289,27 +241,12 @@ - - - - - diff --git a/data/com.steampowered.SteamOSManager1.conf b/data/com.steampowered.SteamOSManager1.conf index d5e101f..52cf89c 100644 --- a/data/com.steampowered.SteamOSManager1.conf +++ b/data/com.steampowered.SteamOSManager1.conf @@ -9,8 +9,8 @@ - + - + diff --git a/data/steamos-manager.service b/data/steamos-manager.service index 1675fc8..c28393b 100644 --- a/data/steamos-manager.service +++ b/data/steamos-manager.service @@ -5,7 +5,7 @@ After=steamos-log-submitter.service [Service] Type=dbus -BusName=com.steampowered.SteamOSManager1 +BusName=com.steampowered.SteamOSManager1.Manager Environment=RUST_LOG='INFO' ExecStart=/usr/lib/steamos-manager Restart=on-failure diff --git a/src/ds_inhibit.rs b/src/ds_inhibit.rs index b7fb5cb..fd7aed2 100644 --- a/src/ds_inhibit.rs +++ b/src/ds_inhibit.rs @@ -302,14 +302,14 @@ mod test { use tokio::time::sleep; async fn nyield(times: u32) { - for i in 0..times { + for _ in 0..times { sleep(Duration::from_millis(1)).await; } } #[tokio::test] async fn hid_nodes() { - let h = testing::start(); + let _h = testing::start(); let hid = HidNode::new(0); let sys_base = hid.sys_base(); @@ -326,7 +326,7 @@ mod test { #[tokio::test] async fn hid_can_inhibit() { - let h = testing::start(); + let _h = testing::start(); let hids = [ HidNode::new(0), @@ -363,7 +363,7 @@ mod test { #[tokio::test] async fn hid_inhibit() { - let h = testing::start(); + let _h = testing::start(); let hid = HidNode::new(0); let sys_base = hid.sys_base(); @@ -373,12 +373,12 @@ mod test { assert!(hid.can_inhibit().await); - hid.inhibit().await; + hid.inhibit().await.expect("inhibit"); assert_eq!( read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), "1\n" ); - hid.uninhibit().await; + hid.uninhibit().await.expect("uninhibit"); assert_eq!( read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), "0\n" @@ -387,7 +387,7 @@ mod test { #[tokio::test] async fn hid_inhibit_error_continue() { - let h = testing::start(); + let _h = testing::start(); let hid = HidNode::new(0); let sys_base = hid.sys_base(); @@ -399,12 +399,12 @@ mod test { assert!(hid.can_inhibit().await); - hid.inhibit().await; + assert!(hid.inhibit().await.is_err()); assert_eq!( read_to_string(sys_base.join("input/input1/inhibited")).expect("inhibited"), "1\n" ); - hid.uninhibit().await; + assert!(hid.uninhibit().await.is_err()); assert_eq!( read_to_string(sys_base.join("input/input1/inhibited")).expect("inhibited"), "0\n" @@ -496,7 +496,7 @@ mod test { let mut inhibitor = Inhibitor::init().await.expect("init"); let task = tokio::spawn(async move { - inhibitor.run().await; + inhibitor.run().await.expect("run"); }); nyield(1).await; @@ -540,7 +540,7 @@ mod test { let mut inhibitor = Inhibitor::init().await.expect("init"); let task = tokio::spawn(async move { - inhibitor.run().await; + inhibitor.run().await.expect("run"); }); nyield(1).await; @@ -548,7 +548,7 @@ mod test { File::create(hid.hidraw()).expect("hidraw"); symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink"); - let f = File::open(hid.hidraw()).expect("hidraw"); + let _f = File::open(hid.hidraw()).expect("hidraw"); nyield(4).await; assert_eq!( read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), @@ -560,21 +560,20 @@ mod test { #[tokio::test] async fn inhibitor_create() { - let h = testing::start(); - let path = h.test.path(); + let _h = testing::start(); let hid = HidNode::new(0); let sys_base = hid.sys_base(); - create_dir_all(path.join("dev")).expect("dev"); + create_dir_all(path("/dev")).expect("dev"); create_dir_all(sys_base.join("input/input0/mouse0")).expect("mouse0"); symlink("sony", sys_base.join("driver")).expect("driver"); - create_dir_all(path.join("proc/1/fd")).expect("fd"); - write(path.join("proc/1/comm"), "steam\n").expect("comm"); + create_dir_all(path("/proc/1/fd")).expect("fd"); + write(path("/proc/1/comm"), "steam\n").expect("comm"); let mut inhibitor = Inhibitor::init().await.expect("init"); let task = tokio::spawn(async move { - inhibitor.run().await; + inhibitor.run().await.expect("run"); }); nyield(3).await; @@ -582,8 +581,8 @@ mod test { File::create(hid.hidraw()).expect("hidraw"); nyield(3).await; - symlink(hid.hidraw(), path.join("proc/1/fd/3")).expect("symlink"); - let f = File::open(hid.hidraw()).expect("hidraw"); + symlink(hid.hidraw(), path("/proc/1/fd/3")).expect("symlink"); + let _f = File::open(hid.hidraw()).expect("hidraw"); nyield(3).await; assert_eq!( read_to_string(sys_base.join("input/input0/inhibited")).expect("inhibited"), diff --git a/src/hardware.rs b/src/hardware.rs index 5c19b06..8f5113e 100644 --- a/src/hardware.rs +++ b/src/hardware.rs @@ -6,10 +6,12 @@ */ use anyhow::{Error, Result}; +use std::fmt; use std::str::FromStr; use tokio::fs; use crate::path; +use crate::process::script_exit_code; const BOARD_VENDOR_PATH: &str = "/sys/class/dmi/id/board_vendor"; const BOARD_NAME_PATH: &str = "/sys/class/dmi/id/board_name"; @@ -21,6 +23,13 @@ pub enum HardwareVariant { Galileo, } +#[derive(PartialEq, Debug, Copy, Clone)] +#[repr(u32)] +pub enum HardwareCurrentlySupported { + Unsupported = 0, + Supported = 1, +} + impl FromStr for HardwareVariant { type Err = Error; fn from_str(input: &str) -> Result { @@ -32,6 +41,30 @@ impl FromStr for HardwareVariant { } } +impl TryFrom for HardwareCurrentlySupported { + type Error = &'static str; + fn try_from(v: u32) -> Result { + match v { + x if x == HardwareCurrentlySupported::Unsupported as u32 => { + Ok(HardwareCurrentlySupported::Unsupported) + } + x if x == HardwareCurrentlySupported::Supported as u32 => { + Ok(HardwareCurrentlySupported::Supported) + } + _ => Err("No enum match for value {v}"), + } + } +} + +impl fmt::Display for HardwareCurrentlySupported { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + HardwareCurrentlySupported::Unsupported => write!(f, "Unsupported"), + HardwareCurrentlySupported::Supported => write!(f, "Supported"), + } + } +} + pub async fn variant() -> Result { let board_vendor = fs::read_to_string(path(BOARD_VENDOR_PATH)).await?; if board_vendor.trim_end() != "Valve" { @@ -42,6 +75,17 @@ pub async fn variant() -> Result { HardwareVariant::from_str(board_name.trim_end()) } +pub async fn check_support() -> Result { + // Run jupiter-check-support note this script does exit 1 for "Support: No" case + // so no need to parse output, etc. + let res = script_exit_code("/usr/bin/jupiter-check-support", &[] as &[String; 0]).await?; + + Ok(match res { + 0 => HardwareCurrentlySupported::Supported, + _ => HardwareCurrentlySupported::Unsupported, + }) +} + #[cfg(test)] mod test { use super::*; @@ -50,7 +94,7 @@ mod test { #[tokio::test] async fn board_lookup() { - let h = testing::start(); + let _h = testing::start(); create_dir_all(crate::path("/sys/class/dmi/id")) .await diff --git a/src/main.rs b/src/main.rs index 12242eb..47219ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ mod manager; mod power; mod process; mod sls; +mod systemd; mod wifi; #[cfg(test)] @@ -123,15 +124,25 @@ async fn reload() -> Result<()> { } } -async fn create_connection() -> Result { - let manager = manager::SMManager::new().await?; +pub fn anyhow_to_zbus(error: Error) -> zbus::Error { + zbus::Error::Failure(error.to_string()) +} - ConnectionBuilder::system()? - .name("com.steampowered.SteamOSManager1")? - .serve_at("/com/steampowered/SteamOSManager1", manager)? +pub fn anyhow_to_zbus_fdo(error: Error) -> zbus::fdo::Error { + zbus::fdo::Error::Failed(error.to_string()) +} + +async fn create_connection() -> Result { + let connection = ConnectionBuilder::system()? + .name("com.steampowered.SteamOSManager1.Manager")? .build() - .await - .map_err(|e| e.into()) + .await?; + let manager = manager::SteamOSManager::new(connection.clone()).await?; + connection + .object_server() + .at("/com/steampowered/SteamOSManager1", manager) + .await?; + Ok(connection) } #[tokio::main] diff --git a/src/manager.rs b/src/manager.rs index a4664f5..dafb038 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -1,104 +1,176 @@ /* * Copyright © 2023 Collabora Ltd. * Copyright © 2024 Valve Software + * Copyright © 2024 Igalia S.L. * * SPDX-License-Identifier: MIT */ use anyhow::Result; +use std::fmt; use tokio::fs::File; -use tracing::{error, warn}; -use zbus::{interface, zvariant::Fd}; +use tracing::error; +use zbus::zvariant::Fd; +use zbus::{interface, Connection, SignalContext}; -use crate::hardware::{variant, HardwareVariant}; -use crate::power::{set_gpu_clocks, set_gpu_performance_level, set_tdp_limit}; -use crate::process::{run_script, script_output, SYSTEMCTL_PATH}; -use crate::wifi::{restart_iwd, setup_iwd_config, start_tracing, stop_tracing, WifiDebugMode}; +use crate::hardware::{check_support, variant, HardwareVariant}; +use crate::power::{ + get_gpu_clocks, get_gpu_performance_level, get_tdp_limit, set_gpu_clocks, + set_gpu_performance_level, set_tdp_limit, GPUPerformanceLevel, +}; +use crate::process::{run_script, script_output}; +use crate::systemd::SystemdUnit; +use crate::wifi::{ + get_wifi_backend, set_wifi_backend, set_wifi_debug_mode, WifiBackend, WifiDebugMode, + WifiPowerManagement, +}; +use crate::{anyhow_to_zbus, anyhow_to_zbus_fdo}; -pub struct SMManager { +#[derive(PartialEq, Debug, Copy, Clone)] +#[repr(u32)] +enum PrepareFactoryReset { + Unknown = 0, + RebootRequired = 1, +} + +#[derive(PartialEq, Debug, Copy, Clone)] +#[repr(u32)] +enum FanControl { + BIOS = 0, + OS = 1, +} + +impl TryFrom for FanControl { + type Error = &'static str; + fn try_from(v: u32) -> Result { + match v { + x if x == FanControl::BIOS as u32 => Ok(FanControl::BIOS), + x if x == FanControl::OS as u32 => Ok(FanControl::BIOS), + _ => Err("No enum match for value {v}"), + } + } +} + +impl fmt::Display for FanControl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FanControl::BIOS => write!(f, "BIOS"), + FanControl::OS => write!(f, "OS"), + } + } +} + +pub struct SteamOSManager { + connection: Connection, wifi_debug_mode: WifiDebugMode, // Whether we should use trace-cmd or not. // True on galileo devices, false otherwise should_trace: bool, } -impl SMManager { - pub async fn new() -> Result { - Ok(SMManager { +impl SteamOSManager { + pub async fn new(connection: Connection) -> Result { + Ok(SteamOSManager { + connection, wifi_debug_mode: WifiDebugMode::Off, should_trace: variant().await? == HardwareVariant::Galileo, }) } } -const MIN_BUFFER_SIZE: u32 = 100; - const ALS_INTEGRATION_PATH: &str = "/sys/devices/platform/AMDI0010:00/i2c-0/i2c-PRP0001:01/iio:device0/in_illuminance_integration_time"; -#[interface(name = "com.steampowered.SteamOSManager1")] -impl SMManager { - const API_VERSION: u32 = 1; +#[interface(name = "com.steampowered.SteamOSManager1.Manager")] +impl SteamOSManager { + const API_VERSION: u32 = 7; - async fn say_hello(&self, name: &str) -> String { - format!("Hello {}!", name) - } - - async fn factory_reset(&self) -> bool { + async fn prepare_factory_reset(&self) -> u32 { // Run steamos factory reset script and return true on success - run_script( - "factory reset", - "/usr/bin/steamos-factory-reset-config", - &[""], - ) - .await - .unwrap_or(false) - } - - async fn disable_wifi_power_management(&self) -> bool { - // Run polkit helper script and return true on success - run_script( - "disable wifi power management", - "/usr/bin/steamos-polkit-helpers/steamos-disable-wireless-power-management", - &[""], - ) - .await - .unwrap_or(false) - } - - async fn enable_fan_control(&self, enable: bool) -> bool { - // Run what steamos-polkit-helpers/jupiter-fan-control does - if enable { - run_script( - "enable fan control", - SYSTEMCTL_PATH, - &["start", "jupiter-fan-control-service"], - ) - .await - .unwrap_or(false) - } else { - run_script( - "disable fan control", - SYSTEMCTL_PATH, - &["stop", "jupiter-fan-control.service"], - ) - .await - .unwrap_or(false) + let res = run_script("/usr/bin/steamos-factory-reset-config", &[""]).await; + match res { + Ok(_) => PrepareFactoryReset::RebootRequired as u32, + Err(_) => PrepareFactoryReset::Unknown as u32, } } - async fn hardware_check_support(&self) -> bool { - // Run jupiter-check-support note this script does exit 1 for "Support: No" case - // so no need to parse output, etc. - run_script( - "check hardware support", - "/usr/bin/jupiter-check-support", - &[""], - ) - .await - .unwrap_or(false) + #[zbus(property(emits_changed_signal = "false"))] + async fn wifi_power_management_state(&self) -> zbus::fdo::Result { + let output = script_output("/usr/bin/iwconfig", &["wlan0"]) + .await + .map_err(anyhow_to_zbus_fdo)?; + for line in output.lines() { + return Ok(match line.trim() { + "Power Management:on" => WifiPowerManagement::Enabled as u32, + "Power Management:off" => WifiPowerManagement::Disabled as u32, + _ => continue, + }); + } + Err(zbus::fdo::Error::Failed(String::from( + "Failed to query power management state", + ))) } - async fn read_als_calibration(&self) -> f32 { + #[zbus(property)] + async fn set_wifi_power_management_state(&self, state: u32) -> zbus::Result<()> { + let state = match WifiPowerManagement::try_from(state) { + Ok(state) => state, + Err(err) => return Err(zbus::fdo::Error::InvalidArgs(err.to_string()).into()), + }; + let state = match state { + WifiPowerManagement::Disabled => "off", + WifiPowerManagement::Enabled => "on", + }; + + run_script("/usr/bin/iwconfig", &["wlan0", "power", state]) + .await + .inspect_err(|message| error!("Error setting wifi power management state: {message}")) + .map_err(anyhow_to_zbus) + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn fan_control_state(&self) -> zbus::fdo::Result { + let jupiter_fan_control = + SystemdUnit::new(self.connection.clone(), "jupiter_2dfan_2dcontrol_2eservice") + .await + .map_err(anyhow_to_zbus_fdo)?; + let active = jupiter_fan_control + .active() + .await + .map_err(anyhow_to_zbus_fdo)?; + Ok(match active { + true => FanControl::OS as u32, + false => FanControl::BIOS as u32, + }) + } + + #[zbus(property)] + async fn set_fan_control_state(&self, state: u32) -> zbus::Result<()> { + let state = match FanControl::try_from(state) { + Ok(state) => state, + Err(err) => return Err(zbus::fdo::Error::InvalidArgs(err.to_string()).into()), + }; + // Run what steamos-polkit-helpers/jupiter-fan-control does + let jupiter_fan_control = + SystemdUnit::new(self.connection.clone(), "jupiter_2dfan_2dcontrol_2eservice") + .await + .map_err(anyhow_to_zbus)?; + match state { + FanControl::OS => jupiter_fan_control.start().await, + FanControl::BIOS => jupiter_fan_control.stop().await, + } + .map_err(anyhow_to_zbus) + } + + #[zbus(property(emits_changed_signal = "const"))] + async fn hardware_currently_supported(&self) -> zbus::fdo::Result { + match check_support().await { + Ok(res) => Ok(res as u32), + Err(e) => Err(anyhow_to_zbus_fdo(e)), + } + } + + #[zbus(property(emits_changed_signal = "false"))] + async fn als_calibration_gain(&self) -> f64 { // Run script to get calibration value let result = script_output( "/usr/bin/steamos-polkit-helpers/jupiter-get-als-gain", @@ -114,67 +186,7 @@ impl SMManager { } } - async fn update_bios(&self) -> bool { - // Update the bios as needed - // Return true if the script was successful (though that might mean no update was needed), false otherwise - run_script( - "update bios", - "/usr/bin/steamos-potlkit-helpers/jupiter-biosupdate", - &["--auto"], - ) - .await - .unwrap_or(false) - } - - async fn update_dock(&self) -> bool { - // Update the dock firmware as needed - // Retur true if successful, false otherwise - run_script( - "update dock firmware", - "/usr/bin/steamos-polkit-helpers/jupiter-dock-updater", - &[""], - ) - .await - .unwrap_or(false) - } - - async fn trim_devices(&self) -> bool { - // Run steamos-trim-devices script - // return true on success, false otherwise - run_script( - "trim devices", - "/usr/bin/steamos-polkit-helpers/steamos-trim-devices", - &[""], - ) - .await - .unwrap_or(false) - } - - async fn format_sdcard(&self) -> bool { - // Run steamos-format-sdcard script - // return true on success, false otherwise - run_script( - "format sdcard", - "/usr/bin/steamos-polkit-helpers/steamos-format-sdcard", - &[""], - ) - .await - .unwrap_or(false) - } - - async fn set_gpu_performance_level(&self, level: i32) -> bool { - set_gpu_performance_level(level).await.is_ok() - } - - async fn set_gpu_clocks(&self, clocks: i32) -> bool { - set_gpu_clocks(clocks).await.is_ok() - } - - async fn set_tdp_limit(&self, limit: i32) -> bool { - set_tdp_limit(limit).await.is_ok() - } - - async fn get_als_integration_time_file_descriptor(&self) -> Result { + async fn get_als_integration_time_file_descriptor(&self) -> zbus::fdo::Result { // Get the file descriptor for the als integration time sysfs path let result = File::create(ALS_INTEGRATION_PATH).await; match result { @@ -186,109 +198,348 @@ impl SMManager { } } - async fn get_wifi_debug_mode(&mut self) -> u32 { + async fn update_bios(&self) -> zbus::fdo::Result<()> { + // Update the bios as needed + run_script( + "/usr/bin/steamos-potlkit-helpers/jupiter-biosupdate", + &["--auto"], + ) + .await + .inspect_err(|message| error!("Error updating BIOS: {message}")) + .map_err(anyhow_to_zbus_fdo) + } + + async fn update_dock(&self) -> zbus::fdo::Result<()> { + // Update the dock firmware as needed + run_script( + "/usr/bin/steamos-polkit-helpers/jupiter-dock-updater", + &[""], + ) + .await + .inspect_err(|message| error!("Error updating dock: {message}")) + .map_err(anyhow_to_zbus_fdo) + } + + async fn trim_devices(&self) -> zbus::fdo::Result<()> { + // Run steamos-trim-devices script + run_script( + "/usr/bin/steamos-polkit-helpers/steamos-trim-devices", + &[""], + ) + .await + .inspect_err(|message| error!("Error updating trimming devices: {message}")) + .map_err(anyhow_to_zbus_fdo) + } + + async fn format_device( + &self, + device: &str, + label: &str, + validate: bool, + ) -> zbus::fdo::Result<()> { + let mut args = vec!["--label", label, "--device", device]; + if !validate { + args.push("--skip-validation"); + } + run_script("/usr/lib/hwsupport/format-device.sh", args.as_ref()) + .await + .inspect_err(|message| error!("Error formatting {device}: {message}")) + .map_err(anyhow_to_zbus_fdo) + } + + #[zbus(property(emits_changed_signal = "false"), name = "GPUPerformanceLevel")] + async fn gpu_performance_level(&self) -> zbus::fdo::Result { + match get_gpu_performance_level().await { + Ok(level) => Ok(level as u32), + Err(e) => { + error!("Error getting GPU performance level: {e}"); + Err(anyhow_to_zbus_fdo(e)) + } + } + } + + #[zbus(property, name = "GPUPerformanceLevel")] + async fn set_gpu_performance_level(&self, level: u32) -> zbus::Result<()> { + let level = match GPUPerformanceLevel::try_from(level) { + Ok(level) => level, + Err(e) => return Err(zbus::Error::Failure(e.to_string())), + }; + set_gpu_performance_level(level) + .await + .inspect_err(|message| error!("Error setting GPU performance level: {message}")) + .map_err(anyhow_to_zbus) + } + + #[zbus(property(emits_changed_signal = "false"), name = "ManualGPUClock")] + async fn manual_gpu_clock(&self) -> zbus::fdo::Result { + get_gpu_clocks() + .await + .inspect_err(|message| error!("Error getting manual GPU clock: {message}")) + .map_err(anyhow_to_zbus_fdo) + } + + #[zbus(property, name = "ManualGPUClock")] + async fn set_manual_gpu_clock(&self, clocks: u32) -> zbus::Result<()> { + set_gpu_clocks(clocks) + .await + .inspect_err(|message| error!("Error setting manual GPU clock: {message}")) + .map_err(anyhow_to_zbus) + } + + #[zbus(property(emits_changed_signal = "const"), name = "ManualGPUClockMin")] + async fn manual_gpu_clock_min(&self) -> u32 { + // TODO: Can this be queried from somewhere? + 200 + } + + #[zbus(property(emits_changed_signal = "const"), name = "ManualGPUClockMax")] + async fn manual_gpu_clock_max(&self) -> u32 { + // TODO: Can this be queried from somewhere? + 1600 + } + + #[zbus(property(emits_changed_signal = "false"), name = "TDPLimit")] + async fn tdp_limit(&self) -> zbus::fdo::Result { + get_tdp_limit().await.map_err(anyhow_to_zbus_fdo) + } + + #[zbus(property, name = "TDPLimit")] + async fn set_tdp_limit(&self, limit: u32) -> zbus::Result<()> { + set_tdp_limit(limit).await.map_err(anyhow_to_zbus) + } + + #[zbus(property(emits_changed_signal = "const"), name = "TDPLimitMin")] + async fn tdp_limit_min(&self) -> u32 { + // TODO: Can this be queried from somewhere? + 3 + } + + #[zbus(property(emits_changed_signal = "const"), name = "TDPLimitMax")] + async fn tdp_limit_max(&self) -> u32 { + // TODO: Can this be queried from somewhere? + 15 + } + + #[zbus(property)] + async fn wifi_debug_mode_state(&self) -> u32 { // Get the wifi debug mode self.wifi_debug_mode as u32 } - async fn set_wifi_debug_mode(&mut self, mode: u32, buffer_size: u32) -> bool { + async fn set_wifi_debug_mode( + &mut self, + mode: u32, + buffer_size: u32, + #[zbus(signal_context)] ctx: SignalContext<'_>, + ) -> zbus::fdo::Result<()> { // Set the wifi debug mode to mode, using an int for flexibility going forward but only // doing things on 0 or 1 for now // Return false on error - - let wanted_mode = WifiDebugMode::try_from(mode); - match wanted_mode { - Ok(WifiDebugMode::Off) => { - // If mode is 0 disable wifi debug mode - // Stop any existing trace and flush to disk. - if self.should_trace { - let result = match stop_tracing().await { - Ok(result) => result, - Err(message) => { - error!("stop_tracing command got an error: {message}"); - return false; - } - }; - if !result { - error!("stop_tracing command returned non-zero"); - return false; - } - } - // Stop_tracing was successful - if let Err(message) = setup_iwd_config(false).await { - error!("setup_iwd_config false got an error: {message}"); - return false; - } - // setup_iwd_config false worked - let value = match restart_iwd().await { - Ok(value) => value, - Err(message) => { - error!("restart_iwd got an error: {message}"); - return false; - } - }; - if value { - // restart iwd worked - self.wifi_debug_mode = WifiDebugMode::Off; - } else { - // restart_iwd failed - error!("restart_iwd failed, check log above"); - return false; - } - } - Ok(WifiDebugMode::On) => { - // If mode is 1 enable wifi debug mode - if buffer_size < MIN_BUFFER_SIZE { - return false; - } - - if let Err(message) = setup_iwd_config(true).await { - error!("setup_iwd_config true got an error: {message}"); - return false; - } - // setup_iwd_config worked - let value = match restart_iwd().await { - Ok(value) => value, - Err(message) => { - error!("restart_iwd got an error: {message}"); - return false; - } - }; - if !value { - error!("restart_iwd failed"); - return false; - } - // restart_iwd worked - if self.should_trace { - let value = match start_tracing(buffer_size).await { - Ok(value) => value, - Err(message) => { - error!("start_tracing got an error: {message}"); - return false; - } - }; - if !value { - // start_tracing failed - error!("start_tracing failed"); - return false; - } - } - // start_tracing worked - self.wifi_debug_mode = WifiDebugMode::On; - } - Err(_) => { - // Invalid mode requested, more coming later, but add this catch-all for now - warn!("Invalid wifi debug mode {mode} requested"); - return false; + match get_wifi_backend().await { + Ok(WifiBackend::IWD) => (), + Ok(backend) => { + return Err(zbus::fdo::Error::Failed(format!( + "Setting wifi debug mode not supported when backend is {backend}", + ))); } + Err(e) => return Err(anyhow_to_zbus_fdo(e)), } - true + let wanted_mode = match WifiDebugMode::try_from(mode) { + Ok(mode) => mode, + Err(e) => return Err(zbus::fdo::Error::InvalidArgs(e.to_string())), + }; + match set_wifi_debug_mode( + wanted_mode, + buffer_size, + self.should_trace, + self.connection.clone(), + ) + .await + { + Ok(()) => { + self.wifi_debug_mode = wanted_mode; + self.wifi_debug_mode_state_changed(&ctx).await?; + Ok(()) + } + Err(e) => { + error!("Error setting wifi debug mode: {e}"); + Err(anyhow_to_zbus_fdo(e)) + } + } + } + + /// WifiBackend property. + #[zbus(property(emits_changed_signal = "false"))] + async fn wifi_backend(&self) -> zbus::fdo::Result { + match get_wifi_backend().await { + Ok(backend) => Ok(backend as u32), + Err(e) => Err(anyhow_to_zbus_fdo(e)), + } + } + + #[zbus(property)] + async fn set_wifi_backend(&mut self, backend: u32) -> zbus::fdo::Result<()> { + if self.wifi_debug_mode == WifiDebugMode::On { + return Err(zbus::fdo::Error::Failed(String::from( + "operation not supported when wifi_debug_mode=on", + ))); + } + let backend = match WifiBackend::try_from(backend) { + Ok(backend) => backend, + Err(e) => return Err(zbus::fdo::Error::InvalidArgs(e.to_string())), + }; + set_wifi_backend(backend) + .await + .inspect_err(|message| error!("Error setting wifi backend: {message}")) + .map_err(anyhow_to_zbus_fdo) } /// A version property. - #[zbus(property)] + #[zbus(property(emits_changed_signal = "const"))] async fn version(&self) -> u32 { - SMManager::API_VERSION + SteamOSManager::API_VERSION + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{power, testing}; + use tokio::fs::{create_dir_all, write}; + use zbus::connection::Connection; + use zbus::ConnectionBuilder; + + struct TestHandle { + _handle: testing::TestHandle, + connection: Connection, + } + + async fn start(name: &str) -> TestHandle { + let handle = testing::start(); + create_dir_all(crate::path("/sys/class/dmi/id")) + .await + .expect("create_dir_all"); + write(crate::path("/sys/class/dmi/id/board_vendor"), "Valve\n") + .await + .expect("write"); + write(crate::path("/sys/class/dmi/id/board_name"), "Jupiter\n") + .await + .expect("write"); + create_dir_all(crate::path("/etc/NetworkManager/conf.d")) + .await + .expect("create_dir_all"); + write( + crate::path("/etc/NetworkManager/conf.d/wifi_backend.conf"), + "wifi.backend=iwd\n", + ) + .await + .expect("write"); + + let connection = ConnectionBuilder::session() + .unwrap() + .name(format!("com.steampowered.SteamOSManager1.Test.{name}")) + .unwrap() + .build() + .await + .unwrap(); + let manager = SteamOSManager::new(connection.clone()).await.unwrap(); + connection + .object_server() + .at("/com/steampowered/SteamOSManager1", manager) + .await + .expect("object_server at"); + + TestHandle { + _handle: handle, + connection, + } + } + + #[zbus::proxy( + interface = "com.steampowered.SteamOSManager1.Manager", + default_service = "com.steampowered.SteamOSManager1.Test.GPUPerformanceLevel", + default_path = "/com/steampowered/SteamOSManager1" + )] + trait GPUPerformanceLevel { + #[zbus(property, name = "GPUPerformanceLevel")] + fn gpu_performance_level(&self) -> zbus::Result; + + #[zbus(property, name = "GPUPerformanceLevel")] + fn set_gpu_performance_level(&self, level: u32) -> zbus::Result<()>; + } + + #[tokio::test] + async fn gpu_performance_level() { + let test = start("GPUPerformanceLevel").await; + power::test::setup().await; + + let proxy = GPUPerformanceLevelProxy::new(&test.connection) + .await + .unwrap(); + set_gpu_performance_level(GPUPerformanceLevel::Auto) + .await + .expect("set"); + assert_eq!( + proxy.gpu_performance_level().await.unwrap(), + GPUPerformanceLevel::Auto as u32 + ); + + proxy + .set_gpu_performance_level(GPUPerformanceLevel::Low as u32) + .await + .expect("proxy_set"); + assert_eq!( + get_gpu_performance_level().await.unwrap(), + GPUPerformanceLevel::Low + ); + } + + #[zbus::proxy( + interface = "com.steampowered.SteamOSManager1.Manager", + default_service = "com.steampowered.SteamOSManager1.Test.ManualGPUClock", + default_path = "/com/steampowered/SteamOSManager1" + )] + trait ManualGPUClock { + #[zbus(property, name = "ManualGPUClock")] + fn manual_gpu_clock(&self) -> zbus::Result; + + #[zbus(property, name = "ManualGPUClock")] + fn set_manual_gpu_clock(&self, clocks: u32) -> zbus::Result<()>; + } + + #[tokio::test] + async fn manual_gpu_clock() { + let test = start("ManualGPUClock").await; + + let proxy = ManualGPUClockProxy::new(&test.connection).await.unwrap(); + + assert!(proxy.manual_gpu_clock().await.is_err()); + + power::test::write_clocks(1600).await; + assert_eq!(proxy.manual_gpu_clock().await.unwrap(), 1600); + + proxy.set_manual_gpu_clock(200).await.expect("proxy_set"); + power::test::expect_clocks(200).await; + + assert!(proxy.set_manual_gpu_clock(100).await.is_err()); + power::test::expect_clocks(200).await; + } + + #[zbus::proxy( + interface = "com.steampowered.SteamOSManager1.Manager", + default_service = "com.steampowered.SteamOSManager1.Test.Version", + default_path = "/com/steampowered/SteamOSManager1" + )] + trait Version { + #[zbus(property)] + fn version(&self) -> zbus::Result; + } + + #[tokio::test] + async fn version() { + let test = start("Version").await; + let proxy = VersionProxy::new(&test.connection).await.unwrap(); + assert_eq!(proxy.version().await, Ok(SteamOSManager::API_VERSION)); } } diff --git a/src/power.rs b/src/power.rs index 363a00c..b3b5aa3 100644 --- a/src/power.rs +++ b/src/power.rs @@ -5,9 +5,11 @@ * SPDX-License-Identifier: MIT */ -use anyhow::{bail, ensure, Result}; +use anyhow::{anyhow, bail, ensure, Error, Result}; +use std::path::PathBuf; +use std::str::FromStr; use tokio::fs::{self, File}; -use tokio::io::AsyncWriteExt; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tracing::error; use crate::path; @@ -18,33 +20,89 @@ const GPU_PERFORMANCE_LEVEL_PATH: &str = "/sys/class/drm/card0/device/power_dpm_force_performance_level"; const GPU_CLOCKS_PATH: &str = "/sys/class/drm/card0/device/pp_od_clk_voltage"; -pub async fn set_gpu_performance_level(level: i32) -> Result<()> { - // Set given GPU performance level - // Levels are defined below - // return true if able to write, false otherwise or if level is out of range, etc. - let levels = ["auto", "low", "high", "manual", "peak_performance"]; - ensure!( - level >= 0 && level < levels.len() as i32, - "Invalid performance level" - ); +const TDP_LIMIT1: &str = "power1_cap"; +const TDP_LIMIT2: &str = "power2_cap"; - let mut myfile = File::create(GPU_PERFORMANCE_LEVEL_PATH) +#[derive(PartialEq, Debug, Copy, Clone)] +#[repr(u32)] +pub enum GPUPerformanceLevel { + Auto = 0, + Low = 1, + High = 2, + Manual = 3, + ProfilePeak = 4, +} + +impl TryFrom for GPUPerformanceLevel { + type Error = &'static str; + fn try_from(v: u32) -> Result { + match v { + x if x == GPUPerformanceLevel::Auto as u32 => Ok(GPUPerformanceLevel::Auto), + x if x == GPUPerformanceLevel::Low as u32 => Ok(GPUPerformanceLevel::Low), + x if x == GPUPerformanceLevel::High as u32 => Ok(GPUPerformanceLevel::High), + x if x == GPUPerformanceLevel::Manual as u32 => Ok(GPUPerformanceLevel::Manual), + x if x == GPUPerformanceLevel::ProfilePeak as u32 => { + Ok(GPUPerformanceLevel::ProfilePeak) + } + _ => Err("No enum match for value {v}"), + } + } +} + +impl FromStr for GPUPerformanceLevel { + type Err = Error; + fn from_str(input: &str) -> Result { + match input { + "auto" => Ok(GPUPerformanceLevel::Auto), + "low" => Ok(GPUPerformanceLevel::Low), + "high" => Ok(GPUPerformanceLevel::High), + "manual" => Ok(GPUPerformanceLevel::Manual), + "peak_performance" => Ok(GPUPerformanceLevel::ProfilePeak), + v => Err(anyhow!("No enum match for value {v}")), + } + } +} + +impl ToString for GPUPerformanceLevel { + fn to_string(&self) -> String { + String::from(match self { + GPUPerformanceLevel::Auto => "auto", + GPUPerformanceLevel::Low => "low", + GPUPerformanceLevel::High => "high", + GPUPerformanceLevel::Manual => "manual", + GPUPerformanceLevel::ProfilePeak => "peak_performance", + }) + } +} + +pub async fn get_gpu_performance_level() -> Result { + let level = fs::read_to_string(path(GPU_PERFORMANCE_LEVEL_PATH)) + .await + .inspect_err(|message| error!("Error opening sysfs file for reading: {message}"))?; + + GPUPerformanceLevel::from_str(level.trim().as_ref()) +} + +pub async fn set_gpu_performance_level(level: GPUPerformanceLevel) -> Result<()> { + let mut myfile = File::create(path(GPU_PERFORMANCE_LEVEL_PATH)) .await .inspect_err(|message| error!("Error opening sysfs file for writing: {message}"))?; + let level: String = level.to_string(); + myfile - .write_all(levels[level as usize].as_bytes()) + .write_all(level.as_bytes()) .await .inspect_err(|message| error!("Error writing to sysfs file: {message}"))?; Ok(()) } -pub async fn set_gpu_clocks(clocks: i32) -> Result<()> { +pub async fn set_gpu_clocks(clocks: u32) -> Result<()> { // Set GPU clocks to given value valid between 200 - 1600 // Only used when GPU Performance Level is manual, but write whenever called. ensure!((200..=1600).contains(&clocks), "Invalid clocks"); - let mut myfile = File::create(GPU_CLOCKS_PATH) + let mut myfile = File::create(path(GPU_CLOCKS_PATH)) .await .inspect_err(|message| error!("Error opening sysfs file for writing: {message}"))?; @@ -67,25 +125,63 @@ pub async fn set_gpu_clocks(clocks: i32) -> Result<()> { Ok(()) } -pub async fn set_tdp_limit(limit: i32) -> Result<()> { - // Set TDP limit given if within range (3-15) - // Returns false on error or out of range - ensure!((3..=15).contains(&limit), "Invalid limit"); +pub async fn get_gpu_clocks() -> Result { + let clocks_file = File::open(path(GPU_CLOCKS_PATH)).await?; + let mut reader = BufReader::new(clocks_file); + loop { + let mut line = String::new(); + if reader.read_line(&mut line).await? == 0 { + break; + } + if line != "OD_SCLK:\n" { + continue; + } + let mut line = String::new(); + if reader.read_line(&mut line).await? == 0 { + break; + } + let mhz = match line.split_whitespace().nth(1) { + Some(mhz) if mhz.ends_with("Mhz") => mhz.trim_end_matches("Mhz"), + _ => break, + }; + + match mhz.parse() { + Ok(mhz) => return Ok(mhz), + Err(e) => return Err(e.into()), + } + } + Err(anyhow!("Couldn't find GPU clocks")) +} + +async fn find_hwmon() -> Result { let mut dir = fs::read_dir(path(GPU_HWMON_PREFIX)).await?; - let base = loop { + loop { let base = match dir.next_entry().await? { Some(entry) => entry.path(), None => bail!("hwmon not found"), }; - if fs::try_exists(base.join("power1_cap")).await? { - break base; + if fs::try_exists(base.join(TDP_LIMIT1)).await? { + return Ok(base); } - }; + } +} +pub async fn get_tdp_limit() -> Result { + let base = find_hwmon().await?; + let power1cap = fs::read_to_string(base.join(TDP_LIMIT1)).await?; + let power1cap: u32 = power1cap.parse()?; + Ok(power1cap / 1000000) +} + +pub async fn set_tdp_limit(limit: u32) -> Result<()> { + // Set TDP limit given if within range (3-15) + // Returns false on error or out of range + ensure!((3..=15).contains(&limit), "Invalid limit"); let data = format!("{limit}000000"); - let mut power1file = File::create(base.join("power1_cap")) + let base = find_hwmon().await?; + let mut power1file = File::create(base.join(TDP_LIMIT1)) .await .inspect_err(|message| { error!("Error opening sysfs power1_cap file for writing TDP limits {message}") @@ -95,7 +191,7 @@ pub async fn set_tdp_limit(limit: i32) -> Result<()> { .await .inspect_err(|message| error!("Error writing to power1_cap file: {message}"))?; - if let Ok(mut power2file) = File::create(base.join("power2_cap")).await { + if let Ok(mut power2file) = File::create(base.join(TDP_LIMIT2)).await { power2file .write(data.as_bytes()) .await @@ -105,15 +201,153 @@ pub async fn set_tdp_limit(limit: i32) -> Result<()> { } #[cfg(test)] -mod test { +pub mod test { use super::*; use crate::testing; use anyhow::anyhow; use tokio::fs::{create_dir_all, read_to_string, remove_dir, write}; + pub async fn setup() { + let filename = path(GPU_PERFORMANCE_LEVEL_PATH); + create_dir_all(filename.parent().unwrap()) + .await + .expect("create_dir_all"); + } + + pub async fn write_clocks(mhz: u32) { + let filename = path(GPU_CLOCKS_PATH); + create_dir_all(filename.parent().unwrap()) + .await + .expect("create_dir_all"); + + let contents = format!( + "OD_SCLK: +0: {mhz}Mhz +1: {mhz}Mhz +OD_RANGE: +SCLK: 200Mhz 1600Mhz +CCLK: 1400Mhz 3500Mhz +CCLK_RANGE in Core0: +0: 1400Mhz +1: 3500Mhz\n" + ); + + write(filename.as_path(), contents).await.expect("write"); + } + + pub async fn expect_clocks(mhz: u32) { + let clocks = read_to_string(path(GPU_CLOCKS_PATH)).await.expect("read"); + assert_eq!(clocks, format!("s 0 {mhz}\ns 1 {mhz}\nc\n")); + } + + #[tokio::test] + async fn test_get_gpu_performance_level() { + let _h = testing::start(); + + let filename = path(GPU_PERFORMANCE_LEVEL_PATH); + setup().await; + assert!(get_gpu_performance_level().await.is_err()); + + write(filename.as_path(), "auto\n").await.expect("write"); + assert_eq!( + get_gpu_performance_level().await.unwrap(), + GPUPerformanceLevel::Auto + ); + + write(filename.as_path(), "low\n").await.expect("write"); + assert_eq!( + get_gpu_performance_level().await.unwrap(), + GPUPerformanceLevel::Low + ); + + write(filename.as_path(), "high\n").await.expect("write"); + assert_eq!( + get_gpu_performance_level().await.unwrap(), + GPUPerformanceLevel::High + ); + + write(filename.as_path(), "manual\n").await.expect("write"); + assert_eq!( + get_gpu_performance_level().await.unwrap(), + GPUPerformanceLevel::Manual + ); + + write(filename.as_path(), "peak_performance\n") + .await + .expect("write"); + assert_eq!( + get_gpu_performance_level().await.unwrap(), + GPUPerformanceLevel::ProfilePeak + ); + + write(filename.as_path(), "nothing\n").await.expect("write"); + assert!(get_gpu_performance_level().await.is_err()); + } + + #[tokio::test] + async fn test_set_gpu_performance_level() { + let _h = testing::start(); + + let filename = path(GPU_PERFORMANCE_LEVEL_PATH); + setup().await; + + set_gpu_performance_level(GPUPerformanceLevel::Auto) + .await + .expect("set"); + assert_eq!( + read_to_string(filename.as_path()).await.unwrap().trim(), + "auto" + ); + set_gpu_performance_level(GPUPerformanceLevel::Low) + .await + .expect("set"); + assert_eq!( + read_to_string(filename.as_path()).await.unwrap().trim(), + "low" + ); + set_gpu_performance_level(GPUPerformanceLevel::High) + .await + .expect("set"); + assert_eq!( + read_to_string(filename.as_path()).await.unwrap().trim(), + "high" + ); + set_gpu_performance_level(GPUPerformanceLevel::Manual) + .await + .expect("set"); + assert_eq!( + read_to_string(filename.as_path()).await.unwrap().trim(), + "manual" + ); + set_gpu_performance_level(GPUPerformanceLevel::ProfilePeak) + .await + .expect("set"); + assert_eq!( + read_to_string(filename.as_path()).await.unwrap().trim(), + "peak_performance" + ); + } + + #[tokio::test] + async fn test_get_tdp_limit() { + let _h = testing::start(); + + let hwmon = path(GPU_HWMON_PREFIX); + create_dir_all(hwmon.join("hwmon5").as_path()) + .await + .expect("create_dir_all"); + + assert!(get_tdp_limit().await.is_err()); + + write(hwmon.join("hwmon5").join(TDP_LIMIT1), "15000000") + .await + .expect("write"); + assert_eq!(get_tdp_limit().await.unwrap(), 15); + } + #[tokio::test] async fn test_set_tdp_limit() { - let h = testing::start(); + let _h = testing::start(); assert_eq!( set_tdp_limit(2).await.unwrap_err().to_string(), @@ -135,10 +369,10 @@ mod test { ); let hwmon = hwmon.join("hwmon5"); - create_dir_all(hwmon.join("power1_cap")) + create_dir_all(hwmon.join(TDP_LIMIT1)) .await .expect("create_dir_all"); - create_dir_all(hwmon.join("power2_cap")) + create_dir_all(hwmon.join(TDP_LIMIT2)) .await .expect("create_dir_all"); assert_eq!( @@ -146,28 +380,56 @@ mod test { anyhow!("Is a directory (os error 21)").to_string() ); - remove_dir(hwmon.join("power1_cap")) + remove_dir(hwmon.join(TDP_LIMIT1)) .await .expect("remove_dir"); - write(hwmon.join("power1_cap"), "0").await.expect("write"); + write(hwmon.join(TDP_LIMIT1), "0").await.expect("write"); assert!(set_tdp_limit(10).await.is_ok()); - let power1_cap = read_to_string(hwmon.join("power1_cap")) + let power1_cap = read_to_string(hwmon.join(TDP_LIMIT1)) .await .expect("power1_cap"); assert_eq!(power1_cap, "10000000"); - remove_dir(hwmon.join("power2_cap")) + remove_dir(hwmon.join(TDP_LIMIT2)) .await .expect("remove_dir"); - write(hwmon.join("power2_cap"), "0").await.expect("write"); + write(hwmon.join(TDP_LIMIT2), "0").await.expect("write"); assert!(set_tdp_limit(15).await.is_ok()); - let power1_cap = read_to_string(hwmon.join("power1_cap")) + let power1_cap = read_to_string(hwmon.join(TDP_LIMIT1)) .await .expect("power1_cap"); assert_eq!(power1_cap, "15000000"); - let power2_cap = read_to_string(hwmon.join("power2_cap")) + let power2_cap = read_to_string(hwmon.join(TDP_LIMIT2)) .await .expect("power2_cap"); assert_eq!(power2_cap, "15000000"); } + + #[tokio::test] + async fn test_get_gpu_clocks() { + let _h = testing::start(); + setup().await; + + assert!(get_gpu_clocks().await.is_err()); + write_clocks(1600).await; + + assert_eq!(get_gpu_clocks().await.unwrap(), 1600); + } + + #[tokio::test] + async fn test_set_gpu_clocks() { + let _h = testing::start(); + + assert!(set_gpu_clocks(1600).await.is_err()); + setup().await; + + assert!(set_gpu_clocks(100).await.is_err()); + assert!(set_gpu_clocks(2000).await.is_err()); + + assert!(set_gpu_clocks(200).await.is_ok()); + expect_clocks(200).await; + + assert!(set_gpu_clocks(1600).await.is_ok()); + expect_clocks(1600).await; + } } diff --git a/src/process.rs b/src/process.rs index e38f15c..ad0da0f 100644 --- a/src/process.rs +++ b/src/process.rs @@ -5,28 +5,38 @@ * SPDX-License-Identifier: MIT */ -use anyhow::Result; +use anyhow::{anyhow, Result}; use std::ffi::OsStr; +#[cfg(not(test))] use tokio::process::Command; -use tracing::warn; -pub const SYSTEMCTL_PATH: &str = "/usr/bin/systemctl"; - -pub async fn script_exit_code(executable: &str, args: &[impl AsRef]) -> Result { - // Run given script and return true on success +#[cfg(not(test))] +pub async fn script_exit_code(executable: &str, args: &[impl AsRef]) -> Result { + // Run given script and return the exit code let mut child = Command::new(executable).args(args).spawn()?; let status = child.wait().await?; - Ok(status.success()) + status.code().ok_or(anyhow!("Killed by signal")) } -pub async fn run_script(name: &str, executable: &str, args: &[impl AsRef]) -> Result { +#[cfg(test)] +pub async fn script_exit_code(executable: &str, args: &[impl AsRef]) -> Result { + let test = crate::testing::current(); + let args: Vec<&OsStr> = args.iter().map(|arg| arg.as_ref()).collect(); + let cb = test.process_cb.get(); + cb(executable, args.as_ref()).map(|(res, _)| res) +} + +pub async fn run_script(executable: &str, args: &[impl AsRef]) -> Result<()> { // Run given script to get exit code and return true on success. - // Return false on failure, but also print an error if needed - script_exit_code(executable, args) - .await - .inspect_err(|message| warn!("Error running {name} {message}")) + // Return Err on failure, but also print an error if needed + match script_exit_code(executable, args).await { + Ok(0) => Ok(()), + Ok(code) => Err(anyhow!("Exited {code}")), + Err(message) => Err(message), + } } +#[cfg(not(test))] pub async fn script_output(executable: &str, args: &[impl AsRef]) -> Result { // Run given command and return the output given let output = Command::new(executable).args(args).output(); @@ -36,3 +46,55 @@ pub async fn script_output(executable: &str, args: &[impl AsRef]) -> Resu let s = std::str::from_utf8(&output.stdout)?; Ok(s.to_string()) } + +#[cfg(test)] +pub async fn script_output(executable: &str, args: &[impl AsRef]) -> Result { + let test = crate::testing::current(); + let args: Vec<&OsStr> = args.iter().map(|arg| arg.as_ref()).collect(); + let cb = test.process_cb.get(); + cb(executable, args.as_ref()).map(|(_, res)| res) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::testing; + + fn ok(_: &str, args: &[&OsStr]) -> Result<(i32, String)> { + Ok((0, String::from("ok"))) + } + + fn code(_: &str, args: &[&OsStr]) -> Result<(i32, String)> { + Ok((1, String::from("code"))) + } + + fn exit(_: &str, args: &[&OsStr]) -> Result<(i32, String)> { + Err(anyhow!("oops!")) + } + + #[tokio::test] + async fn test_run_script() { + let h = testing::start(); + + h.test.process_cb.set(ok); + assert!(run_script("", &[] as &[&OsStr]).await.is_ok()); + + h.test.process_cb.set(code); + assert_eq!( + run_script("", &[] as &[&OsStr]) + .await + .unwrap_err() + .to_string(), + "Exited 1" + ); + + h.test.process_cb.set(exit); + assert_eq!( + run_script("", &[] as &[&OsStr]) + .await + .unwrap_err() + .to_string(), + "oops!" + ); + } +} diff --git a/src/sls/ftrace.rs b/src/sls/ftrace.rs index b98bcf8..e2a5028 100644 --- a/src/sls/ftrace.rs +++ b/src/sls/ftrace.rs @@ -117,10 +117,8 @@ mod test { use crate::testing; use nix::sys::stat::Mode; use nix::unistd; - use std::cell::Cell; - use std::fs; - use std::path::PathBuf; - use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + use tokio::fs::{create_dir_all, read_to_string, write}; + use tokio::sync::mpsc::{error, unbounded_channel, UnboundedSender}; struct MockTrace { traces: UnboundedSender<(String, HashMap)>, @@ -133,7 +131,7 @@ mod test { trace: &str, data: HashMap<&str, zvariant::Value<'_>>, ) -> zbus::fdo::Result<()> { - self.traces.send(( + let _ = self.traces.send(( String::from(trace), HashMap::from_iter( data.iter() @@ -146,18 +144,31 @@ mod test { #[tokio::test] async fn handle_pid() { - let h = testing::start(); - let path = h.test.path(); + let _h = testing::start(); - fs::create_dir_all(path.join("proc/1234")).expect("create_dir_all"); - fs::write(path.join("proc/1234/comm"), "ftrace\n").expect("write comm"); - fs::write(path.join("proc/1234/environ"), "SteamGameId=5678").expect("write environ"); + create_dir_all(path("/proc/1234")) + .await + .expect("create_dir_all"); + write(path("/proc/1234/comm"), "ftrace\n") + .await + .expect("write comm"); + write(path("/proc/1234/environ"), "SteamGameId=5678") + .await + .expect("write environ"); - fs::create_dir_all(path.join("proc/1235")).expect("create_dir_all"); - fs::write(path.join("proc/1235/comm"), "ftrace\n").expect("write comm"); + create_dir_all(path("/proc/1235")) + .await + .expect("create_dir_all"); + write(path("/proc/1235/comm"), "ftrace\n") + .await + .expect("write comm"); - fs::create_dir_all(path.join("proc/1236")).expect("create_dir_all"); - fs::write(path.join("proc/1236/environ"), "SteamGameId=5678").expect("write environ"); + create_dir_all(path("/proc/1236")) + .await + .expect("create_dir_all"); + write(path("/proc/1236/environ"), "SteamGameId=5678") + .await + .expect("write environ"); let mut map = HashMap::new(); assert!(Ftrace::handle_pid(&mut map, 1234).await.is_ok()); @@ -189,43 +200,53 @@ mod test { #[tokio::test] async fn ftrace_init() { - let h = testing::start(); - let path = h.test.path(); + let _h = testing::start(); let tracefs = Ftrace::base(); - fs::create_dir_all(tracefs.join("events/oom/mark_victim")).expect("create_dir_all"); + create_dir_all(tracefs.join("events/oom/mark_victim")) + .await + .expect("create_dir_all"); unistd::mkfifo( tracefs.join("trace_pipe").as_path(), Mode::S_IRUSR | Mode::S_IWUSR, ) .expect("trace_pipe"); let dbus = Connection::session().await.expect("dbus"); - let ftrace = Ftrace::init(dbus).await.expect("ftrace"); + let _ftrace = Ftrace::init(dbus).await.expect("ftrace"); assert_eq!( - fs::read_to_string(tracefs.join("events/oom/mark_victim/enable")).unwrap(), + read_to_string(tracefs.join("events/oom/mark_victim/enable")) + .await + .unwrap(), "1" ); } #[tokio::test] async fn ftrace_relay() { - let h = testing::start(); - let path = h.test.path(); + let _h = testing::start(); let tracefs = Ftrace::base(); - fs::create_dir_all(tracefs.join("events/oom/mark_victim")).expect("create_dir_all"); + create_dir_all(tracefs.join("events/oom/mark_victim")) + .await + .expect("create_dir_all"); unistd::mkfifo( tracefs.join("trace_pipe").as_path(), Mode::S_IRUSR | Mode::S_IWUSR, ) .expect("trace_pipe"); - fs::create_dir_all(path.join("proc/14351")).expect("create_dir_all"); - fs::write(path.join("proc/14351/comm"), "ftrace\n").expect("write comm"); - fs::write(path.join("proc/14351/environ"), "SteamGameId=5678").expect("write environ"); + create_dir_all(path("/proc/14351")) + .await + .expect("create_dir_all"); + write(path("/proc/14351/comm"), "ftrace\n") + .await + .expect("write comm"); + write(path("/proc/14351/environ"), "SteamGameId=5678") + .await + .expect("write environ"); let (sender, mut receiver) = unbounded_channel(); let trace = MockTrace { traces: sender }; @@ -241,7 +262,7 @@ mod test { let mut ftrace = Ftrace::init(dbus).await.expect("ftrace"); assert!(match receiver.try_recv() { - Empty => true, + Err(error::TryRecvError::Empty) => true, _ => false, }); ftrace diff --git a/src/systemd.rs b/src/systemd.rs new file mode 100644 index 0000000..6c111f5 --- /dev/null +++ b/src/systemd.rs @@ -0,0 +1,76 @@ +/* + * Copyright © 2023 Collabora Ltd. + * Copyright © 2024 Valve Software + * + * SPDX-License-Identifier: MIT + */ + +use anyhow::{anyhow, Result}; +use std::path::PathBuf; +use zbus::zvariant::OwnedObjectPath; +use zbus::Connection; + +#[zbus::proxy( + interface = "org.freedesktop.systemd1.Unit", + default_service = "org.freedesktop.systemd1" +)] +trait SystemdUnit { + #[zbus(property)] + fn active_state(&self) -> Result; + + async fn restart(&self, mode: &str) -> Result; + async fn start(&self, mode: &str) -> Result; + async fn stop(&self, mode: &str) -> Result; +} + +#[zbus::proxy( + interface = "org.freedesktop.systemd1.Manager", + default_service = "org.freedesktop.systemd1", + default_path = "/org/freedesktop/systemd1" +)] +trait SystemdManager { + async fn reload(&self) -> Result<()>; +} + +pub struct SystemdUnit<'dbus> { + proxy: SystemdUnitProxy<'dbus>, +} + +pub async fn daemon_reload(connection: &Connection) -> Result<()> { + let proxy = SystemdManagerProxy::new(&connection).await?; + proxy.reload().await?; + Ok(()) +} + +impl<'dbus> SystemdUnit<'dbus> { + pub async fn new(connection: Connection, name: &str) -> Result> { + let path = PathBuf::from("/org/freedesktop/systemd1/unit").join(name); + let path = String::from(path.to_str().ok_or(anyhow!("Unit name {name} invalid"))?); + Ok(SystemdUnit { + proxy: SystemdUnitProxy::builder(&connection) + .cache_properties(zbus::CacheProperties::No) + .path(path)? + .build() + .await?, + }) + } + + pub async fn restart(&self) -> Result<()> { + self.proxy.restart("fail").await?; + Ok(()) + } + + pub async fn start(&self) -> Result<()> { + self.proxy.start("fail").await?; + Ok(()) + } + + pub async fn stop(&self) -> Result<()> { + self.proxy.stop("fail").await?; + Ok(()) + } + + pub async fn active(&self) -> Result { + Ok(self.proxy.active_state().await? == "active") + } +} diff --git a/src/testing.rs b/src/testing.rs index 0276d09..ae19cd2 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,4 +1,6 @@ -use std::cell::RefCell; +use anyhow::{anyhow, Result}; +use std::cell::{Cell, RefCell}; +use std::ffi::OsStr; use std::path::Path; use std::rc::Rc; use tempfile::{tempdir, TempDir}; @@ -12,6 +14,7 @@ pub fn start() -> TestHandle { assert!(lock.borrow().as_ref().is_none()); let test: Rc = Rc::new(Test { base: tempdir().expect("Couldn't create test directory"), + process_cb: Cell::new(|_, _| Err(anyhow!("No current process_cb"))), }); *lock.borrow_mut() = Some(test.clone()); TestHandle { test } @@ -28,6 +31,7 @@ pub fn current() -> Rc { pub struct Test { base: TempDir, + pub process_cb: Cell Result<(i32, String)>>, } pub struct TestHandle { diff --git a/src/wifi.rs b/src/wifi.rs index 70c5600..14c99f7 100644 --- a/src/wifi.rs +++ b/src/wifi.rs @@ -5,12 +5,16 @@ * SPDX-License-Identifier: MIT */ -use anyhow::Result; +use anyhow::{bail, ensure, Error, Result}; use std::fmt; +use std::str::FromStr; use tokio::fs; use tracing::error; +use zbus::Connection; -use crate::process::{run_script, SYSTEMCTL_PATH}; +use crate::path; +use crate::process::run_script; +use crate::systemd::{daemon_reload, SystemdUnit}; const OVERRIDE_CONTENTS: &str = "[Service] ExecStart= @@ -24,19 +28,29 @@ const OVERRIDE_PATH: &str = "/etc/systemd/system/iwd.service.d/override.conf"; const OUTPUT_FILE: &str = "/var/log/wifitrace.dat"; const TRACE_CMD_PATH: &str = "/usr/bin/trace-cmd"; +const MIN_BUFFER_SIZE: u32 = 100; + +const WIFI_BACKEND_PATH: &str = "/etc/NetworkManager/conf.d/wifi_backend.conf"; + #[derive(PartialEq, Debug, Copy, Clone)] #[repr(u32)] pub enum WifiDebugMode { - Off, - On, + Off = 0, + On = 1, } #[derive(PartialEq, Debug, Copy, Clone)] #[repr(u32)] pub enum WifiPowerManagement { - UnsupportedFeature = 0, - Disabled = 1, - Enabled = 2, + Disabled = 0, + Enabled = 1, +} + +#[derive(PartialEq, Debug, Copy, Clone)] +#[repr(u32)] +pub enum WifiBackend { + IWD = 0, + WPASupplicant = 1, } impl TryFrom for WifiDebugMode { @@ -63,9 +77,6 @@ impl TryFrom for WifiPowerManagement { type Error = &'static str; fn try_from(v: u32) -> Result { match v { - x if x == WifiPowerManagement::UnsupportedFeature as u32 => { - Ok(WifiPowerManagement::UnsupportedFeature) - } x if x == WifiPowerManagement::Disabled as u32 => Ok(WifiPowerManagement::Disabled), x if x == WifiPowerManagement::Enabled as u32 => Ok(WifiPowerManagement::Enabled), _ => Err("No enum match for value {v}"), @@ -76,13 +87,43 @@ impl TryFrom for WifiPowerManagement { impl fmt::Display for WifiPowerManagement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - WifiPowerManagement::UnsupportedFeature => write!(f, "Unsupported feature"), WifiPowerManagement::Disabled => write!(f, "Disabled"), WifiPowerManagement::Enabled => write!(f, "Enabled"), } } } +impl TryFrom for WifiBackend { + type Error = &'static str; + fn try_from(v: u32) -> Result { + match v { + x if x == WifiBackend::IWD as u32 => Ok(WifiBackend::IWD), + x if x == WifiBackend::WPASupplicant as u32 => Ok(WifiBackend::WPASupplicant), + _ => Err("No enum match for WifiBackend value {v}"), + } + } +} + +impl FromStr for WifiBackend { + type Err = Error; + fn from_str(input: &str) -> Result { + Ok(match input { + "iwd" => WifiBackend::IWD, + "wpa_supplicant" => WifiBackend::WPASupplicant, + _ => bail!("Unknown backend"), + }) + } +} + +impl fmt::Display for WifiBackend { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + WifiBackend::IWD => write!(f, "iwd"), + WifiBackend::WPASupplicant => write!(f, "wpa_supplicant"), + } + } +} + pub async fn setup_iwd_config(want_override: bool) -> std::io::Result<()> { // Copy override.conf file into place or out of place depending // on install value @@ -90,55 +131,160 @@ pub async fn setup_iwd_config(want_override: bool) -> std::io::Result<()> { if want_override { // Copy it in // Make sure the folder exists - fs::create_dir_all(OVERRIDE_FOLDER).await?; + fs::create_dir_all(path(OVERRIDE_FOLDER)).await?; // Then write the contents into the file - fs::write(OVERRIDE_PATH, OVERRIDE_CONTENTS).await + fs::write(path(OVERRIDE_PATH), OVERRIDE_CONTENTS).await } else { // Delete it - fs::remove_file(OVERRIDE_PATH).await + fs::remove_file(path(OVERRIDE_PATH)).await } } -pub async fn restart_iwd() -> Result { +async fn restart_iwd(connection: Connection) -> Result<()> { // First reload systemd since we modified the config most likely // otherwise we wouldn't be restarting iwd. - match run_script("reload systemd", SYSTEMCTL_PATH, &["daemon-reload"]).await { - Ok(value) => { - if value { - // worked, now restart iwd - run_script("restart iwd", SYSTEMCTL_PATH, &["restart", "iwd"]).await - } else { - // reload failed - error!("restart_iwd: reload systemd failed with non-zero exit code"); - Ok(false) - } - } - Err(message) => { - error!("restart_iwd: reload systemd got an error: {message}"); - Err(message) - } - } + daemon_reload(&connection) + .await + .inspect_err(|message| error!("restart_iwd: reload systemd got an error: {message}"))?; + + // worked, now restart iwd + let unit = SystemdUnit::new(connection, "iwd_2eservice").await?; + unit.restart() + .await + .inspect_err(|message| error!("restart_iwd: restart unit got an error: {message}")) } -pub async fn stop_tracing() -> Result { +async fn stop_tracing() -> Result<()> { // Stop tracing and extract ring buffer to disk for capture - run_script("stop tracing", TRACE_CMD_PATH, &["stop"]).await?; + run_script(TRACE_CMD_PATH, &["stop"]).await?; // stop tracing worked - run_script( - "extract traces", - TRACE_CMD_PATH, - &["extract", "-o", OUTPUT_FILE], - ) - .await + run_script(TRACE_CMD_PATH, &["extract", "-o", OUTPUT_FILE]).await } -pub async fn start_tracing(buffer_size: u32) -> Result { +async fn start_tracing(buffer_size: u32) -> Result<()> { // Start tracing - let size_str = format!("{}", buffer_size); + let size_str = buffer_size.to_string(); run_script( - "start tracing", TRACE_CMD_PATH, &["start", "-e", "ath11k_wmi_diag", "-b", &size_str], ) .await } + +pub async fn set_wifi_debug_mode( + mode: WifiDebugMode, + buffer_size: u32, + should_trace: bool, + connection: Connection, +) -> Result<()> { + // Set the wifi debug mode to mode, using an int for flexibility going forward but only + // doing things on 0 or 1 for now + // Return false on error + + match mode { + WifiDebugMode::Off => { + // If mode is 0 disable wifi debug mode + // Stop any existing trace and flush to disk. + if should_trace { + if let Err(message) = stop_tracing().await { + bail!("stop_tracing command got an error: {message}"); + }; + } + // Stop_tracing was successful + if let Err(message) = setup_iwd_config(false).await { + bail!("setup_iwd_config false got an error: {message}"); + }; + // setup_iwd_config false worked + if let Err(message) = restart_iwd(connection).await { + bail!("restart_iwd got an error: {message}"); + }; + } + WifiDebugMode::On => { + ensure!(buffer_size > MIN_BUFFER_SIZE, "Buffer size too small"); + + if let Err(message) = setup_iwd_config(true).await { + bail!("setup_iwd_config true got an error: {message}"); + } + // setup_iwd_config worked + if let Err(message) = restart_iwd(connection).await { + bail!("restart_iwd got an error: {message}"); + }; + // restart_iwd worked + if should_trace { + if let Err(message) = start_tracing(buffer_size).await { + bail!("start_tracing got an error: {message}"); + }; + } + } + } + Ok(()) +} + +pub async fn get_wifi_backend() -> Result { + let wifi_backend_contents = fs::read_to_string(path(WIFI_BACKEND_PATH)) + .await? + .trim() + .to_string(); + for line in wifi_backend_contents.lines() { + if line.starts_with("wifi.backend=") { + let backend = line.trim_start_matches("wifi.backend=").trim(); + return WifiBackend::from_str(backend); + } + } + + bail!("WiFi backend not found in config"); +} + +pub async fn set_wifi_backend(backend: WifiBackend) -> Result<()> { + run_script("/usr/bin/steamos-wifi-set-backend", &[backend.to_string()]).await +} + +#[cfg(test)] +mod test { + use super::*; + use crate::testing; + use tokio::fs::{create_dir_all, write}; + + #[tokio::test] + async fn test_wifi_backend_to_string() { + assert_eq!(WifiBackend::IWD.to_string(), "iwd"); + assert_eq!(WifiBackend::WPASupplicant.to_string(), "wpa_supplicant"); + } + + #[tokio::test] + async fn test_get_wifi_backend() { + let _h = testing::start(); + + create_dir_all(path(WIFI_BACKEND_PATH).parent().unwrap()) + .await + .expect("create_dir_all"); + + assert!(get_wifi_backend().await.is_err()); + + write(path(WIFI_BACKEND_PATH), "[device]") + .await + .expect("write"); + assert!(get_wifi_backend().await.is_err()); + + write(path(WIFI_BACKEND_PATH), "[device]\nwifi.backend=fake\n") + .await + .expect("write"); + assert!(get_wifi_backend().await.is_err()); + + write(path(WIFI_BACKEND_PATH), "[device]\nwifi.backend=iwd\n") + .await + .expect("write"); + assert_eq!(get_wifi_backend().await.unwrap(), WifiBackend::IWD); + + write( + path(WIFI_BACKEND_PATH), + "[device]\nwifi.backend=wpa_supplicant\n", + ) + .await + .expect("write"); + assert_eq!( + get_wifi_backend().await.unwrap(), + WifiBackend::WPASupplicant + ); + } +}