From 22d7b719be36796229e8e944aae0e63785e97d32 Mon Sep 17 00:00:00 2001 From: Markus Pettersson Date: Tue, 9 Apr 2024 20:59:42 +0200 Subject: [PATCH] fixup! WIP Reset daemon environment if needed in cleanup routine --- test/test-runner/src/sys.rs | 157 ++++++++++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 14 deletions(-) diff --git a/test/test-runner/src/sys.rs b/test/test-runner/src/sys.rs index 5f1b80c5f965..fb58e59a1968 100644 --- a/test/test-runner/src/sys.rs +++ b/test/test-runner/src/sys.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; #[cfg(target_os = "windows")] use std::io; +use std::{collections::HashMap, str::FromStr}; use test_rpc::{meta::OsVersion, mullvad_daemon::Verbosity}; #[cfg(target_os = "windows")] @@ -154,6 +154,7 @@ pub fn reboot() -> Result<(), test_rpc::Error> { #[cfg(target_os = "linux")] pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test_rpc::Error> { use tokio::io::AsyncWriteExt; + // TODO(markus): Only define once const SYSTEMD_OVERRIDE_FILE: &str = "/etc/systemd/system/mullvad-daemon.service.d/override.conf"; @@ -388,9 +389,63 @@ pub async fn set_daemon_log_level(_verbosity_level: Verbosity) -> Result<(), tes Ok(()) } -pub async fn get_daemon_environment() -> Result, test_rpc::Error> { - let env = HashMap::new(); - Ok(env) +// TODO(markus): Move somewhere sane! +#[derive(Debug)] +struct EnvVar { + var: String, + value: String, +} + +impl, V: Into> From<(K, V)> for EnvVar { + fn from(value: (K, V)) -> Self { + Self { + var: value.0.into(), + value: value.1.into(), + } + } +} + +// TODO(markus): This is only valid on Linux +impl std::str::FromStr for EnvVar { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + // Here, we are only concerned with parsing a line that starts with "Environment". + let error = "Failed to parse systemd env-config"; + let mut input = s.trim().split('='); + let pre = input.next().ok_or(error)?; + match pre { + "Environment" => { + // Pre-proccess the input just a bit more - remove the leading and trailing quote ("). + let var = { + let var = input.next().ok_or(error)?; + var[1..].to_string() + }; + let value = { + let value = input.next().ok_or(error)?; + value[..(value.len() - 1)].to_string() + }; + Ok(EnvVar { var, value }) + } + _ => Err(error), + } + } +} + +impl EnvVar { + /// Convert `self` into a tuple of its parts. + pub fn tuple(self) -> (String, String) { + (self.var, self.value) + } + + #[cfg(target_os = "linux")] + fn to_systemd_string(&self) -> String { + format!( + "Environment=\"{key}={value}\"", + key = self.var, + value = self.value + ) + } } #[cfg(target_os = "linux")] @@ -403,8 +458,9 @@ pub async fn set_daemon_environment(env: HashMap) -> Result<(), let mut override_content = String::new(); override_content.push_str("[Service]\n"); - for (k, v) in env { - writeln!(&mut override_content, "Environment=\"{k}={v}\"").unwrap(); + for var in env.iter().map(EnvVar::from) { + writeln!(&mut override_content, "{}", var.to_systemd_string()) + .map_err(|err| test_rpc::Error::Service(err.to_string()))?; } let override_path = std::path::Path::new(SYSTEMD_OVERRIDE_FILE); @@ -414,15 +470,10 @@ pub async fn set_daemon_environment(env: HashMap) -> Result<(), .map_err(|e| test_rpc::Error::Service(e.to_string()))?; } - let mut file = tokio::fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(override_path) + tokio::fs::File::create(override_path) .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - - file.write_all(override_content.as_bytes()) + .map_err(|e| test_rpc::Error::Service(e.to_string()))? + .write_all(override_content.as_bytes()) .await .map_err(|e| test_rpc::Error::Service(e.to_string()))?; @@ -545,6 +596,57 @@ async fn set_launch_daemon_state(on: bool) -> Result<(), test_rpc::Error> { Ok(()) } +#[cfg(target_os = "linux")] +pub async fn get_daemon_environment() -> Result, test_rpc::Error> { + use tokio::fs::File; + use tokio::io::AsyncReadExt; + const SYSTEMD_OVERRIDE_FILE: &str = "/etc/systemd/system/mullvad-daemon.service.d/env.conf"; + + let mut file = File::open(SYSTEMD_OVERRIDE_FILE) + .await + .map_err(|err| test_rpc::Error::FileSystem(err.to_string()))?; + let text = { + let mut buf = String::new(); + file.read_to_string(&mut buf) + .await + .map_err(|err| test_rpc::Error::FileSystem(err.to_string()))?; + buf + }; + + let env: HashMap = parse_systemd_env_file(&text) + .into_iter() + .map(EnvVar::tuple) + .collect(); + Ok(env) +} + +/// Parse a systemd env-file. +/// TODO(markus): Define what the `input` parameter is. +/// +/// Example systemd-env file: +/// [Service] +/// Environment="VAR1=pGNqduRFkB4K9C2vijOmUDa2kPtUhArN" +/// Environment="VAR2=JP8YLOc2bsNlrGuD6LVTq7L36obpjzxd" +fn parse_systemd_env_file(input: &str) -> impl IntoIterator + '_ { + input + .lines() + // Skip the [Service] line + .skip(1) + .map(EnvVar::from_str) + .filter_map(|env_var| env_var.ok()) + .inspect(|env_var| println!("Parsed {env_var:?}")) +} + +#[cfg(target_os = "windows")] +pub async fn get_daemon_environment() -> Result, test_rpc::Error> { + todo!("Not implemented on Windows") +} + +#[cfg(target_os = "macos")] +pub async fn get_daemon_environment() -> Result, test_rpc::Error> { + todo!("Not implemented on macOS") +} + #[cfg(target_os = "linux")] enum ServiceState { Running, @@ -623,3 +725,30 @@ pub fn get_os_version() -> Result { pub fn get_os_version() -> Result { Ok(OsVersion::Linux) } + +#[cfg(test)] +mod test { + + #[test] + fn parse_systemd_environment_variables() { + use super::parse_systemd_env_file; + // Define an example systemd environment file + let systemd_file = " + [Service] + Environment=\"var1=value1\" + Environment=\"var2=value2\" + "; + + // Parse the "file" + let env_vars: Vec<_> = parse_systemd_env_file(systemd_file).into_iter().collect(); + + // Assert that the environment variables it defines are parsed as expected. + assert_eq!(env_vars.len(), 2); + let first = env_vars.first().unwrap(); + assert_eq!(first.var, "var1"); + assert_eq!(first.value, "value1"); + let second = env_vars.get(1).unwrap(); + assert_eq!(second.var, "var2"); + assert_eq!(second.value, "value2"); + } +}