diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-09-11 16:10:31 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-09-12 11:29:56 +0200 |
| commit | eeb8da48efab6b635d11fb92ab57a3db72d486bd (patch) | |
| tree | 6ab7adabe89746ad1e800e0720565974a9204896 /test/test-runner/src/sys.rs | |
| parent | 295eff0a7d5b0b2ab388bab47035a68d7169f7d1 (diff) | |
| download | mullvadvpn-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.rs | 954 |
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"); - } -} |
