summaryrefslogtreecommitdiffhomepage
path: root/test/test-runner/src/sys.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.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.rs')
-rw-r--r--test/test-runner/src/sys.rs954
1 files changed, 0 insertions, 954 deletions
diff --git a/test/test-runner/src/sys.rs b/test/test-runner/src/sys.rs
deleted file mode 100644
index b6e109afc8..0000000000
--- a/test/test-runner/src/sys.rs
+++ /dev/null
@@ -1,954 +0,0 @@
-use std::collections::HashMap;
-#[cfg(target_os = "windows")]
-use std::io;
-use std::path::Path;
-use test_rpc::mullvad_daemon::ServiceStatus;
-use test_rpc::{meta::OsVersion, mullvad_daemon::Verbosity};
-
-#[cfg(target_os = "windows")]
-use std::ffi::OsString;
-use test_rpc::mullvad_daemon::SOCKET_PATH;
-#[cfg(target_os = "windows")]
-use windows_service::{
- service::{Service, ServiceAccess, ServiceInfo, ServiceState},
- service_manager::{ServiceManager, ServiceManagerAccess},
-};
-
-#[cfg(target_os = "linux")]
-const SYSTEMD_OVERRIDE_FILE: &str = "/etc/systemd/system/mullvad-daemon.service.d/override.conf";
-
-#[cfg(target_os = "macos")]
-const PLIST_OVERRIDE_FILE: &str = "/Library/LaunchDaemons/net.mullvad.daemon.plist";
-
-#[cfg(target_os = "windows")]
-const MULLVAD_WIN_REGISTRY: &str = r"SYSTEM\CurrentControlSet\Services\Mullvad VPN";
-
-#[cfg(target_os = "windows")]
-pub fn reboot() -> Result<(), test_rpc::Error> {
- use windows_sys::Win32::{
- System::Shutdown::{
- EWX_REBOOT, ExitWindowsEx, SHTDN_REASON_FLAG_PLANNED, SHTDN_REASON_MAJOR_APPLICATION,
- SHTDN_REASON_MINOR_OTHER,
- },
- UI::WindowsAndMessaging::EWX_FORCEIFHUNG,
- };
-
- grant_shutdown_privilege()?;
-
- std::thread::spawn(|| {
- std::thread::sleep(std::time::Duration::from_secs(5));
-
- let shutdown_result = unsafe {
- ExitWindowsEx(
- EWX_FORCEIFHUNG | EWX_REBOOT,
- SHTDN_REASON_MAJOR_APPLICATION
- | SHTDN_REASON_MINOR_OTHER
- | SHTDN_REASON_FLAG_PLANNED,
- )
- };
-
- if shutdown_result == 0 {
- log::error!(
- "Failed to restart test machine: {}",
- io::Error::last_os_error()
- );
- std::process::exit(1);
- }
-
- std::process::exit(0);
- });
-
- // NOTE: We do not bother to revoke the privilege.
-
- Ok(())
-}
-
-#[cfg(target_os = "windows")]
-fn grant_shutdown_privilege() -> Result<(), test_rpc::Error> {
- use windows_sys::Win32::{
- Foundation::{CloseHandle, HANDLE, LUID},
- Security::{
- AdjustTokenPrivileges, LUID_AND_ATTRIBUTES, LookupPrivilegeValueW,
- SE_PRIVILEGE_ENABLED, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES,
- },
- System::{
- SystemServices::SE_SHUTDOWN_NAME,
- Threading::{GetCurrentProcess, OpenProcessToken},
- },
- };
-
- let mut privileges = TOKEN_PRIVILEGES {
- PrivilegeCount: 1,
- Privileges: [LUID_AND_ATTRIBUTES {
- Luid: LUID {
- HighPart: 0,
- LowPart: 0,
- },
- Attributes: SE_PRIVILEGE_ENABLED,
- }],
- };
-
- if unsafe {
- LookupPrivilegeValueW(
- std::ptr::null(),
- SE_SHUTDOWN_NAME,
- &mut privileges.Privileges[0].Luid,
- )
- } == 0
- {
- log::error!(
- "Failed to lookup shutdown privilege LUID: {}",
- io::Error::last_os_error()
- );
- return Err(test_rpc::Error::Syscall);
- }
-
- let mut token_handle: HANDLE = 0;
-
- if unsafe {
- OpenProcessToken(
- GetCurrentProcess(),
- TOKEN_ADJUST_PRIVILEGES,
- &mut token_handle,
- )
- } == 0
- {
- log::error!("OpenProcessToken() failed: {}", io::Error::last_os_error());
- return Err(test_rpc::Error::Syscall);
- }
-
- let result = unsafe {
- AdjustTokenPrivileges(
- token_handle,
- 0,
- &privileges,
- 0,
- std::ptr::null_mut(),
- std::ptr::null_mut(),
- )
- };
-
- unsafe { CloseHandle(token_handle) };
-
- if result == 0 {
- log::error!(
- "Failed to enable SE_SHUTDOWN_NAME: {}",
- io::Error::last_os_error()
- );
- return Err(test_rpc::Error::Syscall);
- }
-
- Ok(())
-}
-
-#[cfg(unix)]
-pub fn reboot() -> Result<(), test_rpc::Error> {
- log::debug!("Rebooting system");
-
- std::thread::spawn(|| {
- #[cfg(target_os = "linux")]
- let mut cmd = std::process::Command::new("/usr/sbin/shutdown");
- #[cfg(target_os = "macos")]
- let mut cmd = std::process::Command::new("/sbin/shutdown");
- cmd.args(["-r", "now"]);
-
- std::thread::sleep(std::time::Duration::from_secs(5));
-
- let _ = cmd.spawn().map_err(|error| {
- log::error!("Failed to spawn shutdown command: {error}");
- error
- });
- });
-
- Ok(())
-}
-
-#[cfg(target_os = "linux")]
-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.
-#[cfg(target_os = "linux")]
-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.
-#[cfg(target_os = "linux")]
-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.
-#[cfg(target_os = "linux")]
-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(())
-}
-
-/// Restart the Mullvad VPN application.
-///
-/// This function waits for the app to successfully start again.
-#[cfg(target_os = "windows")]
-pub async fn restart_app() -> Result<(), test_rpc::Error> {
- stop_app().await?;
- start_app().await?;
- Ok(())
-}
-
-/// Stop the Mullvad VPN application.
-///
-/// This function waits for the app to successfully shut down.
-#[cfg(target_os = "windows")]
-pub async fn stop_app() -> Result<(), test_rpc::Error> {
- let _ = tokio::process::Command::new("net")
- .args(["stop", "mullvadvpn"])
- .status()
- .await
- .map_err(|e| test_rpc::Error::ServiceStop(e.to_string()))?;
- Ok(())
-}
-
-/// Start the Mullvad VPN application.
-///
-/// This function waits for the app to successfully start again.
-#[cfg(target_os = "windows")]
-pub async fn start_app() -> Result<(), test_rpc::Error> {
- let _ = tokio::process::Command::new("net")
- .args(["start", "mullvadvpn"])
- .status()
- .await
- .map_err(|e| test_rpc::Error::ServiceStart(e.to_string()))?;
- Ok(())
-}
-
-/// Disable the Mullvad VPN system service startup. This will not trigger the service to stop
-/// immediately, but it will prevent it from starting on the next system boot.
-#[cfg(target_os = "windows")]
-pub async fn disable_system_service_startup() -> Result<(), test_rpc::Error> {
- let status = tokio::process::Command::new("powershell")
- .args([
- "-NoProfile",
- "-Command",
- "Set-Service -Name MullvadVPN -StartupType Disabled",
- ])
- .status()
- .await
- .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
-
- if !status.success() {
- return Err(test_rpc::Error::ServiceChange(
- "Failed to disable MullvadVPN service".to_string(),
- ));
- }
-
- Ok(())
-}
-
-/// Enable the Mullvad VPN system service startup. This will configure the service to start automatically on system boot.
-#[cfg(target_os = "windows")]
-pub async fn enable_system_service_startup() -> Result<(), test_rpc::Error> {
- let status = tokio::process::Command::new("powershell")
- .args([
- "-NoProfile",
- "-Command",
- "Set-Service -Name MullvadVPN -StartupType Automatic",
- ])
- .status()
- .await
- .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
-
- if !status.success() {
- return Err(test_rpc::Error::ServiceChange(
- "Failed to enable MullvadVPN service".to_string(),
- ));
- }
-
- Ok(())
-}
-
-/// Restart the Mullvad VPN application.
-///
-/// This function waits for the app to successfully start again.
-#[cfg(target_os = "macos")]
-pub async fn restart_app() -> Result<(), test_rpc::Error> {
- stop_app().await?;
- start_app().await?;
- Ok(())
-}
-
-/// Stop the Mullvad VPN application.
-///
-/// This function waits for the app to successfully shut down.
-#[cfg(target_os = "macos")]
-pub async fn stop_app() -> Result<(), test_rpc::Error> {
- set_launch_daemon_state(false).await?;
- tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
- Ok(())
-}
-
-/// Start the Mullvad VPN application.
-///
-/// This function waits for the app to successfully start again.
-#[cfg(target_os = "macos")]
-pub async fn start_app() -> Result<(), test_rpc::Error> {
- set_launch_daemon_state(true).await?;
- tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
- Ok(())
-}
-
-#[cfg(target_os = "windows")]
-pub fn get_daemon_system_service_status() -> Result<ServiceStatus, test_rpc::Error> {
- let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
- .map_err(|e| test_rpc::Error::ServiceNotFound(e.to_string()))?;
- let service = manager
- .open_service("mullvadvpn", ServiceAccess::QUERY_STATUS)
- .map_err(|e| test_rpc::Error::ServiceNotFound(e.to_string()))?;
-
- get_daemon_system_service_status_inner(&service)
-}
-
-#[cfg(target_os = "windows")]
-fn get_daemon_system_service_status_inner(
- service: &Service,
-) -> Result<ServiceStatus, test_rpc::Error> {
- let status = service
- .query_status()
- .map_err(|e| test_rpc::Error::Other(e.to_string()))?;
-
- let status = match status.current_state {
- ServiceState::Running => ServiceStatus::Running,
- // NOTE: not counting pending start as running, since we cannot set log level then
- _ => ServiceStatus::NotRunning,
- };
-
- Ok(status)
-}
-
-#[cfg(target_os = "windows")]
-async fn wait_for_service_status(
- service: &Service,
- accept_fn: impl Fn(&windows_service::service::ServiceStatus) -> bool,
-) -> Result<(), test_rpc::Error> {
- const MAX_ATTEMPTS: usize = 10;
- const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(3);
-
- for _ in 0..MAX_ATTEMPTS {
- let status = service
- .query_status()
- .map_err(|e| test_rpc::Error::Other(e.to_string()))?;
- if accept_fn(&status) {
- return Ok(());
- }
- tokio::time::sleep(POLL_INTERVAL).await;
- }
- Err(test_rpc::Error::ServiceStart(
- "Awaiting new service state timed out".to_string(),
- ))
-}
-
-#[cfg(target_os = "windows")]
-pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test_rpc::Error> {
- use std::error::Error;
-
- fn error_with_source(e: &impl Error) -> String {
- if let Some(source) = e.source() {
- format!("{e}: {source}")
- } else {
- e.to_string()
- }
- }
-
- log::debug!("Setting log level");
-
- let verbosity = match verbosity_level {
- Verbosity::Info => "",
- Verbosity::Debug => "-v",
- Verbosity::Trace => "-vv",
- };
-
- let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)
- .map_err(|e| test_rpc::Error::ServiceNotFound(e.to_string()))?;
- let service = manager
- .open_service(
- "mullvadvpn",
- ServiceAccess::QUERY_CONFIG
- | ServiceAccess::QUERY_STATUS
- | ServiceAccess::CHANGE_CONFIG
- | ServiceAccess::START
- | ServiceAccess::STOP,
- )
- .map_err(|e| {
- test_rpc::Error::ServiceNotFound(format!(
- "Failed to open service: {}",
- error_with_source(&e)
- ))
- })?;
-
- log::info!("Stopping service");
-
- // Stop the service
- service
- .stop()
- .map_err(|e| test_rpc::Error::ServiceStop(e.to_string()))?;
-
- // Wait until the service is fully stopped
- wait_for_service_status(&service, |status| {
- status.current_state == ServiceState::Stopped
- })
- .await?;
-
- // Get the current service configuration
- let config = service.query_config().map_err(|e| {
- test_rpc::Error::ServiceNotFound(format!(
- "Failed to query service config: {}",
- error_with_source(&e)
- ))
- })?;
-
- let executable_path = "C:\\Program Files\\Mullvad VPN\\resources\\mullvad-daemon.exe";
- let launch_arguments = vec![
- OsString::from("--run-as-service"),
- OsString::from(verbosity),
- ];
-
- // Update the service binary arguments
- let updated_config = ServiceInfo {
- name: config.display_name.clone(),
- display_name: config.display_name.clone(),
- service_type: config.service_type,
- start_type: config.start_type,
- error_control: config.error_control,
- executable_path: std::path::PathBuf::from(executable_path),
- launch_arguments,
- dependencies: config.dependencies.clone(),
- account_name: config.account_name.clone(),
- account_password: None,
- };
-
- // Apply the updated configuration
- service.change_config(&updated_config).map_err(|e| {
- test_rpc::Error::ServiceChange(format!("Update service config: {}", error_with_source(&e)))
- })?;
-
- // Start the service
- service.start::<String>(&[]).map_err(|e| {
- test_rpc::Error::ServiceNotFound(format!(
- "Failed to start service: {}",
- error_with_source(&e)
- ))
- })?;
-
- // Wait until the service is fully started
- wait_for_service_status(&service, |status| {
- status.current_state == ServiceState::Running
- })
- .await?;
-
- Ok(())
-}
-
-#[cfg(target_os = "macos")]
-#[allow(clippy::unused_async)]
-pub async fn set_daemon_log_level(_verbosity_level: Verbosity) -> Result<(), test_rpc::Error> {
- // TODO: Not implemented
- log::warn!("Setting log level is not implemented on macOS");
- Ok(())
-}
-
-#[cfg(target_os = "linux")]
-#[derive(Debug)]
-struct EnvVar {
- var: String,
- value: String,
-}
-
-#[cfg(target_os = "linux")]
-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
- )
- }
-}
-
-#[cfg(target_os = "linux")]
-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(())
-}
-
-#[cfg(target_os = "windows")]
-pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), test_rpc::Error> {
- // Set environment globally (not for service) to prevent it from being lost on upgrade
- for (k, v) in env.clone() {
- tokio::process::Command::new("setx")
- .arg("/m")
- .args([k, v])
- .status()
- .await
- .map_err(|e| test_rpc::Error::Registry(e.to_string()))?;
- }
- // Persist the changed environment variables, such that we can retrieve them at will.
- use winreg::{RegKey, enums::*};
- let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
- let path = Path::new(MULLVAD_WIN_REGISTRY).join("Environment");
- let (registry, _) = hklm.create_subkey(&path).map_err(|error| {
- test_rpc::Error::Registry(format!("Failed to open Mullvad VPN subkey: {error}"))
- })?;
- for (k, v) in env {
- registry.set_value(k, &v).map_err(|error| {
- test_rpc::Error::Registry(format!("Failed to set Environment var: {error}"))
- })?;
- }
-
- // Restart service
- stop_app().await?;
- start_app().await?;
-
- Ok(())
-}
-
-#[cfg(target_os = "windows")]
-pub fn get_system_path_var() -> Result<String, test_rpc::Error> {
- use winreg::{enums::*, *};
-
- let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
- let key = hklm
- .open_subkey("SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment")
- .map_err(|error| {
- test_rpc::Error::Registry(format!("Failed to open Environment subkey: {error}"))
- })?;
-
- let path: String = key
- .get_value("Path")
- .map_err(|error| test_rpc::Error::Registry(format!("Failed to get PATH: {error}")))?;
-
- Ok(path)
-}
-
-#[cfg(target_os = "macos")]
-pub async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), test_rpc::Error> {
- tokio::task::spawn_blocking(|| {
- let mut parsed_plist = plist::Value::from_file(PLIST_OVERRIDE_FILE).map_err(|error| {
- test_rpc::Error::ServiceNotFound(format!("failed to parse plist: {error}"))
- })?;
-
- let mut vars = plist::Dictionary::new();
- for (k, v) in env {
- // Set environment globally (not for service) to prevent it from being lost on upgrade
- std::process::Command::new("launchctl")
- .arg("setenv")
- .args([&k, &v])
- .status()
- .map_err(|e| test_rpc::Error::ServiceChange(e.to_string()))?;
- vars.insert(k, plist::Value::String(v));
- }
-
- // Add permanent env var
- parsed_plist
- .as_dictionary_mut()
- .ok_or_else(|| test_rpc::Error::ServiceChange("plist missing dict".to_owned()))?
- .insert(
- "EnvironmentVariables".to_owned(),
- plist::Value::Dictionary(vars),
- );
-
- let daemon_plist = std::fs::File::create(PLIST_OVERRIDE_FILE)
- .map_err(|e| test_rpc::Error::ServiceChange(format!("failed to open plist: {e}")))?;
-
- parsed_plist
- .to_writer_xml(daemon_plist)
- .map_err(|e| test_rpc::Error::ServiceChange(format!("failed to replace plist: {e}")))?;
-
- Ok::<(), test_rpc::Error>(())
- })
- .await
- .unwrap()?;
-
- // Restart service
- set_launch_daemon_state(false).await?;
- tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
- set_launch_daemon_state(true).await?;
- tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
- Ok(())
-}
-
-#[cfg(target_os = "macos")]
-async fn set_launch_daemon_state(on: bool) -> Result<(), test_rpc::Error> {
- let mut launchctl = tokio::process::Command::new("launchctl");
- if on {
- launchctl
- .args(["load", "-w", PLIST_OVERRIDE_FILE])
- .status()
- .await
- .map_err(|e| test_rpc::Error::ServiceStart(e.to_string()))?;
- } else {
- launchctl
- .args(["unload", "-w", PLIST_OVERRIDE_FILE])
- .status()
- .await
- .map_err(|e| test_rpc::Error::ServiceStop(e.to_string()))?;
- }
- Ok(())
-}
-
-#[cfg(target_os = "linux")]
-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"
-/// ```
-#[cfg(target_os = "linux")]
-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:?}"))
-}
-
-#[cfg(target_os = "windows")]
-pub async fn get_daemon_environment() -> Result<HashMap<String, String>, test_rpc::Error> {
- use winreg::{RegKey, enums::*};
-
- let env =
- tokio::task::spawn_blocking(|| -> Result<HashMap<String, String>, test_rpc::Error> {
- let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
- let key = hklm.open_subkey(MULLVAD_WIN_REGISTRY).map_err(|error| {
- test_rpc::Error::Registry(format!("Failed to open Mullvad VPN subkey: {error}"))
- })?;
-
- // The Strings will be quoted (surrounded by ") when read from the registry - we should
- // trim that!
- let trim = |string: String| string.trim_matches('"').to_owned();
- let env = key
- .open_subkey("Environment")
- .map_err(|error| {
- test_rpc::Error::Registry(
- format!("Failed to open Environment subkey: {error}",),
- )
- })?
- .enum_values()
- .filter_map(|x| x.inspect_err(|err| log::trace!("{err}")).ok())
- .map(|(k, v)| (trim(k), trim(v.to_string())))
- .collect();
- Ok(env)
- })
- .await
- .map_err(test_rpc::Error::from_tokio_join_error)??;
-
- Ok(env)
-}
-
-#[cfg(target_os = "macos")]
-pub async fn get_daemon_environment() -> Result<HashMap<String, String>, test_rpc::Error> {
- let plist = tokio::task::spawn_blocking(|| {
- let parsed_plist = plist::Value::from_file(PLIST_OVERRIDE_FILE).map_err(|error| {
- test_rpc::Error::ServiceNotFound(format!("failed to parse plist: {error}"))
- })?;
-
- Ok::<plist::Value, test_rpc::Error>(parsed_plist)
- })
- .await
- .map_err(test_rpc::Error::from_tokio_join_error)??;
-
- let plist_tree = plist
- .as_dictionary()
- .ok_or_else(|| test_rpc::Error::ServiceNotFound("plist missing dict".to_owned()))?;
- let Some(env_vars) = plist_tree.get("EnvironmentVariables") else {
- // `EnvironmentVariables` does not exist in plist file, so there are no env variables to
- // parse.
- return Ok(HashMap::new());
- };
- let env_vars = env_vars.as_dictionary().ok_or_else(|| {
- test_rpc::Error::ServiceNotFound("`EnvironmentVariables` is not a dict".to_owned())
- })?;
-
- let env = env_vars
- .clone()
- .into_iter()
- .filter_map(|(key, value)| Some((key, value.into_string()?)))
- .collect();
-
- Ok(env)
-}
-
-#[cfg(target_os = "linux")]
-enum ServiceState {
- Running,
- Inactive,
-}
-
-#[cfg(target_os = "linux")]
-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(())
-}
-
-#[cfg(target_os = "macos")]
-pub fn get_os_version() -> Result<OsVersion, test_rpc::Error> {
- use test_rpc::meta::MacosVersion;
-
- let version = talpid_platform_metadata::MacosVersion::new()
- .inspect_err(|error| {
- log::error!("Failed to obtain OS version: {error}");
- })
- .map_err(|_| test_rpc::Error::Syscall)?;
-
- Ok(OsVersion::Macos(MacosVersion {
- major: version.major_version(),
- }))
-}
-
-#[cfg(target_os = "windows")]
-pub fn get_os_version() -> Result<OsVersion, test_rpc::Error> {
- use test_rpc::meta::WindowsVersion;
-
- let version = talpid_platform_metadata::WindowsVersion::new()
- .inspect_err(|error| {
- log::error!("Failed to obtain OS version: {error}");
- })
- .map_err(|_| test_rpc::Error::Syscall)?;
-
- Ok(OsVersion::Windows(WindowsVersion {
- major: version.release_version().0,
- }))
-}
-
-#[cfg(target_os = "linux")]
-pub fn get_os_version() -> Result<OsVersion, test_rpc::Error> {
- Ok(OsVersion::Linux)
-}
-
-pub fn get_daemon_status() -> ServiceStatus {
- let rpc_socket_exists = Path::new(SOCKET_PATH).exists();
-
- // On Windows, we must also make sure service isn't in a pending state, since interacting with
- // the service may fail even if there is a working named pipe.
- #[cfg(target_os = "windows")]
- let service_is_started =
- get_daemon_system_service_status().unwrap_or(ServiceStatus::NotRunning);
-
- // NOTE: May not be necessary on non-Windows
- #[cfg(not(target_os = "windows"))]
- let service_is_started = ServiceStatus::Running;
-
- match (rpc_socket_exists, service_is_started) {
- (true, ServiceStatus::Running) => ServiceStatus::Running,
- _ => ServiceStatus::NotRunning,
- }
-}
-
-#[cfg(test)]
-mod test {
-
- #[cfg(target_os = "linux")]
- #[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");
- }
-}