diff options
Diffstat (limited to 'test/test-runner/src/sys')
| -rw-r--r-- | test/test-runner/src/sys/linux.rs | 281 | ||||
| -rw-r--r-- | test/test-runner/src/sys/macos.rs | 148 | ||||
| -rw-r--r-- | test/test-runner/src/sys/mod.rs | 62 | ||||
| -rw-r--r-- | test/test-runner/src/sys/windows.rs | 443 |
4 files changed, 934 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"); + } +} diff --git a/test/test-runner/src/sys/macos.rs b/test/test-runner/src/sys/macos.rs new file mode 100644 index 0000000000..7febdb24d3 --- /dev/null +++ b/test/test-runner/src/sys/macos.rs @@ -0,0 +1,148 @@ +use std::collections::HashMap; +use test_rpc::{meta::OsVersion, mullvad_daemon::Verbosity}; + +const PLIST_OVERRIDE_FILE: &str = "/Library/LaunchDaemons/net.mullvad.daemon.plist"; + +/// Restart the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +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. +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. +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(()) +} + +#[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(()) +} + +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(()) +} + +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(()) +} + +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) +} + +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(), + })) +} diff --git a/test/test-runner/src/sys/mod.rs b/test/test-runner/src/sys/mod.rs new file mode 100644 index 0000000000..48b539739e --- /dev/null +++ b/test/test-runner/src/sys/mod.rs @@ -0,0 +1,62 @@ +use std::path::Path; +use test_rpc::mullvad_daemon::SOCKET_PATH; +use test_rpc::mullvad_daemon::ServiceStatus; + +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(target_os = "windows")] +pub use windows::*; + +#[cfg(target_os = "linux")] +mod linux; + +#[cfg(target_os = "linux")] +pub use linux::*; + +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "macos")] +pub use macos::*; + +#[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(()) +} + +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, + } +} diff --git a/test/test-runner/src/sys/windows.rs b/test/test-runner/src/sys/windows.rs new file mode 100644 index 0000000000..a8ef5e4f0e --- /dev/null +++ b/test/test-runner/src/sys/windows.rs @@ -0,0 +1,443 @@ +use std::collections::HashMap; +use std::ffi::OsString; +use std::io; +use std::path::Path; +use test_rpc::mullvad_daemon::ServiceStatus; +use test_rpc::{meta::OsVersion, mullvad_daemon::Verbosity}; +use windows_service::{ + service::{Service, ServiceAccess, ServiceInfo, ServiceState}, + service_manager::{ServiceManager, ServiceManagerAccess}, +}; +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, +}; + +const MULLVAD_WIN_REGISTRY: &str = r"SYSTEM\CurrentControlSet\Services\Mullvad VPN"; + +pub fn reboot() -> Result<(), test_rpc::Error> { + 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(()) +} + +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(()) +} + +/// Restart the Mullvad VPN application. +/// +/// This function waits for the app to successfully start again. +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. +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. +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. +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. +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(()) +} + +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) +} + +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) +} + +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(), + )) +} + +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(()) +} + +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(()) +} + +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) +} + +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) +} + +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, + })) +} |
