mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-15 18:56:49 -04:00
SessionManagement1 provides control over which sessions are started by the login manager. See the dbus API documentation for more information. This commit lifts rust-ini from transitive dependency to direct dependency since we need slightly better control over the ini content. SessionManagement1 is implemented in both user and root scope. User scope implements the public API and internally calls into root scope for root-elevated actions, where necessary augmenting with user-level actions (namely managing user systemd services). Two SDDM configuration files are in play here - `/etc/sddm.conf.d/yy-steamos-session.conf`: acting as "persistent" default configuration - `/etc/sddm.conf.d/zz-steamos-autologin.conf`: the "temporary" configuration overrides the persistent one whenever present and is meant to be cleared by either the login manager or the session. This follows current behavior where we have oneshot sessions that simply reset the temporary back to gamescope. Internal configuration keys are stored in the persistent config inside an `[X-SteamOS]` section.
172 lines
5.1 KiB
Rust
172 lines
5.1 KiB
Rust
/*
|
|
* 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}"))
|
|
}
|