mirror of
https://gitlab.steamos.cloud/holo/steamos-manager.git
synced 2025-07-08 07:30:36 -04:00
Previously there was a half-baked parser that looked for a fixed value in a fixed place, but this was not robust. This approach is properly robust, in case various different files set or override it.
417 lines
13 KiB
Rust
417 lines
13 KiB
Rust
/*
|
|
* Copyright © 2023 Collabora Ltd.
|
|
* Copyright © 2024 Valve Software
|
|
*
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
|
|
use anyhow::{anyhow, bail, ensure, Error, Result};
|
|
use config::builder::AsyncState;
|
|
use config::{ConfigBuilder, FileFormat};
|
|
use std::str::FromStr;
|
|
use strum::{Display, EnumString};
|
|
use tokio::fs;
|
|
use tracing::error;
|
|
use zbus::Connection;
|
|
|
|
use crate::process::{run_script, script_output};
|
|
use crate::systemd::{daemon_reload, SystemdUnit};
|
|
use crate::{path, read_config_directory};
|
|
|
|
const OVERRIDE_CONTENTS: &str = "[Service]
|
|
ExecStart=
|
|
ExecStart=/usr/lib/iwd/iwd -d
|
|
";
|
|
const OVERRIDE_FOLDER: &str = "/etc/systemd/system/iwd.service.d";
|
|
const OVERRIDE_PATH: &str = "/etc/systemd/system/iwd.service.d/99-valve-override.conf";
|
|
|
|
// Only use one path for output for now. If needed we can add a timestamp later
|
|
// to have multiple files, etc.
|
|
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_PATHS: &[&str] = &[
|
|
"/usr/lib/etc/NetworkManager/conf.d",
|
|
"/etc/NetworkManager/conf.d",
|
|
];
|
|
|
|
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
|
|
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
|
|
#[repr(u32)]
|
|
pub enum WifiDebugMode {
|
|
#[strum(
|
|
to_string = "off",
|
|
serialize = "disable",
|
|
serialize = "disabled",
|
|
serialize = "0"
|
|
)]
|
|
Off = 0,
|
|
Tracing = 1,
|
|
}
|
|
|
|
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
|
|
#[strum(ascii_case_insensitive)]
|
|
#[repr(u32)]
|
|
pub enum WifiPowerManagement {
|
|
#[strum(
|
|
to_string = "disabled",
|
|
serialize = "off",
|
|
serialize = "disable",
|
|
serialize = "0"
|
|
)]
|
|
Disabled = 0,
|
|
#[strum(
|
|
to_string = "enabled",
|
|
serialize = "on",
|
|
serialize = "enable",
|
|
serialize = "1"
|
|
)]
|
|
Enabled = 1,
|
|
}
|
|
|
|
#[derive(Display, EnumString, PartialEq, Debug, Copy, Clone)]
|
|
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
|
|
#[repr(u32)]
|
|
pub enum WifiBackend {
|
|
Iwd = 0,
|
|
WPASupplicant = 1,
|
|
}
|
|
|
|
impl TryFrom<u32> for WifiDebugMode {
|
|
type Error = Error;
|
|
fn try_from(v: u32) -> Result<Self, Self::Error> {
|
|
match v {
|
|
x if x == WifiDebugMode::Off as u32 => Ok(WifiDebugMode::Off),
|
|
x if x == WifiDebugMode::Tracing as u32 => Ok(WifiDebugMode::Tracing),
|
|
_ => Err(anyhow!("No enum match for value {v}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u32> for WifiPowerManagement {
|
|
type Error = Error;
|
|
fn try_from(v: u32) -> Result<Self, Self::Error> {
|
|
match v {
|
|
x if x == WifiPowerManagement::Disabled as u32 => Ok(WifiPowerManagement::Disabled),
|
|
x if x == WifiPowerManagement::Enabled as u32 => Ok(WifiPowerManagement::Enabled),
|
|
_ => Err(anyhow!("No enum match for value {v}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u32> for WifiBackend {
|
|
type Error = Error;
|
|
fn try_from(v: u32) -> Result<Self, Self::Error> {
|
|
match v {
|
|
x if x == WifiBackend::Iwd as u32 => Ok(WifiBackend::Iwd),
|
|
x if x == WifiBackend::WPASupplicant as u32 => Ok(WifiBackend::WPASupplicant),
|
|
_ => Err(anyhow!("No enum match for WifiBackend value {v}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) 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
|
|
|
|
if want_override {
|
|
// Copy it in
|
|
// Make sure the folder exists
|
|
fs::create_dir_all(path(OVERRIDE_FOLDER)).await?;
|
|
// Then write the contents into the file
|
|
fs::write(path(OVERRIDE_PATH), OVERRIDE_CONTENTS).await
|
|
} else {
|
|
// Delete it
|
|
match fs::remove_file(path(OVERRIDE_PATH)).await {
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
res => res,
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn restart_iwd(connection: Connection) -> Result<()> {
|
|
// First reload systemd since we modified the config most likely
|
|
// otherwise we wouldn't be restarting iwd.
|
|
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.service").await?;
|
|
unit.restart()
|
|
.await
|
|
.inspect_err(|message| error!("restart_iwd: restart unit got an error: {message}"))
|
|
}
|
|
|
|
async fn stop_tracing() -> Result<()> {
|
|
// Stop tracing and extract ring buffer to disk for capture
|
|
run_script(TRACE_CMD_PATH, &["stop"]).await?;
|
|
// stop tracing worked
|
|
run_script(TRACE_CMD_PATH, &["extract", "-o", OUTPUT_FILE]).await
|
|
}
|
|
|
|
async fn start_tracing(buffer_size: u32) -> Result<()> {
|
|
// Start tracing
|
|
let size_str = buffer_size.to_string();
|
|
run_script(
|
|
TRACE_CMD_PATH,
|
|
&["start", "-e", "ath11k_wmi_diag", "-b", &size_str],
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn set_wifi_debug_mode(
|
|
mode: WifiDebugMode,
|
|
buffer_size: u32,
|
|
should_trace: bool,
|
|
connection: Connection,
|
|
) -> Result<()> {
|
|
match get_wifi_backend().await {
|
|
Ok(WifiBackend::Iwd) => (),
|
|
Ok(backend) => bail!("Setting Wi-Fi debug mode not supported with backend {backend}"),
|
|
Err(e) => return Err(e),
|
|
}
|
|
|
|
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::Tracing => {
|
|
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(crate) async fn get_wifi_backend() -> Result<WifiBackend> {
|
|
let mut builder = ConfigBuilder::<AsyncState>::default();
|
|
for dir in WIFI_BACKEND_PATHS {
|
|
println!("{dir}");
|
|
builder = read_config_directory(builder, path(dir), &["conf"], FileFormat::Ini).await?;
|
|
}
|
|
println!("{builder:?}");
|
|
let config = builder.build().await?;
|
|
println!("{config:?}");
|
|
|
|
if let Some(backend) = config.get_table("device")?.remove("wifi.backend") {
|
|
let backend = backend.into_string()?;
|
|
println!("{backend:?}");
|
|
return Ok(WifiBackend::from_str(backend.as_str())?);
|
|
}
|
|
|
|
bail!("Wi-Fi backend not found in config");
|
|
}
|
|
|
|
pub(crate) async fn set_wifi_backend(backend: WifiBackend) -> Result<()> {
|
|
run_script("/usr/bin/steamos-wifi-set-backend", &[backend.to_string()]).await
|
|
}
|
|
|
|
pub(crate) async fn list_wifi_interfaces() -> Result<Vec<String>> {
|
|
let output = script_output("/usr/bin/iw", &["dev"]).await?;
|
|
Ok(output
|
|
.lines()
|
|
.filter_map(|line| match line.trim().split_once(' ') {
|
|
Some(("Interface", name)) => Some(name.to_string()),
|
|
_ => None,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
pub(crate) async fn get_wifi_power_management_state() -> Result<WifiPowerManagement> {
|
|
let mut found_any = false;
|
|
for iface in list_wifi_interfaces().await? {
|
|
let output =
|
|
script_output("/usr/bin/iw", &["dev", iface.as_str(), "get", "power_save"]).await?;
|
|
for line in output.lines() {
|
|
match line.trim() {
|
|
"Power save: on" => return Ok(WifiPowerManagement::Enabled),
|
|
"Power save: off" => found_any = true,
|
|
_ => continue,
|
|
}
|
|
}
|
|
}
|
|
ensure!(found_any, "No interfaces found");
|
|
Ok(WifiPowerManagement::Disabled)
|
|
}
|
|
|
|
pub(crate) async fn set_wifi_power_management_state(state: WifiPowerManagement) -> Result<()> {
|
|
let state = match state {
|
|
WifiPowerManagement::Disabled => "off",
|
|
WifiPowerManagement::Enabled => "on",
|
|
};
|
|
|
|
for iface in list_wifi_interfaces().await? {
|
|
run_script(
|
|
"/usr/bin/iw",
|
|
&["dev", iface.as_str(), "set", "power_save", state],
|
|
)
|
|
.await
|
|
.inspect_err(|message| error!("Error setting Wi-Fi power management state: {message}"))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use crate::{enum_on_off, enum_roundtrip, testing};
|
|
use tokio::fs::{create_dir_all, read_to_string, remove_dir, try_exists, write};
|
|
|
|
#[test]
|
|
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_setup_iwd_config() {
|
|
let _h = testing::start();
|
|
|
|
// Remove with no dir
|
|
assert!(setup_iwd_config(false).await.is_ok());
|
|
|
|
create_dir_all(path(OVERRIDE_FOLDER))
|
|
.await
|
|
.expect("create_dir_all");
|
|
|
|
// Remove with dir but no file
|
|
assert!(setup_iwd_config(false).await.is_ok());
|
|
|
|
// Remove with dir and file
|
|
write(path(OVERRIDE_PATH), "").await.expect("write");
|
|
assert!(try_exists(path(OVERRIDE_PATH)).await.unwrap());
|
|
|
|
assert!(setup_iwd_config(false).await.is_ok());
|
|
assert!(!try_exists(path(OVERRIDE_PATH)).await.unwrap());
|
|
|
|
// Double remove
|
|
assert!(setup_iwd_config(false).await.is_ok());
|
|
|
|
// Create with no dir
|
|
remove_dir(path(OVERRIDE_FOLDER)).await.expect("remove_dir");
|
|
|
|
assert!(setup_iwd_config(true).await.is_ok());
|
|
assert_eq!(
|
|
read_to_string(path(OVERRIDE_PATH)).await.unwrap(),
|
|
OVERRIDE_CONTENTS
|
|
);
|
|
|
|
// Create with dir
|
|
assert!(setup_iwd_config(false).await.is_ok());
|
|
assert!(setup_iwd_config(true).await.is_ok());
|
|
assert_eq!(
|
|
read_to_string(path(OVERRIDE_PATH)).await.unwrap(),
|
|
OVERRIDE_CONTENTS
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_get_wifi_backend() {
|
|
let _h = testing::start();
|
|
|
|
for dir in WIFI_BACKEND_PATHS {
|
|
create_dir_all(path(dir)).await.expect("create_dir_all");
|
|
}
|
|
|
|
assert!(get_wifi_backend().await.is_err());
|
|
|
|
write(path(WIFI_BACKEND_PATHS[0]).join("test.conf"), "[device]")
|
|
.await
|
|
.expect("write");
|
|
assert!(get_wifi_backend().await.is_err());
|
|
|
|
write(
|
|
path(WIFI_BACKEND_PATHS[0]).join("test.conf"),
|
|
"[device]\nwifi.backend=fake\n",
|
|
)
|
|
.await
|
|
.expect("write");
|
|
assert!(get_wifi_backend().await.is_err());
|
|
|
|
write(
|
|
path(WIFI_BACKEND_PATHS[0]).join("test.conf"),
|
|
"[device]\nwifi.backend=iwd\n",
|
|
)
|
|
.await
|
|
.expect("write");
|
|
assert_eq!(get_wifi_backend().await.unwrap(), WifiBackend::Iwd);
|
|
|
|
write(
|
|
path(WIFI_BACKEND_PATHS[0]).join("test.conf"),
|
|
"[device]\nwifi.backend=wpa_supplicant\n",
|
|
)
|
|
.await
|
|
.expect("write");
|
|
assert_eq!(
|
|
get_wifi_backend().await.unwrap(),
|
|
WifiBackend::WPASupplicant
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wifi_debug_mode_roundtrip() {
|
|
enum_roundtrip!(WifiDebugMode {
|
|
0: u32 = Off,
|
|
1: u32 = Tracing,
|
|
"off": str = Off,
|
|
"tracing": str = Tracing,
|
|
});
|
|
assert!(WifiDebugMode::try_from(2).is_err());
|
|
assert!(WifiDebugMode::from_str("onf").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn wifi_power_management_roundtrip() {
|
|
enum_roundtrip!(WifiPowerManagement {
|
|
0: u32 = Disabled,
|
|
1: u32 = Enabled,
|
|
"disabled": str = Disabled,
|
|
"enabled": str = Enabled,
|
|
});
|
|
enum_on_off!(WifiPowerManagement => (Enabled, Disabled));
|
|
assert!(WifiPowerManagement::try_from(2).is_err());
|
|
assert!(WifiPowerManagement::from_str("onf").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn wifi_backend_roundtrip() {
|
|
enum_roundtrip!(WifiBackend {
|
|
0: u32 = Iwd,
|
|
1: u32 = WPASupplicant,
|
|
"iwd": str = Iwd,
|
|
"wpa_supplicant": str = WPASupplicant,
|
|
});
|
|
assert!(WifiBackend::try_from(2).is_err());
|
|
assert!(WifiBackend::from_str("iwl").is_err());
|
|
}
|
|
}
|