summaryrefslogtreecommitdiffhomepage
path: root/test/test-runner/src/sys/linux.rs
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-09-11 16:10:31 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-09-12 11:29:56 +0200
commiteeb8da48efab6b635d11fb92ab57a3db72d486bd (patch)
tree6ab7adabe89746ad1e800e0720565974a9204896 /test/test-runner/src/sys/linux.rs
parent295eff0a7d5b0b2ab388bab47035a68d7169f7d1 (diff)
downloadmullvadvpn-eeb8da48efab6b635d11fb92ab57a3db72d486bd.tar.xz
mullvadvpn-eeb8da48efab6b635d11fb92ab57a3db72d486bd.zip
Split sys module in test-runner into one per platform
Diffstat (limited to 'test/test-runner/src/sys/linux.rs')
-rw-r--r--test/test-runner/src/sys/linux.rs281
1 files changed, 281 insertions, 0 deletions
diff --git a/test/test-runner/src/sys/linux.rs b/test/test-runner/src/sys/linux.rs
new file mode 100644
index 0000000000..b26cd2b618
--- /dev/null
+++ b/test/test-runner/src/sys/linux.rs
@@ -0,0 +1,281 @@
+use std::collections::HashMap;
+use test_rpc::{meta::OsVersion, mullvad_daemon::Verbosity};
+
+const SYSTEMD_OVERRIDE_FILE: &str = "/etc/systemd/system/mullvad-daemon.service.d/override.conf";
+
+pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test_rpc::Error> {
+ use tokio::io::AsyncWriteExt;
+ log::debug!("Setting log level");
+
+ let verbosity = match verbosity_level {
+ Verbosity::Info => "",
+ Verbosity::Debug => "-v",
+ Verbosity::Trace => "-vv",
+ };
+ let systemd_service_file_content = format!(
+ r#"[Service]
+ExecStart=
+ExecStart=/usr/bin/mullvad-daemon --disable-stdout-timestamps {verbosity}"#
+ );
+
+ let override_path = std::path::Path::new(SYSTEMD_OVERRIDE_FILE);
+ if let Some(parent) = override_path.parent() {
+ tokio::fs::create_dir_all(parent)
+ .await
+ .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
+ }
+
+ let mut file = tokio::fs::OpenOptions::new()
+ .create(true)
+ .truncate(true)
+ .write(true)
+ .open(override_path)
+ .await
+ .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
+
+ file.write_all(systemd_service_file_content.as_bytes())
+ .await
+ .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
+
+ tokio::process::Command::new("systemctl")
+ .args(["daemon-reload"])
+ .status()
+ .await
+ .map_err(|e| test_rpc::Error::ServiceStart(e.to_string()))?;
+
+ restart_app().await?;
+ Ok(())
+}
+
+/// Restart the Mullvad VPN application.
+///
+/// This function waits for the app to successfully start again.
+pub async fn restart_app() -> Result<(), test_rpc::Error> {
+ tokio::process::Command::new("systemctl")
+ .args(["restart", "mullvad-daemon"])
+ .status()
+ .await
+ .map_err(|e| test_rpc::Error::ServiceStart(e.to_string()))?;
+ wait_for_service_state(ServiceState::Running).await?;
+ Ok(())
+}
+
+/// Stop the Mullvad VPN application.
+///
+/// This function waits for the app to successfully shut down.
+pub async fn stop_app() -> Result<(), test_rpc::Error> {
+ tokio::process::Command::new("systemctl")
+ .args(["stop", "mullvad-daemon"])
+ .status()
+ .await
+ .map_err(|e| test_rpc::Error::ServiceStop(e.to_string()))?;
+ wait_for_service_state(ServiceState::Inactive).await?;
+
+ Ok(())
+}
+
+/// Start the Mullvad VPN application.
+///
+/// This function waits for the app to successfully start again.
+pub async fn start_app() -> Result<(), test_rpc::Error> {
+ tokio::process::Command::new("systemctl")
+ .args(["start", "mullvad-daemon"])
+ .status()
+ .await
+ .map_err(|e| test_rpc::Error::ServiceStart(e.to_string()))?;
+ wait_for_service_state(ServiceState::Running).await?;
+ Ok(())
+}
+
+#[derive(Debug)]
+struct EnvVar {
+ var: String,
+ value: String,
+}
+
+impl EnvVar {
+ fn from_systemd_string(s: &str) -> Result<Self, &'static str> {
+ // 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" => {
+ // Process the input just a bit more - remove the leading and trailing quote (").
+ let var = input
+ .next()
+ .ok_or(error)?
+ .trim_start_matches('"')
+ .to_string();
+ let value = input.next().ok_or(error)?.trim_end_matches('"').to_string();
+ Ok(EnvVar { var, value })
+ }
+ _ => Err(error),
+ }
+ }
+
+ fn to_systemd_string(&self) -> String {
+ format!(
+ "Environment=\"{key}={value}\"",
+ key = self.var,
+ value = self.value
+ )
+ }
+}
+
+pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), test_rpc::Error> {
+ use std::{fmt::Write, ops::Not};
+
+ let mut override_content = String::new();
+ override_content.push_str("[Service]\n");
+
+ for env_var in env
+ .into_iter()
+ .map(|(var, value)| EnvVar { var, value })
+ .map(|env_var| env_var.to_systemd_string())
+ {
+ writeln!(&mut override_content, "{env_var}")
+ .map_err(|err| test_rpc::Error::ServiceChange(err.to_string()))?;
+ }
+
+ let override_path = std::path::Path::new(SYSTEMD_OVERRIDE_FILE);
+ if let Some(parent) = override_path.parent() {
+ tokio::fs::create_dir_all(parent)
+ .await
+ .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
+ }
+
+ tokio::fs::write(override_path, override_content)
+ .await
+ .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
+
+ if tokio::process::Command::new("systemctl")
+ .args(["daemon-reload"])
+ .status()
+ .await
+ .map_err(|e| test_rpc::Error::Io(e.to_string()))?
+ .success()
+ .not()
+ {
+ return Err(test_rpc::Error::ServiceChange(
+ "Daemon service could not be reloaded".to_owned(),
+ ));
+ };
+
+ if tokio::process::Command::new("systemctl")
+ .args(["restart", "mullvad-daemon"])
+ .status()
+ .await
+ .map_err(|e| test_rpc::Error::Io(e.to_string()))?
+ .success()
+ .not()
+ {
+ return Err(test_rpc::Error::ServiceStart(
+ "Daemon service could not be restarted".to_owned(),
+ ));
+ };
+
+ wait_for_service_state(ServiceState::Running).await?;
+ Ok(())
+}
+
+pub async fn get_daemon_environment() -> Result<HashMap<String, String>, test_rpc::Error> {
+ let text = tokio::fs::read_to_string(SYSTEMD_OVERRIDE_FILE)
+ .await
+ .map_err(|err| test_rpc::Error::FileSystem(err.to_string()))?;
+
+ let env: HashMap<String, String> = parse_systemd_env_file(&text)
+ .map(|EnvVar { var, value }| (var, value))
+ .collect();
+ Ok(env)
+}
+
+/// Parse a systemd env-file. `input` is assumed to be the entire text content of a systemd-env
+/// file.
+///
+/// Example systemd-env file:
+/// ```
+/// [Service]
+/// Environment="VAR1=pGNqduRFkB4K9C2vijOmUDa2kPtUhArN"
+/// Environment="VAR2=JP8YLOc2bsNlrGuD6LVTq7L36obpjzxd"
+/// ```
+fn parse_systemd_env_file(input: &str) -> impl Iterator<Item = EnvVar> + '_ {
+ input
+ .lines()
+ .map(EnvVar::from_systemd_string)
+ .filter_map(|env_var| env_var.ok())
+ .inspect(|env_var| log::trace!("Parsed {env_var:?}"))
+}
+
+enum ServiceState {
+ Running,
+ Inactive,
+}
+
+async fn wait_for_service_state(awaited_state: ServiceState) -> Result<(), test_rpc::Error> {
+ const RETRY_ATTEMPTS: usize = 10;
+ let mut attempt = 0;
+ loop {
+ attempt += 1;
+ if attempt > RETRY_ATTEMPTS {
+ return Err(test_rpc::Error::ServiceStart(String::from(
+ "Awaiting new service state timed out",
+ )));
+ }
+
+ let output = tokio::process::Command::new("systemctl")
+ .args(["status", "mullvad-daemon"])
+ .output()
+ .await
+ .map_err(|e| test_rpc::Error::ServiceNotFound(e.to_string()))?
+ .stdout;
+ let output = String::from_utf8_lossy(&output);
+
+ match awaited_state {
+ ServiceState::Running => {
+ if output.contains("active (running)") {
+ break;
+ }
+ }
+ ServiceState::Inactive => {
+ if output.contains("inactive (dead)") {
+ break;
+ }
+ }
+ }
+
+ tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
+ }
+ Ok(())
+}
+
+pub fn get_os_version() -> Result<OsVersion, test_rpc::Error> {
+ 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).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");
+ }
+}