diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-02-13 21:58:50 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-03-05 23:32:03 +0100 |
| commit | 25e5ee878516830c559dfea1314d1ad1f7bdef3f (patch) | |
| tree | 612522af4c0255432595e9c299ee033723ad060c | |
| parent | e8984124a3c42fbef5c805be9cffe196c8c426bb (diff) | |
| download | mullvadvpn-25e5ee878516830c559dfea1314d1ad1f7bdef3f.tar.xz mullvadvpn-25e5ee878516830c559dfea1314d1ad1f7bdef3f.zip | |
Add launch step to installer downloader
12 files changed, 98 insertions, 14 deletions
diff --git a/installer-downloader/src/controller.rs b/installer-downloader/src/controller.rs index c5aebaa371..e7b8a6bb40 100644 --- a/installer-downloader/src/controller.rs +++ b/installer-downloader/src/controller.rs @@ -161,9 +161,7 @@ where }); let ui_downloader = UiAppDownloader::new(self_, downloader); - let _ = tx.send(tokio::spawn(async move { - let _ = app::install_and_upgrade(ui_downloader).await; - })); + let _ = tx.send(tokio::spawn(app::install_and_upgrade(ui_downloader))); }); active_download = rx.await.ok(); } diff --git a/installer-downloader/src/delegate.rs b/installer-downloader/src/delegate.rs index beca2e6282..3e82d2ef31 100644 --- a/installer-downloader/src/delegate.rs +++ b/installer-downloader/src/delegate.rs @@ -62,6 +62,9 @@ pub trait AppDelegate { /// Hide beta text fn hide_beta_text(&mut self); + /// Exit the application + fn quit(&mut self); + /// Create queue for scheduling actions on UI thread fn queue(&self) -> Self::Queue; } diff --git a/installer-downloader/src/ui_downloader.rs b/installer-downloader/src/ui_downloader.rs index 5658297082..7ae37df3e5 100644 --- a/installer-downloader/src/ui_downloader.rs +++ b/installer-downloader/src/ui_downloader.rs @@ -88,6 +88,25 @@ impl<Delegate: AppDelegate, Downloader: AppDownloader + Send + 'static> AppDownl } } } + + async fn install(&mut self) -> Result<(), app::DownloadError> { + match self.downloader.install().await { + Ok(()) => { + self.queue.queue_main(move |self_| { + // Success! + self_.quit(); + }); + Ok(()) + } + Err(error) => { + self.queue.queue_main(move |self_| { + self_.set_download_text("ERROR: Failed to launch installer!"); + }); + + Err(error) + } + } + } } /// Implementation of [fetch::ProgressUpdater] that updates some [AppDelegate]. diff --git a/installer-downloader/src/winapi_impl/delegate.rs b/installer-downloader/src/winapi_impl/delegate.rs index 4414559e7c..353dff9691 100644 --- a/installer-downloader/src/winapi_impl/delegate.rs +++ b/installer-downloader/src/winapi_impl/delegate.rs @@ -91,6 +91,10 @@ impl AppDelegate for AppWindow { self.beta_link.set_visible(false); } + fn quit(&mut self) { + nwg::stop_thread_dispatch(); + } + fn queue(&self) -> Self::Queue { Queue { main_wnd: self.window.handle, diff --git a/installer-downloader/tests/controller.rs b/installer-downloader/tests/controller.rs index af46452e89..460e1c168a 100644 --- a/installer-downloader/tests/controller.rs +++ b/installer-downloader/tests/controller.rs @@ -36,16 +36,16 @@ impl VersionInfoProvider for FakeVersionInfoProvider { } /// Downloader for which all steps immediately succeed -pub type FakeAppDownloaderHappyPath = FakeAppDownloader<true, true>; +pub type FakeAppDownloaderHappyPath = FakeAppDownloader<true, true, true>; /// Downloader for which the download step fails -pub type FakeAppDownloaderDownloadFail = FakeAppDownloader<false, false>; +pub type FakeAppDownloaderDownloadFail = FakeAppDownloader<false, false, false>; -/// Downloader for which the final verification step fails -pub type FakeAppDownloaderVerifyFail = FakeAppDownloader<true, false>; +/// Downloader for which the verification step fails +pub type FakeAppDownloaderVerifyFail = FakeAppDownloader<true, false, false>; -impl<const A: bool, const B: bool> From<UiAppDownloaderParameters<FakeAppDelegate>> - for FakeAppDownloader<A, B> +impl<const A: bool, const B: bool, const C: bool> From<UiAppDownloaderParameters<FakeAppDelegate>> + for FakeAppDownloader<A, B, C> { fn from(params: UiAppDownloaderParameters<FakeAppDelegate>) -> Self { FakeAppDownloader { params } @@ -57,13 +57,18 @@ impl<const A: bool, const B: bool> From<UiAppDownloaderParameters<FakeAppDelegat /// Parameters: /// * EXE_SUCCEED - whether fetching the binary succeeds /// * VERIFY_SUCCEED - whether verifying the binary succeeds -pub struct FakeAppDownloader<const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool> { +/// * LAUNCH_SUCCEED - whether launching the binary succeeds +pub struct FakeAppDownloader< + const EXE_SUCCEED: bool, + const VERIFY_SUCCEED: bool, + const LAUNCH_SUCCEED: bool, +> { params: UiAppDownloaderParameters<FakeAppDelegate>, } #[async_trait::async_trait] -impl<const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool> AppDownloader - for FakeAppDownloader<EXE_SUCCEED, VERIFY_SUCCEED> +impl<const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool, const LAUNCH_SUCCEED: bool> AppDownloader + for FakeAppDownloader<EXE_SUCCEED, VERIFY_SUCCEED, LAUNCH_SUCCEED> { async fn create_cache_dir(&mut self) -> Result<(), DownloadError> { Ok(()) @@ -91,6 +96,16 @@ impl<const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool> AppDownloader ))) } } + + async fn install(&mut self) -> Result<(), DownloadError> { + if LAUNCH_SUCCEED { + Ok(()) + } else { + Err(DownloadError::InstallFailed(anyhow::anyhow!( + "install failed" + ))) + } + } } /// A fake queue that stores callbacks so that tests can run them later. @@ -140,6 +155,7 @@ pub struct DelegateState { pub download_progress: u32, pub download_progress_visible: bool, pub beta_text_visible: bool, + pub quit: bool, /// Record of method calls. pub call_log: Vec<String>, } @@ -244,6 +260,11 @@ impl AppDelegate for FakeAppDelegate { self.state.beta_text_visible = false; } + fn quit(&mut self) { + self.state.call_log.push("quit".into()); + self.state.quit = true; + } + fn queue(&self) -> Self::Queue { self.queue.clone() } @@ -311,6 +332,7 @@ async fn test_download() { queue.run_callbacks(&mut delegate); // Everything including verification should have succeeded + // Downloader should have quit assert_yaml_snapshot!(delegate.state); } diff --git a/installer-downloader/tests/snapshots/controller__download-2.snap b/installer-downloader/tests/snapshots/controller__download-2.snap index e86d9055d7..390e172778 100644 --- a/installer-downloader/tests/snapshots/controller__download-2.snap +++ b/installer-downloader/tests/snapshots/controller__download-2.snap @@ -12,6 +12,7 @@ download_button_enabled: true download_progress: 0 download_progress_visible: true beta_text_visible: false +quit: false call_log: - hide_download_progress - show_download_button diff --git a/installer-downloader/tests/snapshots/controller__download-3.snap b/installer-downloader/tests/snapshots/controller__download-3.snap index d7c86160de..6d918cf341 100644 --- a/installer-downloader/tests/snapshots/controller__download-3.snap +++ b/installer-downloader/tests/snapshots/controller__download-3.snap @@ -12,6 +12,7 @@ download_button_enabled: true download_progress: 100 download_progress_visible: true beta_text_visible: false +quit: true call_log: - hide_download_progress - show_download_button @@ -36,3 +37,4 @@ call_log: - "set_download_text: Download complete. Verifying..." - disable_cancel_button - "set_download_text: Verification successful. Starting install..." + - quit diff --git a/installer-downloader/tests/snapshots/controller__download.snap b/installer-downloader/tests/snapshots/controller__download.snap index 45bd1796ad..3d79a1922e 100644 --- a/installer-downloader/tests/snapshots/controller__download.snap +++ b/installer-downloader/tests/snapshots/controller__download.snap @@ -12,6 +12,7 @@ download_button_enabled: true download_progress: 0 download_progress_visible: false beta_text_visible: false +quit: false call_log: - hide_download_progress - show_download_button diff --git a/installer-downloader/tests/snapshots/controller__failed_verification.snap b/installer-downloader/tests/snapshots/controller__failed_verification.snap index d345795b7e..b7825fb1d5 100644 --- a/installer-downloader/tests/snapshots/controller__failed_verification.snap +++ b/installer-downloader/tests/snapshots/controller__failed_verification.snap @@ -12,6 +12,7 @@ download_button_enabled: true download_progress: 100 download_progress_visible: true beta_text_visible: false +quit: false call_log: - hide_download_progress - show_download_button diff --git a/installer-downloader/tests/snapshots/controller__fetch_version-2.snap b/installer-downloader/tests/snapshots/controller__fetch_version-2.snap index 45bd1796ad..3d79a1922e 100644 --- a/installer-downloader/tests/snapshots/controller__fetch_version-2.snap +++ b/installer-downloader/tests/snapshots/controller__fetch_version-2.snap @@ -12,6 +12,7 @@ download_button_enabled: true download_progress: 0 download_progress_visible: false beta_text_visible: false +quit: false call_log: - hide_download_progress - show_download_button diff --git a/installer-downloader/tests/snapshots/controller__fetch_version.snap b/installer-downloader/tests/snapshots/controller__fetch_version.snap index a8c1896189..24c54ce95c 100644 --- a/installer-downloader/tests/snapshots/controller__fetch_version.snap +++ b/installer-downloader/tests/snapshots/controller__fetch_version.snap @@ -12,6 +12,7 @@ download_button_enabled: false download_progress: 0 download_progress_visible: false beta_text_visible: false +quit: false call_log: - hide_download_progress - show_download_button diff --git a/mullvad-update/src/app.rs b/mullvad-update/src/app.rs index f8b0ec3414..b693520bf1 100644 --- a/mullvad-update/src/app.rs +++ b/mullvad-update/src/app.rs @@ -1,18 +1,24 @@ //! This module implements the flow of downloading and verifying the app. -use std::path::PathBuf; +use std::{path::PathBuf, time::Duration}; + +use tokio::{process::Command, time::timeout}; use crate::{ fetch::{self, ProgressUpdater}, verify::{AppVerifier, Sha256Verifier}, }; +const INSTALLER_STARTUP_TIMEOUT: Duration = Duration::from_millis(500); + #[derive(Debug)] pub enum DownloadError { CreateDir(anyhow::Error), FetchSignature(anyhow::Error), FetchApp(anyhow::Error), Verification(anyhow::Error), + Launch(std::io::Error), + InstallFailed(anyhow::Error), } /// Parameters required to construct an [AppDownloader]. @@ -36,13 +42,17 @@ pub trait AppDownloader: Send { /// Verify the app signature. async fn verify(&mut self) -> Result<(), DownloadError>; + + /// Execute installer. + async fn install(&mut self) -> Result<(), DownloadError>; } /// Download the app and signature, and verify the app's signature pub async fn install_and_upgrade(mut downloader: impl AppDownloader) -> Result<(), DownloadError> { downloader.create_cache_dir().await?; downloader.download_executable().await?; - downloader.verify().await + downloader.verify().await?; + downloader.install().await } #[derive(Clone)] @@ -92,6 +102,27 @@ impl<AppProgress: ProgressUpdater> AppDownloader for HttpAppDownloader<AppProgre .await .map_err(DownloadError::Verification) } + + async fn install(&mut self) -> Result<(), DownloadError> { + let bin_path = self.bin_path().expect("Performed after 'create_cache_dir'"); + + // Launch process + // TODO: move to launch.rs? + let mut cmd = Command::new(bin_path); + let mut child = cmd.spawn().map_err(DownloadError::Launch)?; + + // Wait to see if the installer fails + match timeout(INSTALLER_STARTUP_TIMEOUT, child.wait()).await { + // Timeout: Quit and let the installer take over + Err(_timeout) => Ok(()), + // No timeout: Incredibly quick but successful (or wrong exit code, probably) + Ok(Ok(status)) if status.success() => Ok(()), + // Installer failed + Ok(Ok(status)) => Err(DownloadError::InstallFailed(anyhow::anyhow!("Install failed with status: {status}"))), + // Installer failed + Ok(Err(err)) => Err(DownloadError::InstallFailed(anyhow::anyhow!("Install failed : {err}"))), + } + } } impl<AppProgress> HttpAppDownloader<AppProgress> { |
