platform: Bring up platform configurations with script replacement, Steam Deck only for now

This commit is contained in:
Vicki Pfau 2024-06-25 21:47:59 -07:00
parent d465bc2750
commit 77975d9308
6 changed files with 293 additions and 52 deletions

View file

@ -0,0 +1,18 @@
[factory_reset]
script = "/usr/bin/steamos-factory-reset-config"
[update_bios]
script = "/usr/bin/jupiter-biosupdate"
script_args = ["--auto"]
[update_dock]
script = "/usr/lib/jupiter-dock-updater/jupiter-dock-updater.sh"
[storage.trim_devices]
script = "/usr/lib/hwsupport/trim-devices.sh"
[storage.format_device]
script = "/usr/lib/hwsupport/format-device.sh"
label_flag = "--label"
device_flag = "--device"
no_validate_flag = "--skip-validation"

View file

@ -17,6 +17,7 @@ mod ds_inhibit;
mod error;
mod job;
mod manager;
mod platform;
mod process;
mod sls;
mod systemd;

View file

@ -21,6 +21,7 @@ use crate::daemon::DaemonCommand;
use crate::error::{to_zbus_error, to_zbus_fdo_error};
use crate::hardware::{variant, FanControl, FanControlState, HardwareVariant};
use crate::job::JobManager;
use crate::platform::platform_config;
use crate::power::{
set_cpu_scaling_governor, set_gpu_clocks, set_gpu_performance_level, set_gpu_power_profile,
set_tdp_limit, CPUScalingGovernor, GPUPerformanceLevel, GPUPowerProfile,
@ -32,6 +33,25 @@ use crate::wifi::{
};
use crate::API_VERSION;
macro_rules! with_platform_config {
($config:ident = $field:ident ($name:literal) => $eval:expr) => {
if let Some(config) = platform_config()
.await
.map_err(to_zbus_fdo_error)?
.as_ref()
.and_then(|config| config.$field.as_ref())
{
let $config = config;
$eval
} else {
Err(fdo::Error::NotSupported(format!(
"{} is not supported on this platform",
$name
)))
}
};
}
#[derive(PartialEq, Debug, Copy, Clone)]
#[repr(u32)]
enum PrepareFactoryReset {
@ -67,12 +87,16 @@ const ALS_INTEGRATION_PATH: &str = "/sys/devices/platform/AMDI0010:00/i2c-0/i2c-
#[interface(name = "com.steampowered.SteamOSManager1.RootManager")]
impl SteamOSManager {
async fn prepare_factory_reset(&self) -> u32 {
async fn prepare_factory_reset(&self) -> fdo::Result<u32> {
// Run steamos factory reset script and return true on success
let res = run_script("/usr/bin/steamos-factory-reset-config", &[] as &[&OsStr]).await;
match res {
with_platform_config! {
config = factory_reset ("PrepareFactoryReset") => {
let res = run_script(&config.script, &config.script_args).await;
Ok(match res {
Ok(_) => PrepareFactoryReset::RebootRequired as u32,
Err(_) => PrepareFactoryReset::Unknown as u32,
})
}
}
}
@ -139,32 +163,36 @@ impl SteamOSManager {
async fn update_bios(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> {
// Update the bios as needed
with_platform_config! {
config = update_bios ("UpdateBios") => {
self.job_manager
.run_process("/usr/bin/jupiter-biosupdate", &["--auto"], "updating BIOS")
.run_process(&config.script, &config.script_args, "updating BIOS")
.await
}
}
}
async fn update_dock(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> {
// Update the dock firmware as needed
with_platform_config! {
config = update_dock ("UpdateDock") => {
self.job_manager
.run_process(
"/usr/lib/jupiter-dock-updater/jupiter-dock-updater.sh",
&[] as &[String; 0],
"updating dock",
)
.run_process(&config.script, &config.script_args, "updating dock")
.await
}
}
}
async fn trim_devices(&mut self) -> fdo::Result<zvariant::OwnedObjectPath> {
// Run steamos-trim-devices script
with_platform_config! {
config = storage ("TrimDevices") => {
self.job_manager
.run_process(
"/usr/lib/hwsupport/trim-devices.sh",
&[] as &[String; 0],
"trimming devices",
)
.run_process(&config.trim_devices.script, config.trim_devices.script_args.as_ref(), "trimming devices")
.await
}
}
}
async fn format_device(
&mut self,
@ -172,18 +200,34 @@ impl SteamOSManager {
label: &str,
validate: bool,
) -> fdo::Result<zvariant::OwnedObjectPath> {
let mut args = vec!["--label", label, "--device", device];
if !validate {
args.push("--skip-validation");
with_platform_config! {
config = storage ("FormatDevice") => {
let config = &config.format_device;
let mut args: Vec<&OsStr> = config.script_args.iter().map(AsRef::as_ref).collect();
args.extend_from_slice(&[OsStr::new(config.label_flag.as_str()), OsStr::new(label)]);
match (validate, &config.validate_flag, &config.no_validate_flag) {
(true, Some(validate_flag), _) => args.push(OsStr::new(validate_flag)),
(false, _, Some(no_validate_flag)) => args.push(OsStr::new(no_validate_flag)),
_ => (),
}
if let Some(device_flag) = &config.device_flag {
args.push(OsStr::new(device_flag));
}
args.push(OsStr::new(device));
self.job_manager
.run_process(
"/usr/lib/hwsupport/format-device.sh",
args.as_ref(),
&config.script,
&args,
format!("formatting {device}").as_str(),
)
.await
}
}
}
async fn set_gpu_power_profile(&self, value: &str) -> fdo::Result<()> {
let profile = GPUPowerProfile::try_from(value).map_err(to_zbus_fdo_error)?;
@ -326,6 +370,7 @@ mod test {
use super::*;
use crate::daemon::channel;
use crate::daemon::root::RootContext;
use crate::platform::{PlatformConfig, ScriptConfig};
use crate::power::test::{format_clocks, read_clocks};
use crate::power::{self, get_gpu_performance_level};
use crate::process::test::{code, exit, ok};
@ -379,6 +424,11 @@ mod test {
#[tokio::test]
async fn prepare_factory_reset() {
let test = start().await.expect("start");
let mut config = PlatformConfig::default();
config.factory_reset = Some(ScriptConfig::default());
test.h.test.platform_config.replace(Some(config));
let name = test.connection.unique_name().unwrap();
let proxy = PrepareFactoryResetProxy::new(&test.connection, name.clone())
.await

View file

@ -21,6 +21,7 @@ use crate::daemon::DaemonCommand;
use crate::error::{to_zbus_error, to_zbus_fdo_error, zbus_to_zbus_fdo};
use crate::hardware::{check_support, is_deck, HardwareCurrentlySupported};
use crate::job::JobManagerCommand;
use crate::platform::platform_config;
use crate::power::{
get_available_cpu_scaling_governors, get_available_gpu_performance_levels,
get_available_gpu_power_profiles, get_cpu_scaling_governor, get_gpu_clocks,
@ -577,6 +578,7 @@ pub(crate) async fn create_interfaces(
proxy: proxy.clone(),
};
let config = platform_config().await?;
let object_server = session.object_server();
object_server.at(MANAGER_PATH, manager).await?;
@ -585,7 +587,14 @@ pub(crate) async fn create_interfaces(
}
object_server.at(MANAGER_PATH, cpu_scaling).await?;
if config
.as_ref()
.is_some_and(|config| config.factory_reset.is_some())
{
object_server.at(MANAGER_PATH, factory_reset).await?;
}
object_server.at(MANAGER_PATH, fan_control).await?;
if !get_available_gpu_performance_levels()
@ -612,9 +621,28 @@ pub(crate) async fn create_interfaces(
object_server.at(MANAGER_PATH, hdmi_cec).await?;
object_server.at(MANAGER_PATH, manager2).await?;
if config
.as_ref()
.is_some_and(|config| config.storage.is_some())
{
object_server.at(MANAGER_PATH, storage).await?;
}
if config
.as_ref()
.is_some_and(|config| config.update_bios.is_some())
{
object_server.at(MANAGER_PATH, update_bios).await?;
}
if config
.as_ref()
.is_some_and(|config| config.update_dock.is_some())
{
object_server.at(MANAGER_PATH, update_dock).await?;
}
object_server.at(MANAGER_PATH, wifi_debug).await?;
object_server
.at(MANAGER_PATH, wifi_power_management)
@ -630,6 +658,7 @@ mod test {
use crate::daemon::user::UserContext;
use crate::hardware::test::fake_model;
use crate::hardware::HardwareVariant;
use crate::platform::{PlatformConfig, ScriptConfig, StorageConfig};
use crate::{power, testing};
use std::time::Duration;
@ -642,10 +671,21 @@ mod test {
connection: Connection,
}
async fn start() -> Result<TestHandle> {
fn all_config() -> Option<PlatformConfig> {
Some(PlatformConfig {
factory_reset: Some(ScriptConfig::default()),
update_bios: Some(ScriptConfig::default()),
update_dock: Some(ScriptConfig::default()),
storage: Some(StorageConfig::default()),
})
}
async fn start(platform_config: Option<PlatformConfig>) -> Result<TestHandle> {
let mut handle = testing::start();
let (tx_ctx, _rx_ctx) = channel::<UserContext>();
let (tx_job, _rx_job) = unbounded_channel::<JobManagerCommand>();
handle.test.platform_config.replace(platform_config);
let connection = handle.new_dbus().await?;
fake_model(HardwareVariant::Jupiter).await?;
power::test::create_nodes().await?;
@ -661,7 +701,7 @@ mod test {
#[tokio::test]
async fn interface_matches() {
let test = start().await.expect("start");
let test = start(None).await.expect("start");
let remote = testing::InterfaceIntrospection::from_remote::<SteamOSManager, _>(
&test.connection,
@ -689,9 +729,15 @@ mod test {
Ok(remote.compare(&local))
}
async fn test_interface_missing<I: Interface>(connection: &Connection) -> bool {
let remote =
testing::InterfaceIntrospection::from_remote::<I, _>(connection, MANAGER_PATH).await;
return remote.is_err();
}
#[tokio::test]
async fn interface_matches_ambient_light_sensor1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(
test_interface_matches::<AmbientLightSensor1>(&test.connection)
@ -702,7 +748,7 @@ mod test {
#[tokio::test]
async fn interface_matches_cpu_scaling1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<CpuScaling1>(&test.connection)
.await
@ -711,16 +757,23 @@ mod test {
#[tokio::test]
async fn interface_matches_factory_reset1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<FactoryReset1>(&test.connection)
.await
.unwrap());
}
#[tokio::test]
async fn interface_missing_factory_reset1() {
let test = start(None).await.expect("start");
assert!(test_interface_missing::<FactoryReset1>(&test.connection).await);
}
#[tokio::test]
async fn interface_matches_fan_control1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<FanControl1>(&test.connection)
.await
@ -729,7 +782,7 @@ mod test {
#[tokio::test]
async fn interface_matches_gpu_performance_level1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(
test_interface_matches::<GpuPerformanceLevel1>(&test.connection)
@ -740,7 +793,7 @@ mod test {
#[tokio::test]
async fn interface_matches_gpu_power_profile1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<GpuPowerProfile1>(&test.connection)
.await
@ -749,7 +802,7 @@ mod test {
#[tokio::test]
async fn interface_matches_gpu_tdp_limit1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<GpuTdpLimit1>(&test.connection)
.await
@ -758,7 +811,7 @@ mod test {
#[tokio::test]
async fn interface_matches_hdmi_cec1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<HdmiCec1>(&test.connection)
.await
@ -767,7 +820,7 @@ mod test {
#[tokio::test]
async fn interface_matches_manager2() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<Manager2>(&test.connection)
.await
@ -776,34 +829,55 @@ mod test {
#[tokio::test]
async fn interface_matches_storage1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<Storage1>(&test.connection)
.await
.unwrap());
}
#[tokio::test]
async fn interface_missing_storage1() {
let test = start(None).await.expect("start");
assert!(test_interface_missing::<Storage1>(&test.connection).await);
}
#[tokio::test]
async fn interface_matches_update_bios1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<UpdateBios1>(&test.connection)
.await
.unwrap());
}
#[tokio::test]
async fn interface_missing_update_bios1() {
let test = start(None).await.expect("start");
assert!(test_interface_missing::<UpdateBios1>(&test.connection).await);
}
#[tokio::test]
async fn interface_matches_update_dock1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<UpdateDock1>(&test.connection)
.await
.unwrap());
}
#[tokio::test]
async fn interface_missing_update_dock1() {
let test = start(None).await.expect("start");
assert!(test_interface_missing::<UpdateDock1>(&test.connection).await);
}
#[tokio::test]
async fn interface_matches_wifi_power_management1() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(
test_interface_matches::<WifiPowerManagement1>(&test.connection)
@ -814,7 +888,7 @@ mod test {
#[tokio::test]
async fn interface_matches_wifi_debug() {
let test = start().await.expect("start");
let test = start(all_config()).await.expect("start");
assert!(test_interface_matches::<WifiDebug1>(&test.connection)
.await

94
src/platform.rs Normal file
View file

@ -0,0 +1,94 @@
/*
* Copyright © 2023 Collabora Ltd.
* Copyright © 2024 Valve Software
*
* SPDX-License-Identifier: MIT
*/
use anyhow::Result;
use serde::Deserialize;
use std::path::PathBuf;
use tokio::fs::read_to_string;
#[cfg(not(test))]
use tokio::sync::OnceCell;
#[cfg(not(test))]
use crate::hardware::is_deck;
#[cfg(not(test))]
static CONFIG: OnceCell<Option<PlatformConfig>> = OnceCell::const_new();
#[derive(Clone, Default, Deserialize, Debug)]
#[serde(default)]
pub(crate) struct PlatformConfig {
pub factory_reset: Option<ScriptConfig>,
pub update_bios: Option<ScriptConfig>,
pub update_dock: Option<ScriptConfig>,
pub storage: Option<StorageConfig>,
}
#[derive(Clone, Default, Deserialize, Debug)]
pub(crate) struct ScriptConfig {
pub script: PathBuf,
#[serde(default)]
pub script_args: Vec<String>,
}
#[derive(Clone, Default, Deserialize, Debug)]
pub(crate) struct StorageConfig {
pub trim_devices: ScriptConfig,
pub format_device: FormatDeviceConfig,
}
#[derive(Clone, Default, Deserialize, Debug)]
pub(crate) struct FormatDeviceConfig {
pub script: PathBuf,
#[serde(default)]
pub script_args: Vec<String>,
pub label_flag: String,
#[serde(default)]
pub device_flag: Option<String>,
#[serde(default)]
pub validate_flag: Option<String>,
#[serde(default)]
pub no_validate_flag: Option<String>,
}
impl PlatformConfig {
#[cfg(not(test))]
async fn load() -> Result<Option<PlatformConfig>> {
if !is_deck().await? {
// Non-Steam Deck platforms are not yet supported
return Ok(None);
}
let config = read_to_string("/usr/share/steamos-manager/platforms/jupiter.toml").await?;
Ok(Some(toml::from_str(config.as_ref())?))
}
}
#[cfg(not(test))]
pub(crate) async fn platform_config() -> Result<&'static Option<PlatformConfig>> {
CONFIG.get_or_try_init(PlatformConfig::load).await
}
#[cfg(test)]
pub(crate) async fn platform_config() -> Result<Option<PlatformConfig>> {
let test = crate::testing::current();
let config = test.platform_config.borrow().clone();
Ok(config)
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn jupiter_valid() {
let config = read_to_string("data/platforms/jupiter.toml")
.await
.expect("read_to_string");
let res = toml::from_str::<PlatformConfig>(config.as_ref());
assert!(res.is_ok(), "{res:?}");
}
}

View file

@ -22,6 +22,8 @@ use zbus::zvariant::ObjectPath;
use zbus::{Address, Connection, ConnectionBuilder, Interface};
use zbus_xml::{Method, Node, Property};
use crate::platform::PlatformConfig;
thread_local! {
static TEST: RefCell<Option<Rc<Test>>> = RefCell::new(None);
}
@ -66,6 +68,7 @@ pub fn start() -> TestHandle {
process_cb: Cell::new(|_, _| Err(anyhow!("No current process_cb"))),
mock_dbus: Cell::new(None),
dbus_address: Mutex::new(None),
platform_config: RefCell::new(None),
});
*lock.borrow_mut() = Some(test.clone());
TestHandle { test }
@ -98,6 +101,7 @@ pub struct Test {
pub process_cb: Cell<fn(&OsStr, &[&OsStr]) -> Result<(i32, String)>>,
pub mock_dbus: Cell<Option<MockDBus>>,
pub dbus_address: Mutex<Option<Address>>,
pub platform_config: RefCell<Option<PlatformConfig>>,
}
pub struct TestHandle {