Merge branch 'work/sitter/sessionmgmt' into 'master'

implement SessionManagement1

See merge request holo/steamos-manager!9
This commit is contained in:
Harald Sitter 2025-07-04 03:50:06 +00:00
commit f85317b43b
10 changed files with 445 additions and 3 deletions

1
Cargo.lock generated
View file

@ -1064,6 +1064,7 @@ dependencies = [
"nix 0.30.1", "nix 0.30.1",
"num_enum", "num_enum",
"regex", "regex",
"rust-ini",
"serde", "serde",
"serde_json", "serde_json",
"strum", "strum",

View file

@ -21,6 +21,7 @@ libc = "0.2"
nix = { version = "0.30", default-features = false, features = ["fs", "poll", "signal", "time"] } nix = { version = "0.30", default-features = false, features = ["fs", "poll", "signal", "time"] }
num_enum = "0.7" num_enum = "0.7"
regex = "1" regex = "1"
rust-ini = "0.21"
serde = { version = "1.0", default-features = false, features = ["derive"] } serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
strum = { version = "0.27", features = ["derive"] } strum = { version = "0.27", features = ["derive"] }

View file

@ -7,6 +7,7 @@
Copyright © 2023 Collabora Ltd. Copyright © 2023 Collabora Ltd.
Copyright © 2024 Igalia S.L. Copyright © 2024 Igalia S.L.
Copyright © 2024 Valve Corporation. Copyright © 2024 Valve Corporation.
Copyright © 2025 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
@ -681,4 +682,63 @@
</interface> </interface>
<!--
com.steampowered.SteamOSManager1.SessionManagement1
@short_description: Interface to manage session types.
Valid values include all sessions available in /usr/share/xsessions and /usr/share/wayland-sessions.
-->
<interface name="com.steampowered.SteamOSManager1.SessionManagement1">
<!--
DefaultSessionType:
The default session type to use when booting the system.
Valid values include all sessions available in /usr/share/xsessions and /usr/share/wayland-sessions.
-->
<property name="DefaultSessionType" type="s" access="readwrite"/>
<!--
DefaultDesktopSessionType:
The default session type to use when starting desktop mode.
Valid values include all sessions available in /usr/share/xsessions and /usr/share/wayland-sessions.
-->
<property name="DefaultDesktopSessionType" type="s" access="readwrite"/>
<!--
SwitchToSession:
Switch to the provided session type. This will change the session type temporarily and stop the running
session. The login manager will then log into the new session type.
Note that resetting this temporary type needs to happen as part of the login. steamos-manager will not
automatically reset the session type on its own!
Valid values include all sessions available in /usr/share/xsessions and /usr/share/wayland-sessions.
-->
<method name="SwitchToSession">
<arg type="s" name="type" direction="in"/>
</method>
<!--
SwitchToGameMode:
Convenience method to switch to the game mode session type.
-->
<method name="SwitchToGameMode">
</method>
<!--
SwitchToDesktopMode:
Convenience method to switch to the desktop mode session type (adheres to DefaultDesktopSessionType)
-->
<method name="SwitchToDesktopMode">
</method>
</interface>
</node> </node>

View file

@ -1,6 +1,7 @@
/* /*
* Copyright © 2023 Collabora Ltd. * Copyright © 2023 Collabora Ltd.
* Copyright © 2024 Valve Software * Copyright © 2024 Valve Software
* Copyright © 2025 Harald Sitter <sitter@kde.org>
* *
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -17,9 +18,9 @@ use steamos_manager::power::{CPUScalingGovernor, GPUPerformanceLevel, GPUPowerPr
use steamos_manager::proxy::{ use steamos_manager::proxy::{
AmbientLightSensor1Proxy, BatteryChargeLimit1Proxy, CpuScaling1Proxy, FactoryReset1Proxy, AmbientLightSensor1Proxy, BatteryChargeLimit1Proxy, CpuScaling1Proxy, FactoryReset1Proxy,
FanControl1Proxy, GpuPerformanceLevel1Proxy, GpuPowerProfile1Proxy, HdmiCec1Proxy, FanControl1Proxy, GpuPerformanceLevel1Proxy, GpuPowerProfile1Proxy, HdmiCec1Proxy,
LowPowerMode1Proxy, Manager2Proxy, PerformanceProfile1Proxy, ScreenReader0Proxy, Storage1Proxy, LowPowerMode1Proxy, Manager2Proxy, PerformanceProfile1Proxy, ScreenReader0Proxy,
TdpLimit1Proxy, UpdateBios1Proxy, UpdateDock1Proxy, WifiDebug1Proxy, WifiDebugDump1Proxy, SessionManagement1Proxy, Storage1Proxy, TdpLimit1Proxy, UpdateBios1Proxy, UpdateDock1Proxy,
WifiPowerManagement1Proxy, WifiDebug1Proxy, WifiDebugDump1Proxy, WifiPowerManagement1Proxy,
}; };
use steamos_manager::screenreader::{ScreenReaderAction, ScreenReaderMode}; use steamos_manager::screenreader::{ScreenReaderAction, ScreenReaderMode};
use steamos_manager::wifi::{WifiBackend, WifiDebugMode, WifiPowerManagement}; use steamos_manager::wifi::{WifiBackend, WifiDebugMode, WifiPowerManagement};
@ -269,6 +270,36 @@ enum Commands {
/// `toggle_mode` /// `toggle_mode`
action: ScreenReaderAction, action: ScreenReaderAction,
}, },
/// Switch to desktop mode
SwitchToDesktopMode,
/// Switch to game mode
SwitchToGameMode,
/// Switch to a specific session type
SwitchToSession {
/// The session type to switch to, e.g. `plasma`, `gamescope`.
ty: String,
},
/// Default desktop session type
GetDefaultDesktopSessionType,
/// Set default desktop session type
SetDefaultDesktopSessionType {
/// The session type to set as default, e.g. `plasma`, `plasmax11`.
ty: String,
},
/// Default session type
GetDefaultSessionType,
/// Set default session type
SetDefaultSessionType {
/// The session type to set as default, e.g. `plasma`, `gamescope`.
ty: String,
},
} }
async fn get_all_properties(conn: &Connection) -> Result<()> { async fn get_all_properties(conn: &Connection) -> Result<()> {
@ -638,6 +669,36 @@ async fn main() -> Result<()> {
.trigger_action(*action as u32, now.try_into()?) .trigger_action(*action as u32, now.try_into()?)
.await?; .await?;
} }
Commands::SwitchToDesktopMode => {
let proxy = SessionManagement1Proxy::new(&conn).await?;
proxy.switch_to_desktop_mode().await?;
}
Commands::SwitchToGameMode => {
let proxy = SessionManagement1Proxy::new(&conn).await?;
proxy.switch_to_game_mode().await?;
}
Commands::SwitchToSession { ty } => {
let proxy = SessionManagement1Proxy::new(&conn).await?;
proxy.switch_to_session(ty.as_str()).await?;
}
Commands::GetDefaultDesktopSessionType => {
let proxy = SessionManagement1Proxy::new(&conn).await?;
let ty = proxy.default_desktop_session_type().await?;
println!("Default desktop session type: {ty}");
}
Commands::SetDefaultDesktopSessionType { ty } => {
let proxy = SessionManagement1Proxy::new(&conn).await?;
proxy.set_default_desktop_session_type(ty.as_str()).await?;
}
Commands::GetDefaultSessionType => {
let proxy = SessionManagement1Proxy::new(&conn).await?;
let ty = proxy.default_session_type().await?;
println!("Default session type: {ty}");
}
Commands::SetDefaultSessionType { ty } => {
let proxy = SessionManagement1Proxy::new(&conn).await?;
proxy.set_default_session_type(ty.as_str()).await?;
}
} }
Ok(()) Ok(())

View file

@ -36,6 +36,7 @@ pub mod hardware;
pub mod power; pub mod power;
pub mod proxy; pub mod proxy;
pub mod screenreader; pub mod screenreader;
pub mod session_management;
pub mod wifi; pub mod wifi;
#[cfg(test)] #[cfg(test)]

View file

@ -2,6 +2,7 @@
* Copyright © 2023 Collabora Ltd. * Copyright © 2023 Collabora Ltd.
* Copyright © 2024 Valve Software * Copyright © 2024 Valve Software
* Copyright © 2024 Igalia S.L. * Copyright © 2024 Igalia S.L.
* Copyright © 2025 Harald Sitter <sitter@kde.org>
* *
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -33,6 +34,9 @@ use crate::power::{
GPUPerformanceLevel, GPUPowerProfile, SysfsWritten, TdpLimitManager, GPUPerformanceLevel, GPUPowerProfile, SysfsWritten, TdpLimitManager,
}; };
use crate::process::{run_script, script_output}; use crate::process::{run_script, script_output};
use crate::session_management::{
clear_ephemeral_session, set_session_to_switch_to, write_default_desktop_session_type, write_default_session_type
};
use crate::wifi::{ use crate::wifi::{
extract_wifi_trace, generate_wifi_dump, set_wifi_backend, set_wifi_debug_mode, extract_wifi_trace, generate_wifi_dump, set_wifi_backend, set_wifi_debug_mode,
set_wifi_power_management_state, WifiBackend, WifiDebugMode, WifiPowerManagement, set_wifi_power_management_state, WifiBackend, WifiDebugMode, WifiPowerManagement,
@ -488,6 +492,30 @@ impl SteamOSManager {
.map_err(to_zbus_fdo_error) .map_err(to_zbus_fdo_error)
} }
async fn set_session_to_switch_to(&self, ty: &str) -> fdo::Result<()> {
set_session_to_switch_to(ty)
.await
.inspect_err(|message| error!("Error switching to session: {message}"))
.map_err(to_zbus_fdo_error)
}
async fn set_default_desktop_session_type(&mut self, ty: &str) -> fdo::Result<()> {
write_default_desktop_session_type(ty)
.await
.map_err(to_zbus_fdo_error)
}
async fn set_default_session_type(&mut self, ty: &str) -> fdo::Result<()> {
write_default_session_type(ty)
.await
.map_err(to_zbus_fdo_error)
}
async fn set_login_successful(&mut self) -> fdo::Result<()> {
clear_ephemeral_session().await
.map_err(to_zbus_fdo_error)
}
/// A version property. /// A version property.
#[zbus(property(emits_changed_signal = "const"))] #[zbus(property(emits_changed_signal = "const"))]
async fn version(&self) -> u32 { async fn version(&self) -> u32 {

View file

@ -2,6 +2,7 @@
* Copyright © 2023 Collabora Ltd. * Copyright © 2023 Collabora Ltd.
* Copyright © 2024 Valve Software * Copyright © 2024 Valve Software
* Copyright © 2024 Igalia S.L. * Copyright © 2024 Igalia S.L.
* Copyright © 2025 Harald Sitter <sitter@kde.org>
* *
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -35,6 +36,9 @@ use crate::power::{
get_max_charge_level, get_platform_profile, TdpManagerCommand, get_max_charge_level, get_platform_profile, TdpManagerCommand,
}; };
use crate::screenreader::{OrcaManager, ScreenReaderAction, ScreenReaderMode}; use crate::screenreader::{OrcaManager, ScreenReaderAction, ScreenReaderMode};
use crate::session_management::{
read_default_desktop_session_type, read_default_session_type, restart_session,
};
use crate::wifi::{ use crate::wifi::{
get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend, get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend,
}; };
@ -157,6 +161,11 @@ struct PerformanceProfile1 {
tdp_limit_manager: Option<UnboundedSender<TdpManagerCommand>>, tdp_limit_manager: Option<UnboundedSender<TdpManagerCommand>>,
} }
struct SessionManagement1 {
proxy: Proxy<'static>,
connection: Connection,
}
struct ScreenReader0 { struct ScreenReader0 {
screen_reader: OrcaManager<'static>, screen_reader: OrcaManager<'static>,
} }
@ -723,6 +732,65 @@ impl ScreenReader0 {
} }
} }
#[interface(name = "com.steampowered.SteamOSManager1.SessionManagement1")]
impl SessionManagement1 {
async fn switch_to_desktop_mode(&self) -> fdo::Result<()> {
self.switch_to_session(
read_default_desktop_session_type()
.await
.map_err(to_zbus_fdo_error)?
.as_str(),
)
.await
}
async fn switch_to_game_mode(&self) -> fdo::Result<()> {
self.switch_to_session("gamescope-wayland").await
}
async fn switch_to_session(&self, ty: &str) -> fdo::Result<()> {
let _: () = method!(self, "SetSessionToSwitchTo", ty)?;
restart_session(&self.connection)
.await
.map_err(to_zbus_fdo_error)
}
#[zbus(property)]
async fn default_desktop_session_type(&self) -> fdo::Result<String> {
read_default_desktop_session_type()
.await
.map_err(to_zbus_fdo_error)
}
#[zbus(property)]
async fn set_default_desktop_session_type(
&self,
ty: &str,
#[zbus(signal_emitter)] ctx: SignalEmitter<'_>,
) -> zbus::Result<()> {
let _: () = self
.proxy
.call("SetDefaultDesktopSessionType", &(ty))
.await?;
self.default_desktop_session_type_changed(&ctx).await
}
#[zbus(property)]
async fn default_session_type(&self) -> fdo::Result<String> {
read_default_session_type().await.map_err(to_zbus_fdo_error)
}
#[zbus(property)]
async fn set_default_session_type(
&self,
ty: &str,
#[zbus(signal_emitter)] ctx: SignalEmitter<'_>,
) -> zbus::Result<()> {
let _: () = self.proxy.call("SetDefaultSessionType", &(ty)).await?;
self.default_session_type_changed(&ctx).await
}
}
#[interface(name = "com.steampowered.SteamOSManager1.Storage1")] #[interface(name = "com.steampowered.SteamOSManager1.Storage1")]
impl Storage1 { impl Storage1 {
async fn format_device( async fn format_device(
@ -1087,6 +1155,10 @@ pub(crate) async fn create_interfaces(
channel: daemon, channel: daemon,
}; };
let screen_reader = ScreenReader0::new(&session).await?; let screen_reader = ScreenReader0::new(&session).await?;
let session_management = SessionManagement1 {
proxy: proxy.clone(),
connection: session.clone(),
};
let wifi_debug = WifiDebug1 { let wifi_debug = WifiDebug1 {
proxy: proxy.clone(), proxy: proxy.clone(),
}; };
@ -1144,6 +1216,8 @@ pub(crate) async fn create_interfaces(
object_server.at(MANAGER_PATH, screen_reader).await?; object_server.at(MANAGER_PATH, screen_reader).await?;
} }
object_server.at(MANAGER_PATH, session_management).await?;
if steam_deck_variant().await.unwrap_or_default() == SteamDeckVariant::Galileo { if steam_deck_variant().await.unwrap_or_default() == SteamDeckVariant::Galileo {
object_server.at(MANAGER_PATH, wifi_debug).await?; object_server.at(MANAGER_PATH, wifi_debug).await?;
} }

View file

@ -26,6 +26,7 @@ mod low_power_mode1;
mod manager2; mod manager2;
mod performance_profile1; mod performance_profile1;
mod screenreader0; mod screenreader0;
mod session_management1;
mod storage1; mod storage1;
mod tdp_limit1; mod tdp_limit1;
mod update_bios1; mod update_bios1;
@ -45,6 +46,7 @@ pub use crate::proxy::low_power_mode1::LowPowerMode1Proxy;
pub use crate::proxy::manager2::Manager2Proxy; pub use crate::proxy::manager2::Manager2Proxy;
pub use crate::proxy::performance_profile1::PerformanceProfile1Proxy; pub use crate::proxy::performance_profile1::PerformanceProfile1Proxy;
pub use crate::proxy::screenreader0::ScreenReader0Proxy; pub use crate::proxy::screenreader0::ScreenReader0Proxy;
pub use crate::proxy::session_management1::SessionManagement1Proxy;
pub use crate::proxy::storage1::Storage1Proxy; pub use crate::proxy::storage1::Storage1Proxy;
pub use crate::proxy::tdp_limit1::TdpLimit1Proxy; pub use crate::proxy::tdp_limit1::TdpLimit1Proxy;
pub use crate::proxy::update_bios1::UpdateBios1Proxy; pub use crate::proxy::update_bios1::UpdateBios1Proxy;

View file

@ -0,0 +1,42 @@
//! # D-Bus interface proxy for: `com.steampowered.SteamOSManager1.SessionManagement1`
//!
//! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data.
//! Source: `com.steampowered.SteamOSManager1.xml`.
//!
//! You may prefer to adapt it, instead of using it verbatim.
//!
//! More information can be found in the [Writing a client proxy] section of the zbus
//! documentation.
//!
//!
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
use zbus::proxy;
#[proxy(
interface = "com.steampowered.SteamOSManager1.SessionManagement1",
default_service = "com.steampowered.SteamOSManager1",
default_path = "/com/steampowered/SteamOSManager1",
assume_defaults = true
)]
pub trait SessionManagement1 {
/// SwitchToDesktopMode method
fn switch_to_desktop_mode(&self) -> zbus::Result<()>;
/// SwitchToGameMode method
fn switch_to_game_mode(&self) -> zbus::Result<()>;
/// SwitchToSession method
fn switch_to_session(&self, type_: &str) -> zbus::Result<()>;
/// DefaultDesktopSessionType property
#[zbus(property)]
fn default_desktop_session_type(&self) -> zbus::Result<String>;
#[zbus(property)]
fn set_default_desktop_session_type(&self, value: &str) -> zbus::Result<()>;
/// DefaultSessionType property
#[zbus(property)]
fn default_session_type(&self) -> zbus::Result<String>;
#[zbus(property)]
fn set_default_session_type(&self, value: &str) -> zbus::Result<()>;
}

172
src/session_management.rs Normal file
View file

@ -0,0 +1,172 @@
/*
* Copyright © 2025 Harald Sitter <sitter@kde.org>
*
* SPDX-License-Identifier: MIT
*/
use anyhow::{anyhow, bail, Result};
use ini::Ini;
use tracing::debug;
use std::path::Path;
use tokio::fs;
use zbus::{self, Connection};
use crate::systemd::SystemdUnit;
// This is our persistent store. It applies unless an ephemeral session is set.
const PERSISTENT_CONFIG_FILE: &str = "/etc/sddm.conf.d/yy-steamos-session.conf";
// The ephemeral session configuration is ordered AFTER the persistent one so it can temporarily override it.
const EPHEMERAL_CONFIG_FILE: &str = "/etc/sddm.conf.d/zz-steamos-autologin.conf";
const CONFIG_SECTION_STEAM: &str = "X-SteamOS";
const CONFIG_SECTION_AUTOLOGIN: &str = "Autologin";
const DEFAULT_DESKTOP_SESSION: &str = "plasmax11";
const DEFAULT_SESSION: &str = "gamescope-wayland";
const CONFIG_KEY_DEFAULT_DESKTOP_SESSION: &str = "DefaultDesktopSession";
const CONFIG_KEY_SESSION: &str = "Session";
async fn find_type_in_dir(
prefix: impl AsRef<Path>,
ty: &str
) -> Result<String> {
let type_without_suffix = ty.trim_end_matches(".desktop");
let expected_session = format!("{type_without_suffix}.desktop");
let mut dir = fs::read_dir(prefix.as_ref()).await?;
while let Some(entry) = dir.next_entry().await? {
let file_name = entry.file_name();
let session: &str = &file_name.to_string_lossy();
if session == &expected_session {
return Ok(expected_session);
}
}
bail!( "Session type {ty} not found in directory {}",
prefix.as_ref().display()
)
}
async fn ensure_session_exists(ty: &str) -> Result<String> {
// Guard against bad input strings. Notably we don't want relative paths here as they would allow inspecting
// all root owned files.
// While we are at it also figure out if the session type has a one-shot variant and prefer that.
for dir in ["/usr/share/wayland-sessions", "/usr/share/xsessions"] {
match find_type_in_dir(dir, ty).await {
Ok(session) => return Ok(session),
Err(e) => debug!("{e}"), // output and try next
}
}
bail!("Session type {ty} not found in any of the known session directories")
}
async fn ini_load_async(path: &str) -> Result<Ini> {
let data = tokio::fs::read_to_string(path).await?;
Ini::load_from_str(data.as_str()).map_err(|e| anyhow!("Failed to load INI from {path}: {e}"))
}
async fn read_type_from_config(
config_file: &str,
config_section: &str,
config_key: &str,
default_session: &str,
) -> Result<String> {
let config = match ini_load_async(config_file).await {
Ok(config) => config,
_ => return Ok(default_session.to_owned()),
};
match config.section(Some(config_section)) {
Some(section) => {
let session = section.get(config_key);
return Ok(session.unwrap_or(default_session).to_owned());
}
None => return Ok(default_session.to_owned()),
}
}
async fn write_type_to_config(
config_file: &str,
config_section: &str,
config_key: &str,
session_name: &str,
) -> Result<()> {
let mut config = match ini_load_async(config_file).await {
Ok(config) => config,
_ => Ini::new(),
};
config
.with_section(Some(config_section))
.set(config_key, session_name);
config.write_to_file(config_file)?;
Ok(())
}
pub(crate) async fn set_session_to_switch_to(ty: &str) -> Result<()> {
ensure_session_exists(ty).await?;
write_type_to_config(
EPHEMERAL_CONFIG_FILE,
CONFIG_SECTION_AUTOLOGIN,
CONFIG_KEY_SESSION,
&ty,
)
.await
}
pub(crate) async fn read_default_desktop_session_type() -> Result<String> {
read_type_from_config(
PERSISTENT_CONFIG_FILE,
CONFIG_SECTION_STEAM,
CONFIG_KEY_DEFAULT_DESKTOP_SESSION,
DEFAULT_DESKTOP_SESSION,
)
.await
}
pub(crate) async fn write_default_desktop_session_type(ty: &str) -> Result<()> {
ensure_session_exists(ty).await?;
write_type_to_config(
PERSISTENT_CONFIG_FILE,
CONFIG_SECTION_STEAM,
CONFIG_KEY_DEFAULT_DESKTOP_SESSION,
&ty,
)
.await
}
pub(crate) async fn read_default_session_type() -> Result<String> {
read_type_from_config(
PERSISTENT_CONFIG_FILE,
CONFIG_SECTION_AUTOLOGIN,
CONFIG_KEY_SESSION,
DEFAULT_SESSION,
)
.await
}
pub(crate) async fn write_default_session_type(ty: &str) -> Result<()> {
ensure_session_exists(ty).await?;
write_type_to_config(
PERSISTENT_CONFIG_FILE,
CONFIG_SECTION_AUTOLOGIN,
CONFIG_KEY_SESSION,
ty,
)
.await
}
pub(crate) async fn restart_session(connection: &Connection) -> Result<()> {
for service in ["plasma-workspace.target", "gamescope-session.service"] {
let unit = SystemdUnit::new(connection.clone(), service).await?;
unit.stop()
.await
.map_err(|e| anyhow!("Failed to stop {service}: {e}"))?;
}
Ok(())
}
pub(crate) async fn clear_ephemeral_session() -> Result<()> {
tokio::fs::remove_file(EPHEMERAL_CONFIG_FILE)
.await
.map_err(|e| anyhow!("Failed to clear ephemeral session: {e}"))
}