diff --git a/Makefile b/Makefile index f463645..a274747 100644 --- a/Makefile +++ b/Makefile @@ -24,9 +24,11 @@ install: target/release/steamos-manager target/release/steamosctl install -Ds -m755 "target/release/steamos-manager" "$(DESTDIR)/usr/lib/steamos-manager" install -D -m755 "target/release/steamosctl" "$(DESTDIR)/usr/bin/steamosctl" - install -D -m644 -t "$(DESTDIR)/usr/share/steamos-manager/platforms" "data/platforms/"* + install -D -m644 -t "$(DESTDIR)/usr/share/steamos-manager/devices" "data/devices/"* install -D -m644 LICENSE "$(DESTDIR)/usr/share/licenses/steamos-manager/LICENSE" + install -m644 "data/platform.toml" "$(DESTDIR)/usr/share/steamos-manager/" + install -m644 "data/system/com.steampowered.SteamOSManager1.service" "$(DESTDIR)/usr/share/dbus-1/system-services/" install -m644 "data/system/com.steampowered.SteamOSManager1.conf" "$(DESTDIR)/usr/share/dbus-1/system.d/" install -m644 "data/system/steamos-manager.service" "$(DESTDIR)/usr/lib/systemd/system/" diff --git a/data/devices/jupiter.toml b/data/devices/jupiter.toml new file mode 100644 index 0000000..6e7d81d --- /dev/null +++ b/data/devices/jupiter.toml @@ -0,0 +1,16 @@ +[tdp_limit] +method = "gpu_hwmon" +download_mode_limit = 6 + +[tdp_limit.range] +min = 3 +max = 15 + +[gpu_clocks] +min = 200 +max = 1600 + +[battery_charge_limit] +suggested_minimum_limit = 10 +hwmon_name = "steamdeck_hwmon" +attribute = "max_battery_charge_level" diff --git a/data/platforms/legion-go-series.toml b/data/devices/legion-go-series.toml similarity index 100% rename from data/platforms/legion-go-series.toml rename to data/devices/legion-go-series.toml diff --git a/data/platforms/rog-ally-series.toml b/data/devices/rog-ally-series.toml similarity index 100% rename from data/platforms/rog-ally-series.toml rename to data/devices/rog-ally-series.toml diff --git a/data/platforms/zotac-gaming-zone.toml b/data/devices/zotac-gaming-zone.toml similarity index 100% rename from data/platforms/zotac-gaming-zone.toml rename to data/devices/zotac-gaming-zone.toml diff --git a/data/platforms/jupiter.toml b/data/platform.toml similarity index 75% rename from data/platforms/jupiter.toml rename to data/platform.toml index 881ebb8..ddab3e5 100644 --- a/data/platforms/jupiter.toml +++ b/data/platform.toml @@ -28,20 +28,3 @@ no_validate_flag = "--skip-validation" [fan_control] systemd = "jupiter-fan-control.service" - -[tdp_limit] -method = "gpu_hwmon" -download_mode_limit = 6 - -[tdp_limit.range] -min = 3 -max = 15 - -[gpu_clocks] -min = 200 -max = 1600 - -[battery_charge_limit] -suggested_minimum_limit = 10 -hwmon_name = "steamdeck_hwmon" -attribute = "max_battery_charge_level" diff --git a/src/hardware.rs b/src/hardware.rs index 0ffc691..e6fb8dd 100644 --- a/src/hardware.rs +++ b/src/hardware.rs @@ -483,19 +483,11 @@ pub mod test { sleep(Duration::from_millis(10)).await; - h.test.platform_config.replace(Some(PlatformConfig { - factory_reset: None, - update_bios: None, - update_dock: None, - storage: None, - fan_control: Some(ServiceConfig::Systemd(String::from( - "jupiter-fan-control.service", - ))), - tdp_limit: None, - gpu_clocks: None, - battery_charge_limit: None, - performance_profile: None, - })); + let mut platform_config = PlatformConfig::default(); + platform_config.fan_control = Some(ServiceConfig::Systemd(String::from( + "jupiter-fan-control.service", + ))); + h.test.platform_config.replace(Some(platform_config)); let fan_control = FanControl::new(connection); assert_eq!( diff --git a/src/manager/root.rs b/src/manager/root.rs index 9971811..171ec07 100644 --- a/src/manager/root.rs +++ b/src/manager/root.rs @@ -24,7 +24,7 @@ use crate::hardware::{ steam_deck_variant, FactoryResetKind, FanControl, FanControlState, SteamDeckVariant, }; use crate::job::JobManager; -use crate::platform::platform_config; +use crate::platform::{device_config, platform_config}; use crate::power::{ set_cpu_scaling_governor, set_gpu_clocks, set_gpu_performance_level, set_gpu_power_profile, set_max_charge_level, set_platform_profile, tdp_limit_manager, CPUScalingGovernor, @@ -450,7 +450,7 @@ impl SteamOSManager { } async fn set_performance_profile(&self, profile: &str) -> fdo::Result<()> { - let config = platform_config().await.map_err(to_zbus_fdo_error)?; + let config = device_config().await.map_err(to_zbus_fdo_error)?; let config = config .as_ref() .and_then(|config| config.performance_profile.as_ref()) diff --git a/src/manager/user.rs b/src/manager/user.rs index 5a486a8..f4d5654 100644 --- a/src/manager/user.rs +++ b/src/manager/user.rs @@ -24,7 +24,7 @@ use crate::hardware::{ device_type, device_variant, steam_deck_variant, DeviceType, SteamDeckVariant, }; use crate::job::JobManagerCommand; -use crate::platform::platform_config; +use crate::platform::{device_config, platform_config}; use crate::power::{ get_available_cpu_scaling_governors, get_available_gpu_performance_levels, get_available_gpu_power_profiles, get_available_platform_profiles, get_cpu_scaling_governor, @@ -286,7 +286,7 @@ impl BatteryChargeLimit1 { #[zbus(property(emits_changed_signal = "const"))] async fn suggested_minimum_limit(&self) -> i32 { - let Ok(Some(ref config)) = platform_config().await else { + let Ok(Some(ref config)) = device_config().await else { return BatteryChargeLimit1::DEFAULT_SUGGESTED_MINIMUM_LIMIT; }; let Some(ref config) = config.battery_charge_limit else { @@ -552,7 +552,7 @@ impl Manager2 { impl PerformanceProfile1 { #[zbus(property(emits_changed_signal = "const"))] async fn available_performance_profiles(&self) -> fdo::Result> { - let config = platform_config().await.map_err(to_zbus_fdo_error)?; + let config = device_config().await.map_err(to_zbus_fdo_error)?; let config = config .as_ref() .and_then(|config| config.performance_profile.as_ref()) @@ -566,7 +566,7 @@ impl PerformanceProfile1 { #[zbus(property)] async fn performance_profile(&self) -> fdo::Result { - let config = platform_config().await.map_err(to_zbus_fdo_error)?; + let config = device_config().await.map_err(to_zbus_fdo_error)?; let config = config .as_ref() .and_then(|config| config.performance_profile.as_ref()) @@ -614,7 +614,7 @@ impl PerformanceProfile1 { #[zbus(property(emits_changed_signal = "const"))] async fn suggested_default_performance_profile(&self) -> fdo::Result { - let config = platform_config().await.map_err(to_zbus_fdo_error)?; + let config = device_config().await.map_err(to_zbus_fdo_error)?; let config = config .as_ref() .and_then(|config| config.performance_profile.as_ref()) @@ -852,11 +852,11 @@ impl WifiPowerManagement1 { } } -async fn create_config_interfaces( +async fn create_platform_interfaces( proxy: &Proxy<'static>, object_server: &ObjectServer, + connection: &Connection, job_manager: &UnboundedSender, - tdp_manager: Option>, ) -> Result<()> { let Some(config) = platform_config().await? else { return Ok(()); @@ -868,10 +868,6 @@ async fn create_config_interfaces( let fan_control = FanControl1 { proxy: proxy.clone(), }; - let performance_profile = PerformanceProfile1 { - proxy: proxy.clone(), - tdp_limit_manager: tdp_manager.clone(), - }; let storage = Storage1 { proxy: proxy.clone(), job_manager: job_manager.clone(), @@ -885,14 +881,73 @@ async fn create_config_interfaces( job_manager: job_manager.clone(), }; - if config.factory_reset.is_some() { - object_server.at(MANAGER_PATH, factory_reset).await?; + if let Some(config) = config.factory_reset.as_ref() { + match config.is_valid(true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, factory_reset).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if factory reset config is valid: {e}"), + } } - if config.fan_control.is_some() { - object_server.at(MANAGER_PATH, fan_control).await?; + if let Some(config) = config.fan_control.as_ref() { + match config.is_valid(connection, true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, fan_control).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if fan control config is valid: {e}"), + } } + if let Some(config) = config.storage.as_ref() { + match config.is_valid(true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, storage).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if storage config is valid: {e}"), + } + } + + if let Some(config) = config.update_bios.as_ref() { + match config.is_valid(true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, update_bios).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if BIOS update config is valid: {e}"), + } + } + + if let Some(config) = config.update_dock.as_ref() { + match config.is_valid(true).await { + Ok(true) => { + object_server.at(MANAGER_PATH, update_dock).await?; + } + Ok(false) => (), + Err(e) => error!("Failed to verify if dock update config is valid: {e}"), + } + } + + Ok(()) +} + +async fn create_device_interfaces( + proxy: &Proxy<'static>, + object_server: &ObjectServer, + tdp_manager: Option>, +) -> Result<()> { + let Some(config) = device_config().await? else { + return Ok(()); + }; + + let performance_profile = PerformanceProfile1 { + proxy: proxy.clone(), + tdp_limit_manager: tdp_manager.clone(), + }; + if let Some(manager) = tdp_manager { let low_power_mode = LowPowerMode1 { manager: manager.clone(), @@ -918,7 +973,7 @@ async fn create_config_interfaces( }); } - if let Some(ref config) = config.performance_profile { + if let Some(config) = config.performance_profile.as_ref() { if !get_available_platform_profiles(&config.platform_profile_name) .await .unwrap_or_default() @@ -928,34 +983,9 @@ async fn create_config_interfaces( } } - if config.storage.is_some() { - object_server.at(MANAGER_PATH, storage).await?; - } - - if let Some(config) = config.update_bios.as_ref() { - match config.is_valid(true).await { - Ok(true) => { - object_server.at(MANAGER_PATH, update_bios).await?; - } - Ok(false) => (), - Err(e) => error!("Failed to verify if BIOS update config is valid: {e}"), - } - } - - if let Some(config) = config.update_dock.as_ref() { - match config.is_valid(true).await { - Ok(true) => { - object_server.at(MANAGER_PATH, update_dock).await?; - } - Ok(false) => (), - Err(e) => error!("Failed to verify if dock update config is valid: {e}"), - } - } - Ok(()) } -#[allow(clippy::too_many_lines)] pub(crate) async fn create_interfaces( session: Connection, system: Connection, @@ -1007,7 +1037,8 @@ pub(crate) async fn create_interfaces( let object_server = session.object_server(); object_server.at(MANAGER_PATH, manager).await?; - create_config_interfaces(&proxy, object_server, &job_manager, tdp_manager).await?; + create_device_interfaces(&proxy, object_server, tdp_manager).await?; + create_platform_interfaces(&proxy, object_server, &system, &job_manager).await?; if device_type().await.unwrap_or_default() == DeviceType::SteamDeck { object_server.at(MANAGER_PATH, als).await?; @@ -1069,8 +1100,9 @@ mod test { use crate::hardware::test::fake_model; use crate::hardware::SteamDeckVariant; use crate::platform::{ - BatteryChargeLimitConfig, PerformanceProfileConfig, PlatformConfig, RangeConfig, - ResetConfig, ScriptConfig, ServiceConfig, StorageConfig, TdpLimitConfig, + BatteryChargeLimitConfig, DeviceConfig, FormatDeviceConfig, PerformanceProfileConfig, + PlatformConfig, RangeConfig, ResetConfig, ScriptConfig, ServiceConfig, StorageConfig, + TdpLimitConfig, }; use crate::power::TdpLimitingMethod; use crate::systemd::test::{MockManager, MockUnit}; @@ -1078,6 +1110,7 @@ mod test { use std::num::NonZeroU32; use std::os::unix::fs::PermissionsExt; + use std::path::PathBuf; use std::time::Duration; use tokio::fs::{set_permissions, write}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; @@ -1092,7 +1125,7 @@ mod test { rx_tdp: Option>, } - fn all_config() -> Option { + fn all_platform_config() -> Option { Some(PlatformConfig { factory_reset: Some(ResetConfig::default()), update_bios: Some(ScriptConfig::default()), @@ -1101,6 +1134,11 @@ mod test { fan_control: Some(ServiceConfig::Systemd(String::from( "jupiter-fan-control.service", ))), + }) + } + + fn all_device_config() -> Option { + Some(DeviceConfig { tdp_limit: Some(TdpLimitConfig { method: TdpLimitingMethod::GpuHwmon, range: Some(RangeConfig::new(3, 15)), @@ -1120,12 +1158,15 @@ mod test { }) } - async fn start(mut platform_config: Option) -> Result { + async fn start( + mut platform_config: Option, + device_config: Option, + ) -> Result { let mut handle = testing::start(); let (tx_ctx, _rx_ctx) = channel::(); let (tx_job, rx_job) = unbounded_channel::(); let (tx_tdp, rx_tdp) = { - if platform_config + if device_config .as_ref() .and_then(|config| config.tdp_limit.as_ref()) .is_some() @@ -1142,6 +1183,7 @@ mod test { } handle.test.platform_config.replace(platform_config); + handle.test.device_config.replace(device_config); let connection = handle.new_dbus().await?; connection.request_name("org.freedesktop.systemd1").await?; sleep(Duration::from_millis(10)).await; @@ -1192,7 +1234,7 @@ mod test { #[tokio::test] async fn interface_matches() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); let remote = testing::InterfaceIntrospection::from_remote::( &test.connection, @@ -1228,7 +1270,9 @@ mod test { #[tokio::test] async fn interface_matches_ambient_light_sensor1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!( test_interface_matches::(&test.connection) @@ -1239,7 +1283,9 @@ mod test { #[tokio::test] async fn interface_matches_battery_charge_limit() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!( test_interface_matches::(&test.connection) @@ -1250,7 +1296,9 @@ mod test { #[tokio::test] async fn interface_matches_cpu_scaling1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1259,7 +1307,9 @@ mod test { #[tokio::test] async fn interface_matches_factory_reset1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1268,14 +1318,52 @@ mod test { #[tokio::test] async fn interface_missing_factory_reset1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_all_factory_reset1() { + let mut config = all_platform_config().unwrap(); + config.factory_reset.as_mut().unwrap().all = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config), None).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_os_factory_reset1() { + let mut config = all_platform_config().unwrap(); + config.factory_reset.as_mut().unwrap().os = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config), None).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_user_factory_reset1() { + let mut config = all_platform_config().unwrap(); + config.factory_reset.as_mut().unwrap().user = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config), None).await.expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_matches_fan_control1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1284,14 +1372,16 @@ mod test { #[tokio::test] async fn interface_missing_fan_control1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_matches_gpu_performance_level1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!( test_interface_matches::(&test.connection) @@ -1302,7 +1392,9 @@ mod test { #[tokio::test] async fn interface_matches_gpu_power_profile1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1311,7 +1403,9 @@ mod test { #[tokio::test] async fn interface_matches_tdp_limit1() { - let mut test = start(all_config()).await.expect("start"); + let mut test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); let TdpManagerCommand::IsActive(reply) = test.rx_tdp.as_mut().unwrap().recv().await.unwrap() @@ -1328,14 +1422,16 @@ mod test { #[tokio::test] async fn interface_missing_tdp_limit1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_inactive_tdp_limit1() { - let mut test = start(all_config()).await.expect("start"); + let mut test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); let TdpManagerCommand::IsActive(reply) = test.rx_tdp.as_mut().unwrap().recv().await.unwrap() @@ -1350,7 +1446,9 @@ mod test { #[tokio::test] async fn interface_matches_hdmi_cec1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1359,7 +1457,9 @@ mod test { #[tokio::test] async fn interface_matches_low_power_mode1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1368,14 +1468,16 @@ mod test { #[tokio::test] async fn interface_missing_low_power_mode1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_matches_manager2() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1384,7 +1486,9 @@ mod test { #[tokio::test] async fn interface_matches_performance_profile1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!( test_interface_matches::(&test.connection) @@ -1395,14 +1499,16 @@ mod test { #[tokio::test] async fn interface_missing_performance_profile1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_matches_storage1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1411,14 +1517,43 @@ mod test { #[tokio::test] async fn interface_missing_storage1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_trim_storage1() { + let mut config = all_platform_config().unwrap(); + config.storage.as_mut().unwrap().trim_devices = ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }; + let test = start(Some(config), all_device_config()) + .await + .expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_format_storage1() { + let mut config = all_platform_config().unwrap(); + let mut format_config = FormatDeviceConfig::default(); + format_config.script = PathBuf::from("oxo"); + config.storage.as_mut().unwrap().format_device = format_config; + let test = start(Some(config), all_device_config()) + .await + .expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_matches_update_bios1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1427,14 +1562,30 @@ mod test { #[tokio::test] async fn interface_missing_update_bios1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_update_bios1() { + let mut config = all_platform_config().unwrap(); + config.update_bios = Some(ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }); + let test = start(Some(config), all_device_config()) + .await + .expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_matches_update_dock1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1443,14 +1594,30 @@ mod test { #[tokio::test] async fn interface_missing_update_dock1() { - let test = start(None).await.expect("start"); + let test = start(None, None).await.expect("start"); + + assert!(test_interface_missing::(&test.connection).await); + } + + #[tokio::test] + async fn interface_missing_invalid_update_dock1() { + let mut config = all_platform_config().unwrap(); + config.update_dock = Some(ScriptConfig { + script: PathBuf::from("oxo"), + script_args: Vec::new(), + }); + let test = start(Some(config), all_device_config()) + .await + .expect("start"); assert!(test_interface_missing::(&test.connection).await); } #[tokio::test] async fn interface_matches_wifi_power_management1() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!( test_interface_matches::(&test.connection) @@ -1461,7 +1628,9 @@ mod test { #[tokio::test] async fn interface_matches_wifi_debug() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await @@ -1470,7 +1639,9 @@ mod test { #[tokio::test] async fn interface_matches_wifi_debug_dump() { - let test = start(all_config()).await.expect("start"); + let test = start(all_platform_config(), all_device_config()) + .await + .expect("start"); assert!(test_interface_matches::(&test.connection) .await diff --git a/src/platform.rs b/src/platform.rs index ad24c78..f094d89 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -19,13 +19,19 @@ use tokio::fs::{metadata, read_to_string}; #[cfg(not(test))] use tokio::sync::OnceCell; use tokio::task::spawn_blocking; +use zbus::Connection; #[cfg(not(test))] use crate::hardware::{device_type, DeviceType}; +#[cfg(test)] +use crate::path; use crate::power::TdpLimitingMethod; +use crate::systemd::SystemdUnit; #[cfg(not(test))] -static CONFIG: OnceCell> = OnceCell::const_new(); +static PLATFORM_CONFIG: OnceCell> = OnceCell::const_new(); +#[cfg(not(test))] +static DEVICE_CONFIG: OnceCell> = OnceCell::const_new(); #[derive(Clone, Default, Deserialize, Debug)] #[serde(default)] @@ -35,6 +41,11 @@ pub(crate) struct PlatformConfig { pub update_dock: Option, pub storage: Option, pub fan_control: Option, +} + +#[derive(Clone, Default, Deserialize, Debug)] +#[serde(default)] +pub(crate) struct DeviceConfig { pub tdp_limit: Option, pub gpu_clocks: Option>, pub battery_charge_limit: Option, @@ -93,6 +104,14 @@ pub(crate) struct ResetConfig { pub user: ScriptConfig, } +impl ResetConfig { + pub(crate) async fn is_valid(&self, root: bool) -> Result { + Ok(self.all.is_valid(root).await? + && self.os.is_valid(root).await? + && self.user.is_valid(root).await?) + } +} + #[derive(Clone, Deserialize, Debug)] pub(crate) enum ServiceConfig { #[serde(rename = "systemd")] @@ -105,12 +124,33 @@ pub(crate) enum ServiceConfig { }, } +impl ServiceConfig { + pub(crate) async fn is_valid(&self, connection: &Connection, root: bool) -> Result { + match self { + ServiceConfig::Systemd(unit) => SystemdUnit::exists(connection, unit).await, + ServiceConfig::Script { + start, + stop, + status, + } => Ok(start.is_valid(root).await? + && stop.is_valid(root).await? + && status.is_valid(root).await?), + } + } +} + #[derive(Clone, Default, Deserialize, Debug)] pub(crate) struct StorageConfig { pub trim_devices: ScriptConfig, pub format_device: FormatDeviceConfig, } +impl StorageConfig { + pub(crate) async fn is_valid(&self, root: bool) -> Result { + Ok(self.trim_devices.is_valid(root).await? && self.format_device.is_valid(root).await?) + } +} + #[derive(Clone, Deserialize, Debug)] pub(crate) struct BatteryChargeLimitConfig { pub suggested_minimum_limit: Option, @@ -153,6 +193,36 @@ pub(crate) struct FormatDeviceConfig { pub no_validate_flag: Option, } +impl FormatDeviceConfig { + pub(crate) async fn is_valid(&self, root: bool) -> Result { + let meta = match metadata(&self.script).await { + Ok(meta) => meta, + Err(e) if [ErrorKind::NotFound, ErrorKind::PermissionDenied].contains(&e.kind()) => { + return Ok(false) + } + Err(e) => return Err(e.into()), + }; + if !meta.is_file() { + return Ok(false); + } + if root { + let script = self.script.clone(); + if !spawn_blocking(move || match access(&script, AccessFlags::X_OK) { + Ok(()) => Ok(true), + Err(Errno::ENOENT | Errno::EACCES) => Ok(false), + Err(e) => Err(e), + }) + .await?? + { + return Ok(false); + } + } else if (meta.mode() & 0o111) == 0 { + return Ok(false); + } + Ok(true) + } +} + impl RangeConfig { #[allow(unused)] pub(crate) fn new(min: T, max: T) -> RangeConfig { @@ -163,6 +233,47 @@ impl RangeConfig { impl PlatformConfig { #[cfg(not(test))] async fn load() -> Result> { + let config = read_to_string("/usr/share/steamos-manager/platform.toml").await?; + Ok(Some(toml::from_str(config.as_ref())?)) + } + + #[cfg(test)] + pub(crate) fn set_test_paths(&mut self) { + if let Some(ref mut factory_reset) = self.factory_reset { + if factory_reset.all.script.as_os_str().is_empty() { + factory_reset.all.script = path("exe"); + } + if factory_reset.os.script.as_os_str().is_empty() { + factory_reset.os.script = path("exe"); + } + if factory_reset.user.script.as_os_str().is_empty() { + factory_reset.user.script = path("exe"); + } + } + if let Some(ref mut storage) = self.storage { + if storage.trim_devices.script.as_os_str().is_empty() { + storage.trim_devices.script = path("exe"); + } + if storage.format_device.script.as_os_str().is_empty() { + storage.format_device.script = path("exe"); + } + } + if let Some(ref mut update_bios) = self.update_bios { + if update_bios.script.as_os_str().is_empty() { + update_bios.script = path("exe"); + } + } + if let Some(ref mut update_dock) = self.update_dock { + if update_dock.script.as_os_str().is_empty() { + update_dock.script = path("exe"); + } + } + } +} + +impl DeviceConfig { + #[cfg(not(test))] + async fn load() -> Result> { let platform = match device_type().await? { DeviceType::SteamDeck => "jupiter", DeviceType::LegionGo => "legion-go-series", @@ -173,25 +284,11 @@ impl PlatformConfig { _ => return Ok(None), }; let config = read_to_string(format!( - "/usr/share/steamos-manager/platforms/{platform}.toml" + "/usr/share/steamos-manager/devices/{platform}.toml" )) .await?; Ok(Some(toml::from_str(config.as_ref())?)) } - - #[cfg(test)] - pub(crate) fn set_test_paths(&mut self) { - if let Some(ref mut update_bios) = self.update_bios { - if update_bios.script.as_os_str().is_empty() { - update_bios.script = crate::path("exe"); - } - } - if let Some(ref mut update_dock) = self.update_dock { - if update_dock.script.as_os_str().is_empty() { - update_dock.script = crate::path("exe"); - } - } - } } fn de_tdp_limiter_method<'de, D>(deserializer: D) -> Result @@ -206,7 +303,12 @@ where #[cfg(not(test))] pub(crate) async fn platform_config() -> Result<&'static Option> { - CONFIG.get_or_try_init(PlatformConfig::load).await + PLATFORM_CONFIG.get_or_try_init(PlatformConfig::load).await +} + +#[cfg(not(test))] +pub(crate) async fn device_config() -> Result<&'static Option> { + DEVICE_CONFIG.get_or_try_init(DeviceConfig::load).await } #[cfg(test)] @@ -216,6 +318,13 @@ pub(crate) async fn platform_config() -> Result> { Ok(config) } +#[cfg(test)] +pub(crate) async fn device_config() -> Result> { + let test = crate::testing::current(); + let config = test.device_config.borrow().clone(); + Ok(config) +} + #[cfg(test)] mod test { use super::*; @@ -329,7 +438,7 @@ mod test { #[tokio::test] async fn jupiter_valid() { - let config = read_to_string("data/platforms/jupiter.toml") + let config = read_to_string("data/devices/jupiter.toml") .await .expect("read_to_string"); let res = toml::from_str::(config.as_ref()); diff --git a/src/power.rs b/src/power.rs index d026f8d..9a09974 100644 --- a/src/power.rs +++ b/src/power.rs @@ -30,7 +30,7 @@ use zbus::Connection; use crate::hardware::{device_type, DeviceType}; use crate::manager::root::RootManagerProxy; use crate::manager::user::{TdpLimit1, MANAGER_PATH}; -use crate::platform::platform_config; +use crate::platform::device_config; use crate::Service; use crate::{path, write_synced}; @@ -128,7 +128,7 @@ pub(crate) trait TdpLimitManager: Send + Sync { } pub(crate) async fn tdp_limit_manager() -> Result> { - let config = platform_config().await?; + let config = device_config().await?; let config = config .as_ref() .and_then(|config| config.tdp_limit.as_ref()) @@ -340,7 +340,7 @@ pub(crate) async fn set_cpu_scaling_governor(governor: CPUScalingGovernor) -> Re } pub(crate) async fn get_gpu_clocks_range() -> Result> { - if let Some(range) = platform_config() + if let Some(range) = device_config() .await? .as_ref() .and_then(|config| config.gpu_clocks) @@ -490,7 +490,7 @@ impl TdpLimitManager for GpuHwmonTdpLimitManager { } async fn get_tdp_limit_range(&self) -> Result> { - let config = platform_config().await?; + let config = device_config().await?; let config = config .as_ref() .and_then(|config| config.tdp_limit.as_ref()) @@ -578,7 +578,7 @@ impl TdpLimitManager for FirmwareAttributeLimitManager { let Some(ref performance_profile) = self.performance_profile else { return Ok(true); }; - let config = platform_config().await?; + let config = device_config().await?; if let Some(config) = config .as_ref() .and_then(|config| config.performance_profile.as_ref()) @@ -591,7 +591,7 @@ impl TdpLimitManager for FirmwareAttributeLimitManager { } pub(crate) async fn get_max_charge_level() -> Result { - let config = platform_config().await?; + let config = device_config().await?; let config = config .as_ref() .and_then(|config| config.battery_charge_limit.as_ref()) @@ -609,7 +609,7 @@ pub(crate) async fn get_max_charge_level() -> Result { pub(crate) async fn set_max_charge_level(limit: i32) -> Result<()> { ensure!((0..=100).contains(&limit), "Invalid limit"); let data = limit.to_string(); - let config = platform_config().await?; + let config = device_config().await?; let config = config .as_ref() .and_then(|config| config.battery_charge_limit.as_ref()) @@ -654,7 +654,7 @@ impl TdpManagerService { system: &Connection, session: &Connection, ) -> Result { - let config = platform_config().await?; + let config = device_config().await?; let config = config .as_ref() .and_then(|config| config.tdp_limit.as_ref()) @@ -858,8 +858,8 @@ pub(crate) mod test { use crate::hardware::test::fake_model; use crate::hardware::SteamDeckVariant; use crate::platform::{ - BatteryChargeLimitConfig, FirmwareAttributeConfig, PerformanceProfileConfig, - PlatformConfig, RangeConfig, TdpLimitConfig, + BatteryChargeLimitConfig, DeviceConfig, FirmwareAttributeConfig, PerformanceProfileConfig, + RangeConfig, TdpLimitConfig, }; use crate::{enum_roundtrip, testing}; use anyhow::anyhow; @@ -1041,14 +1041,14 @@ CCLK_RANGE in Core0: async fn test_gpu_hwmon_get_tdp_limit() { let handle = testing::start(); - let mut platform_config = PlatformConfig::default(); - platform_config.tdp_limit = Some(TdpLimitConfig { + let mut config = DeviceConfig::default(); + config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::GpuHwmon, range: Some(RangeConfig { min: 3, max: 15 }), download_mode_limit: None, firmware_attribute: None, }); - handle.test.platform_config.replace(Some(platform_config)); + handle.test.device_config.replace(Some(config)); let manager = tdp_limit_manager().await.unwrap(); setup().await.expect("setup"); @@ -1066,14 +1066,14 @@ CCLK_RANGE in Core0: async fn test_gpu_hwmon_set_tdp_limit() { let handle = testing::start(); - let mut platform_config = PlatformConfig::default(); - platform_config.tdp_limit = Some(TdpLimitConfig { + let mut config = DeviceConfig::default(); + config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::GpuHwmon, range: Some(RangeConfig { min: 3, max: 15 }), download_mode_limit: None, firmware_attribute: None, }); - handle.test.platform_config.replace(Some(platform_config)); + handle.test.device_config.replace(Some(config)); let manager = tdp_limit_manager().await.unwrap(); assert_eq!( @@ -1550,13 +1550,13 @@ CCLK_RANGE in Core0: async fn read_max_charge_level() { let handle = testing::start(); - let mut platform_config = PlatformConfig::default(); - platform_config.battery_charge_limit = Some(BatteryChargeLimitConfig { + let mut config = DeviceConfig::default(); + config.battery_charge_limit = Some(BatteryChargeLimitConfig { suggested_minimum_limit: Some(10), hwmon_name: String::from("steamdeck_hwmon"), attribute: String::from("max_battery_charge_level"), }); - handle.test.platform_config.replace(Some(platform_config)); + handle.test.device_config.replace(Some(config)); let base = path(HWMON_PREFIX).join("hwmon6"); create_dir_all(&base).await.expect("create_dir_all"); @@ -1650,14 +1650,14 @@ CCLK_RANGE in Core0: let iface = MockTdpLimit { queue: reply_tx }; - let mut platform_config = PlatformConfig::default(); - platform_config.tdp_limit = Some(TdpLimitConfig { + let mut config = DeviceConfig::default(); + config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::GpuHwmon, range: Some(RangeConfig { min: 3, max: 15 }), download_mode_limit: NonZeroU32::new(6), firmware_attribute: None, }); - h.test.platform_config.replace(Some(platform_config)); + h.test.device_config.replace(Some(config)); let manager = tdp_limit_manager().await.unwrap(); connection @@ -1746,14 +1746,14 @@ CCLK_RANGE in Core0: let iface = MockTdpLimit { queue: reply_tx }; - let mut platform_config = PlatformConfig::default(); - platform_config.tdp_limit = Some(TdpLimitConfig { + let mut config = DeviceConfig::default(); + config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::GpuHwmon, range: Some(RangeConfig { min: 3, max: 15 }), download_mode_limit: None, firmware_attribute: None, }); - h.test.platform_config.replace(Some(platform_config)); + h.test.device_config.replace(Some(config)); let manager = tdp_limit_manager().await.unwrap(); connection @@ -1808,12 +1808,12 @@ CCLK_RANGE in Core0: let h = testing::start(); setup().await.expect("setup"); - let mut platform_config = PlatformConfig::default(); - platform_config.performance_profile = Some(PerformanceProfileConfig { + let mut config = DeviceConfig::default(); + config.performance_profile = Some(PerformanceProfileConfig { platform_profile_name: String::from("platform-profile0"), suggested_default: String::from("custom"), }); - platform_config.tdp_limit = Some(TdpLimitConfig { + config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::FirmwareAttribute, range: Some(RangeConfig { min: 3, max: 15 }), download_mode_limit: None, @@ -1822,7 +1822,7 @@ CCLK_RANGE in Core0: performance_profile: Some(String::from("custom")), }), }); - h.test.platform_config.replace(Some(platform_config)); + h.test.device_config.replace(Some(config)); let attributes_base = path(FirmwareAttributeLimitManager::PREFIX) .join("tdp0") @@ -1903,8 +1903,8 @@ CCLK_RANGE in Core0: let h = testing::start(); setup().await.expect("setup"); - let mut platform_config = PlatformConfig::default(); - platform_config.tdp_limit = Some(TdpLimitConfig { + let mut config = DeviceConfig::default(); + config.tdp_limit = Some(TdpLimitConfig { method: TdpLimitingMethod::FirmwareAttribute, range: Some(RangeConfig { min: 3, max: 15 }), download_mode_limit: None, @@ -1913,7 +1913,7 @@ CCLK_RANGE in Core0: performance_profile: None, }), }); - h.test.platform_config.replace(Some(platform_config)); + h.test.device_config.replace(Some(config)); let attributes_base = path(FirmwareAttributeLimitManager::PREFIX) .join("tdp0") diff --git a/src/systemd.rs b/src/systemd.rs index 879b836..d18cfb7 100644 --- a/src/systemd.rs +++ b/src/systemd.rs @@ -11,7 +11,7 @@ use std::str::FromStr; use strum::{Display, EnumString}; use zbus::proxy::CacheProperties; use zbus::zvariant::OwnedObjectPath; -use zbus::Connection; +use zbus::{self, Connection}; #[zbus::proxy( interface = "org.freedesktop.systemd1.Unit", @@ -19,13 +19,13 @@ use zbus::Connection; )] trait SystemdUnit { #[zbus(property)] - fn active_state(&self) -> Result; + fn active_state(&self) -> zbus::Result; #[zbus(property)] - fn unit_file_state(&self) -> Result; + fn unit_file_state(&self) -> zbus::Result; - async fn restart(&self, mode: &str) -> Result; - async fn start(&self, mode: &str) -> Result; - async fn stop(&self, mode: &str) -> Result; + async fn restart(&self, mode: &str) -> zbus::Result; + async fn start(&self, mode: &str) -> zbus::Result; + async fn stop(&self, mode: &str) -> zbus::Result; } #[zbus::proxy( @@ -40,28 +40,30 @@ trait SystemdManager { files: &[&str], runtime: bool, force: bool, - ) -> Result<(bool, Vec<(String, String, String)>)>; + ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; async fn disable_unit_files( &self, files: &[&str], runtime: bool, - ) -> Result>; + ) -> zbus::Result>; async fn mask_unit_files( &self, files: &[&str], runtime: bool, force: bool, - ) -> Result>; + ) -> zbus::Result>; async fn unmask_unit_files( &self, files: &[&str], runtime: bool, - ) -> Result>; + ) -> zbus::Result>; - async fn reload(&self) -> Result<()>; + async fn reload(&self) -> zbus::Result<()>; + + async fn get_unit(&self, name: &str) -> zbus::Result; } #[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)] @@ -86,6 +88,17 @@ pub async fn daemon_reload(connection: &Connection) -> Result<()> { } impl<'dbus> SystemdUnit<'dbus> { + pub async fn exists(connection: &Connection, name: &str) -> Result { + let manager = SystemdManagerProxy::new(connection).await?; + // This is kinda hacky, but zbus makes it pretty hard to get the proper error name + let expected_error = format!("Unit {name} not loaded."); + match manager.get_unit(name).await { + Ok(_) => Ok(true), + Err(zbus::Error::Failure(message)) if message == expected_error => Ok(false), + Err(e) => Err(e.into()), + } + } + pub async fn new(connection: Connection, name: &str) -> Result> { let path = PathBuf::from("/org/freedesktop/systemd1/unit").join(escape(name)); let path = String::from(path.to_str().ok_or(anyhow!("Unit name {name} invalid"))?); @@ -283,7 +296,6 @@ pub mod test { .interface::<_, MockUnit>(path.to_string_lossy()) .await; if let Ok(mock_unit) = mock_unit { - dbg!(); mock_unit.get_mut().await.unit_file = String::from("enabled"); } } @@ -359,6 +371,14 @@ pub mod test { async fn reload(&self) -> fdo::Result<()> { Ok(()) } + + async fn get_unit(&mut self, unit: &str) -> fdo::Result { + Ok( + ObjectPath::try_from(format!("/org/freedesktop/systemd1/unit/{}", escape(unit))) + .unwrap() + .into(), + ) + } } #[tokio::test] diff --git a/src/testing.rs b/src/testing.rs index 44ca9ee..edf376b 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -23,7 +23,7 @@ use zbus::zvariant::ObjectPath; use zbus::Address; use zbus_xml::{Method, Node, Property, Signal}; -use crate::platform::PlatformConfig; +use crate::platform::{DeviceConfig, PlatformConfig}; thread_local! { static TEST: RefCell>> = const { RefCell::new(None) }; @@ -70,6 +70,7 @@ pub fn start() -> TestHandle { mock_dbus: Cell::new(None), dbus_address: Mutex::new(None), platform_config: RefCell::new(None), + device_config: RefCell::new(None), }); *lock.borrow_mut() = Some(test.clone()); TestHandle { test } @@ -103,6 +104,7 @@ pub struct Test { pub mock_dbus: Cell>, pub dbus_address: Mutex>, pub platform_config: RefCell>, + pub device_config: RefCell>, } pub struct TestHandle {