diff --git a/mullvad-daemon/src/target_state.rs b/mullvad-daemon/src/target_state.rs index cc3094b9863c..1004ca3e8562 100644 --- a/mullvad-daemon/src/target_state.rs +++ b/mullvad-daemon/src/target_state.rs @@ -1,5 +1,6 @@ use mullvad_types::states::TargetState; use std::{ + future::Future, ops::Deref, path::{Path, PathBuf}, }; @@ -21,48 +22,76 @@ impl PersistentTargetState { /// Initialize using the current target state (if there is one) pub async fn new(cache_dir: &Path) -> Self { let cache_path = cache_dir.join(TARGET_START_STATE_FILE); - let mut update_cache = false; - let state = match fs::read_to_string(&cache_path).await { + let TargetStateInner { + state, + update_cache, + } = Self::read_target_state(&cache_path, fs::read_to_string).await; + let state = PersistentTargetState { + state, + cache_path, + locked: false, + }; + if update_cache { + state.save().await; + } + state + } + + /// Construct a [`TargetState`] from cache. + /// + /// `read_cache` allows the caller to decide how to read from a cache of + /// [`TargetState`]. + /// + /// This function will always succeed, even in the presence of IO + /// operations. Errors are handled gracefully by defaulting to safe target + /// states if necessary. + async fn read_target_state(cache: &Path, read_cache: F) -> TargetStateInner + where + F: FnOnce(PathBuf) -> R, + R: Future>, + { + match read_cache(cache.to_path_buf()).await { Ok(content) => serde_json::from_str(&content) .map(|state| { log::info!( "Loaded cached target state \"{}\" from {}", state, - cache_path.display() + cache.display() ); - state + TargetStateInner { + state, + update_cache: false, + } }) .unwrap_or_else(|error| { log::error!( "{}", error.display_chain_with_msg("Failed to parse cached target tunnel state") ); - update_cache = true; - TargetState::Secured + TargetStateInner { + state: TargetState::Secured, + update_cache: true, + } }), + + Err(error) if error.kind() == io::ErrorKind::NotFound => { + log::debug!("No cached target state to load"); + TargetStateInner { + state: DEFAULT_TARGET_STATE, + update_cache: false, + } + } Err(error) => { - if error.kind() == io::ErrorKind::NotFound { - log::debug!("No cached target state to load"); - DEFAULT_TARGET_STATE - } else { - log::error!( - "{}", - error.display_chain_with_msg("Failed to read cached target tunnel state") - ); - update_cache = true; - TargetState::Secured + log::error!( + "{}", + error.display_chain_with_msg("Failed to read cached target tunnel state") + ); + TargetStateInner { + state: TargetState::Secured, + update_cache: true, } } - }; - let state = PersistentTargetState { - state, - cache_path, - locked: false, - }; - if update_cache { - state.save().await; } - state } /// Override the current target state, if there is one @@ -153,3 +182,70 @@ impl Deref for PersistentTargetState { &self.state } } + +/// The result of calling `read_target_state`. +struct TargetStateInner { + state: TargetState, + /// In some circumstances, the target state cache should be updated on disk + /// upon initialization a [`PersistentTargetState`]. This is signaled to the + /// constructor of [`PersistentTargetState`] by setting this value to + /// `true`. + update_cache: bool, +} + +impl Deref for TargetStateInner { + type Target = TargetState; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +#[cfg(test)] +mod test { + use super::*; + + static DUMMY_CACHE_DIR: &str = "target-state-test"; + + /// If no target state cache exist, the default target state is used. This + /// is the most basic check. + #[tokio::test] + async fn test_target_state_initialization_empty() { + let target_state = + PersistentTargetState::read_target_state(Path::new(DUMMY_CACHE_DIR), |_| async { + // A completely blank slate. No target state cache file has been created yet. + Err(io::ErrorKind::NotFound.into()) + }) + .await; + assert_eq!(*target_state, DEFAULT_TARGET_STATE); + } + + /// If a target state cache exist with some target state, the state can be + /// read-back successfully. + #[tokio::test] + async fn test_target_state_initialization_existing() { + for cached_state in [TargetState::Secured, TargetState::Unsecured] { + let target_state = + PersistentTargetState::read_target_state(Path::new(DUMMY_CACHE_DIR), |_| async { + Ok(serde_json::to_string(&cached_state).unwrap()) + }) + .await; + assert_eq!(*target_state, cached_state); + } + } + + /// The state can not be read-back successfully if the state file has become + /// corrupt. In such cases, initializing a [`PersistentTargetState`] should + /// yield a "better safe than sorry"-target state of `Secured`. + #[tokio::test] + async fn test_target_corrupt_state_cache() { + let target_state = + PersistentTargetState::read_target_state(Path::new(DUMMY_CACHE_DIR), |_| async { + // Intentionally corrupt the target state cache. + Ok("Not a valid target state".to_string()) + }) + .await; + // Reading back a corrupt target state cache should yield `TargetState::Secured`. + assert_eq!(*target_state, TargetState::Secured); + } +} diff --git a/test/test-manager/src/tests/account.rs b/test/test-manager/src/tests/account.rs index 92c95346b286..78eb42adc4f8 100644 --- a/test/test-manager/src/tests/account.rs +++ b/test/test-manager/src/tests/account.rs @@ -324,7 +324,7 @@ pub async fn test_automatic_wireguard_rotation( .pubkey; // Stop daemon - rpc.set_mullvad_daemon_service_state(false) + rpc.stop_mullvad_daemon() .await .expect("Could not stop system service"); @@ -334,7 +334,7 @@ pub async fn test_automatic_wireguard_rotation( .expect("Could not change device.json to have an old created timestamp"); // Start daemon - rpc.set_mullvad_daemon_service_state(true) + rpc.start_mullvad_daemon() .await .expect("Could not start system service"); diff --git a/test/test-manager/src/tests/tunnel_state.rs b/test/test-manager/src/tests/tunnel_state.rs index eb78828fd0d6..9ae817c7ee9d 100644 --- a/test/test-manager/src/tests/tunnel_state.rs +++ b/test/test-manager/src/tests/tunnel_state.rs @@ -335,3 +335,54 @@ pub async fn test_connected_state( Ok(()) } + +/// Verify that the app defaults to the connecting state if it is started with a +/// corrupt state cache. +#[test_function] +pub async fn test_connecting_state_when_corrupted_state_cache( + _: TestContext, + rpc: ServiceClient, + mullvad_client: ManagementServiceClient, +) -> Result<(), Error> { + // The test should start in a disconnected state. Normally this would be + // preserved when restarting the app, i.e. the user would still be + // disconnected after a successfull restart. However, as we will + // intentionally corrupt the state target cache the user should end up in + // the connecting/connected state, *not in the disconnected state*, upon + // restart. + + // Stopping the app should write to the state target cache. + log::info!("Stopping the app"); + rpc.stop_mullvad_daemon().await?; + + // Intentionally corrupt the state cache. Note that we can not simply remove + // the cache, as this will put the app in the default target state which is + // 'unsecured'. + log::info!("Figuring out where state cache resides on test runner .."); + let state_cache = rpc + .find_mullvad_app_cache_dir() + .await? + .join("target-start-state.json"); + log::info!( + "Intentionally writing garbage to the state cache {file}", + file = state_cache.display() + ); + rpc.write_file(state_cache, "cookie was here".into()) + .await?; + + // Start the app & make sure that we start in the 'connecting state'. The + // side-effect of this is that no network traffic is allowed to leak. + log::info!("Starting the app back up again"); + rpc.start_mullvad_daemon().await?; + wait_for_tunnel_state(mullvad_client.clone(), |state| !state.is_disconnected()) + .await + .map_err(|err| { + log::error!("App did not start in an expected state. \ + App is not in either `Connecting` or `Connected` state after starting with corrupt state cache! \ + There is a possibility of leaks during app startup "); + err + })?; + log::info!("App successfully recovered from a corrupt tunnel state cache."); + + Ok(()) +} diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs index 6be77afb4035..29f86b944370 100644 --- a/test/test-rpc/src/client.rs +++ b/test/test-rpc/src/client.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + path::Path, time::{Duration, SystemTime}, }; @@ -51,7 +52,7 @@ impl ServiceClient { self.client.uninstall_app(ctx, env).await? } - /// Execute a program. + /// Execute a program with additional environment-variables set. pub async fn exec_env< I: IntoIterator, M: IntoIterator, @@ -151,6 +152,13 @@ impl ServiceClient { .await? } + /// Returns path of Mullvad app cache directorie on the test runner. + pub async fn find_mullvad_app_cache_dir(&self) -> Result { + self.client + .get_mullvad_app_cache_dir(tarpc::context::current()) + .await? + } + /// Send TCP packet pub async fn send_tcp( &self, @@ -213,6 +221,51 @@ impl ServiceClient { .await? } + /// Restarts the app. + /// + /// Shuts down a running app, making it disconnect from any current tunnel + /// connection before starting the app again. + /// + /// # Note + /// This function will return *after* the app is running again, thus + /// blocking execution until then. + pub async fn restart_mullvad_daemon(&self) -> Result<(), Error> { + let _ = self + .client + .restart_mullvad_daemon(tarpc::context::current()) + .await?; + Ok(()) + } + + /// Stop the app. + /// + /// Shuts down a running app, making it disconnect from any current tunnel + /// connection and making it write to caches. + /// + /// # Note + /// This function will return *after* the app has been stopped, thus + /// blocking execution until then. + pub async fn stop_mullvad_daemon(&self) -> Result<(), Error> { + let _ = self + .client + .stop_mullvad_daemon(tarpc::context::current()) + .await?; + Ok(()) + } + + /// Start the app. + /// + /// # Note + /// This function will return *after* the app has been started, thus + /// blocking execution until then. + pub async fn start_mullvad_daemon(&self) -> Result<(), Error> { + let _ = self + .client + .start_mullvad_daemon(tarpc::context::current()) + .await?; + Ok(()) + } + pub async fn set_daemon_log_level( &self, verbosity_level: mullvad_daemon::Verbosity, @@ -247,6 +300,21 @@ impl ServiceClient { .await? } + pub async fn write_file(&self, dest: impl AsRef, bytes: Vec) -> Result<(), Error> { + log::debug!( + "Writing {bytes} bytes to \"{file}\"", + bytes = bytes.len(), + file = dest.as_ref().display() + ); + self.client + .write_file( + tarpc::context::current(), + dest.as_ref().to_path_buf(), + bytes, + ) + .await? + } + pub async fn reboot(&mut self) -> Result<(), Error> { log::debug!("Rebooting server"); @@ -262,23 +330,6 @@ impl ServiceClient { Ok(()) } - pub async fn set_mullvad_daemon_service_state(&self, on: bool) -> Result<(), Error> { - self.client - .set_mullvad_daemon_service_state(tarpc::context::current(), on) - .await??; - - self.mullvad_daemon_wait_for_state(|state| { - if on { - state == ServiceStatus::Running - } else { - state == ServiceStatus::NotRunning - } - }) - .await?; - - Ok(()) - } - pub async fn make_device_json_old(&self) -> Result<(), Error> { self.client .make_device_json_old(tarpc::context::current()) diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs index 6968a3c613e5..73ab467ab8d4 100644 --- a/test/test-rpc/src/lib.rs +++ b/test/test-rpc/src/lib.rs @@ -120,6 +120,8 @@ mod service { /// Returns all Mullvad app files, directories, and other data found on the system. async fn find_mullvad_app_traces() -> Result, Error>; + async fn get_mullvad_app_cache_dir() -> Result; + /// Send TCP packet async fn send_tcp( interface: Option, @@ -149,6 +151,15 @@ mod service { /// Perform DNS resolution. async fn resolve_hostname(hostname: String) -> Result, Error>; + /// Restart the Mullvad VPN application. + async fn restart_mullvad_daemon() -> Result<(), Error>; + + /// Stop the Mullvad VPN application. + async fn stop_mullvad_daemon() -> Result<(), Error>; + + /// Start the Mullvad VPN application. + async fn start_mullvad_daemon() -> Result<(), Error>; + /// Sets the log level of the daemon service, the verbosity level represents the number of /// `-v`s passed on the command line. This will restart the daemon system service. async fn set_daemon_log_level( @@ -161,9 +172,10 @@ mod service { /// Copy a file from `src` to `dest` on the test runner. async fn copy_file(src: String, dest: String) -> Result<(), Error>; - async fn reboot() -> Result<(), Error>; + /// Write arbitrary bytes to some file `dest` on the test runner. + async fn write_file(dest: PathBuf, bytes: Vec) -> Result<(), Error>; - async fn set_mullvad_daemon_service_state(on: bool) -> Result<(), Error>; + async fn reboot() -> Result<(), Error>; async fn make_device_json_old() -> Result<(), Error>; } diff --git a/test/test-runner/Cargo.toml b/test/test-runner/Cargo.toml index 2c431ffa4982..64461d76aab8 100644 --- a/test/test-runner/Cargo.toml +++ b/test/test-runner/Cargo.toml @@ -18,7 +18,7 @@ serde_json = { workspace = true } tokio-serde = { workspace = true } libc = "0.2" -chrono = { workspace = true } +chrono = { workspace = true, features = ["serde"] } test-rpc = { path = "../test-rpc" } mullvad-paths = { path = "../../mullvad-paths" } diff --git a/test/test-runner/src/app.rs b/test/test-runner/src/app.rs index 43aca23abb68..f4e1fc3c5303 100644 --- a/test/test-runner/src/app.rs +++ b/test/test-runner/src/app.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use std::path::Path; +use std::path::{Path, PathBuf}; use test_rpc::{AppTrace, Error}; @@ -14,21 +14,18 @@ pub fn find_traces() -> Result, Error> { Error::Syscall })?; - let mut traces = vec![ + let caches = find_cache_traces()?; + let traces = vec![ Path::new(r"C:\Program Files\Mullvad VPN"), // NOTE: This only works as of `499c06decda37dc639e5f` in the Mullvad app. // Older builds have no way of silently fully uninstalling the app. Path::new(r"C:\ProgramData\Mullvad VPN"), // NOTE: Works as of `4116ebc` (Mullvad app). &settings_dir, + &caches, ]; - filter_non_existent_paths(&mut traces)?; - - Ok(traces - .into_iter() - .map(|path| AppTrace::Path(path.to_path_buf())) - .collect()) + Ok(existing_paths(&traces)) } #[cfg(target_os = "linux")] @@ -36,10 +33,11 @@ pub fn find_traces() -> Result, Error> { // TODO: Check GUI data // TODO: Check temp data - let mut traces = vec![ + let caches = find_cache_traces()?; + let traces = vec![ Path::new(r"/etc/mullvad-vpn/"), Path::new(r"/var/log/mullvad-vpn/"), - Path::new(r"/var/cache/mullvad-vpn/"), + &caches, Path::new(r"/opt/Mullvad VPN/"), // management interface socket Path::new(r"/var/run/mullvad-vpn"), @@ -55,12 +53,11 @@ pub fn find_traces() -> Result, Error> { Path::new(r"/usr/share/fish/vendor_completions.d/mullvad.fish"), ]; - filter_non_existent_paths(&mut traces)?; + Ok(existing_paths(&traces)) +} - Ok(traces - .into_iter() - .map(|path| AppTrace::Path(path.to_path_buf())) - .collect()) +pub fn find_cache_traces() -> Result { + mullvad_paths::get_cache_dir().map_err(|error| Error::FileSystem(error.to_string())) } #[cfg(target_os = "macos")] @@ -68,10 +65,11 @@ pub fn find_traces() -> Result, Error> { // TODO: Check GUI data // TODO: Check temp data - let mut traces = vec![ + let caches = find_cache_traces()?; + let traces = vec![ Path::new(r"/Applications/Mullvad VPN.app/"), Path::new(r"/var/log/mullvad-vpn/"), - Path::new(r"/Library/Caches/mullvad-vpn/"), + &caches, // management interface socket Path::new(r"/var/run/mullvad-vpn"), // launch daemon @@ -84,26 +82,16 @@ pub fn find_traces() -> Result, Error> { Path::new(r"/usr/local/share/fish/vendor_completions.d/mullvad.fish"), ]; - filter_non_existent_paths(&mut traces)?; - - Ok(traces - .into_iter() - .map(|path| AppTrace::Path(path.to_path_buf())) - .collect()) + Ok(existing_paths(&traces)) } -fn filter_non_existent_paths(paths: &mut Vec<&Path>) -> Result<(), Error> { - for i in (0..paths.len()).rev() { - let path_exists = paths[i].try_exists().map_err(|error| { - log::error!("Failed to check whether path exists: {error}"); - Error::Syscall - })?; - if !path_exists { - paths.swap_remove(i); - continue; - } - } - Ok(()) +/// Find all present app traces on the test runner. +fn existing_paths(paths: &[&Path]) -> Vec { + paths + .iter() + .filter(|&path| path.try_exists().is_ok_and(|exists| exists)) + .map(|path| AppTrace::Path(path.to_path_buf())) + .collect() } pub async fn make_device_json_old() -> Result<(), Error> { diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs index ebf0d1e47454..fe85e6f89fe0 100644 --- a/test/test-runner/src/main.rs +++ b/test/test-runner/src/main.rs @@ -3,7 +3,7 @@ use logging::LOGGER; use std::{ collections::{BTreeMap, HashMap}, net::{IpAddr, SocketAddr}, - path::Path, + path::{Path, PathBuf}, }; use tarpc::context; @@ -114,6 +114,13 @@ impl Service for TestServer { app::find_traces() } + async fn get_mullvad_app_cache_dir( + self, + _: context::Context, + ) -> Result { + app::find_cache_traces() + } + async fn send_tcp( self, _: context::Context, @@ -140,7 +147,7 @@ impl Service for TestServer { interface: Option, destination: IpAddr, ) -> Result<(), test_rpc::Error> { - net::send_ping(interface.as_ref().map(String::as_str), destination).await + net::send_ping(interface.as_deref(), destination).await } async fn geoip_lookup( @@ -219,6 +226,20 @@ impl Service for TestServer { logging::get_mullvad_app_logs().await } + async fn restart_mullvad_daemon(self, _: context::Context) -> Result<(), test_rpc::Error> { + sys::restart_app().await + } + + /// Stop the Mullvad VPN application. + async fn stop_mullvad_daemon(self, _: context::Context) -> Result<(), test_rpc::Error> { + sys::stop_app().await + } + + /// Start the Mullvad VPN application. + async fn start_mullvad_daemon(self, _: context::Context) -> Result<(), test_rpc::Error> { + sys::start_app().await + } + async fn set_daemon_log_level( self, _: context::Context, @@ -248,16 +269,27 @@ impl Service for TestServer { Ok(()) } - async fn reboot(self, _: context::Context) -> Result<(), test_rpc::Error> { - sys::reboot() - } - - async fn set_mullvad_daemon_service_state( + /// Write a slice as the entire contents of a file. + /// + /// See the documention of [`tokio::fs::write`] for details of the behavior. + async fn write_file( self, _: context::Context, - on: bool, + dest: PathBuf, + bytes: Vec, ) -> Result<(), test_rpc::Error> { - sys::set_mullvad_daemon_service_state(on).await + tokio::fs::write(&dest, bytes).await.map_err(|error| { + log::error!( + "Failed to write to \"{dest}\": {error}", + dest = dest.display() + ); + test_rpc::Error::Syscall + })?; + Ok(()) + } + + async fn reboot(self, _: context::Context) -> Result<(), test_rpc::Error> { + sys::reboot() } async fn make_device_json_old(self, _: context::Context) -> Result<(), test_rpc::Error> { diff --git a/test/test-runner/src/net.rs b/test/test-runner/src/net.rs index f40aece4c9bb..a4a7a2db47bd 100644 --- a/test/test-runner/src/net.rs +++ b/test/test-runner/src/net.rs @@ -35,7 +35,7 @@ pub async fn send_tcp( }; #[cfg(target_os = "macos")] - sock.bind_device_by_index(Some(interface_index)) + sock.bind_device_by_index_v4(Some(interface_index)) .map_err(|error| { log::error!("Failed to set IP_BOUND_IF on socket: {error}"); test_rpc::Error::SendTcp @@ -102,7 +102,7 @@ pub async fn send_udp( }; #[cfg(target_os = "macos")] - sock.bind_device_by_index(Some(interface_index)) + sock.bind_device_by_index_v4(Some(interface_index)) .map_err(|error| { log::error!("Failed to set IP_BOUND_IF on socket: {error}"); test_rpc::Error::SendUdp diff --git a/test/test-runner/src/sys.rs b/test/test-runner/src/sys.rs index 93d148a2b5c5..562c608120bb 100644 --- a/test/test-runner/src/sys.rs +++ b/test/test-runner/src/sys.rs @@ -193,16 +193,119 @@ ExecStart=/usr/bin/mullvad-daemon --disable-stdout-timestamps {verbosity}"# .await .map_err(|e| test_rpc::Error::Service(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::Service(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::Service(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::Service(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::Service(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::Service(e.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(false).await?; + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + Ok(()) +} + #[cfg(target_os = "windows")] pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test_rpc::Error> { log::debug!("Setting log level"); @@ -226,6 +329,7 @@ pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test .map_err(|e| test_rpc::Error::Service(e.to_string()))?; // Stop the service + // TODO: Extract to separate function. service .stop() .map_err(|e| test_rpc::Error::Service(e.to_string()))?; @@ -266,6 +370,7 @@ pub async fn set_daemon_log_level(verbosity_level: Verbosity) -> Result<(), test .map_err(|e| test_rpc::Error::Service(e.to_string()))?; // Start the service + // TODO: Extract to separate function. service .start::(&[]) .map_err(|e| test_rpc::Error::Service(e.to_string()))?; @@ -341,17 +446,8 @@ pub async fn set_daemon_environment(env: HashMap) -> Result<(), } // Restart service - tokio::process::Command::new("net") - .args(["stop", "mullvadvpn"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - - tokio::process::Command::new("net") - .args(["start", "mullvadvpn"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; + stop_app().await?; + start_app().await?; Ok(()) } @@ -427,51 +523,6 @@ pub async fn set_daemon_environment(env: HashMap) -> Result<(), Ok(()) } -#[cfg(target_os = "linux")] -pub async fn set_mullvad_daemon_service_state(on: bool) -> Result<(), test_rpc::Error> { - if on { - tokio::process::Command::new("systemctl") - .args(["start", "mullvad-daemon"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - wait_for_service_state(ServiceState::Running).await?; - } else { - tokio::process::Command::new("systemctl") - .args(["stop", "mullvad-daemon"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - wait_for_service_state(ServiceState::Inactive).await?; - } - Ok(()) -} - -#[cfg(target_os = "windows")] -pub async fn set_mullvad_daemon_service_state(on: bool) -> Result<(), test_rpc::Error> { - if on { - tokio::process::Command::new("net") - .args(["start", "mullvadvpn"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - } else { - tokio::process::Command::new("net") - .args(["stop", "mullvadvpn"]) - .status() - .await - .map_err(|e| test_rpc::Error::Service(e.to_string()))?; - } - Ok(()) -} - -#[cfg(target_os = "macos")] -pub async fn set_mullvad_daemon_service_state(on: bool) -> Result<(), test_rpc::Error> { - set_launch_daemon_state(on).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> { tokio::process::Command::new("launchctl")