Implement new version check and app downloader

This also fixes 'latest_beta' being required in the API

Co-authored-by: Sebastian Holmin <sebastian.holmin@mullvad.net>
Co-authored-by: Markus Pettersson <markus.pettersson@mullvad.net>
This commit is contained in:
David Lönnhager 2025-03-25 14:21:19 +01:00 committed by Sebastian Holmin
parent 26b6b2567d
commit 17d57b983a
No known key found for this signature in database
GPG Key ID: 9C88494B3F2F9089
20 changed files with 1405 additions and 442 deletions

3
Cargo.lock generated
View File

@ -2937,10 +2937,12 @@ dependencies = [
"mullvad-paths",
"mullvad-relay-selector",
"mullvad-types",
"mullvad-update",
"mullvad-version",
"nix 0.23.2",
"notify 8.0.0",
"objc2",
"rand 0.8.5",
"regex",
"serde",
"serde_json",
@ -3216,6 +3218,7 @@ dependencies = [
"intersection-derive",
"ipnetwork",
"log",
"mullvad-version",
"regex",
"serde",
"talpid-types",

View File

@ -719,7 +719,7 @@ function convertFromAppUpgradeError(error: grpcTypes.AppUpgradeError.Error): Dae
switch (error) {
case grpcTypes.AppUpgradeError.Error.DOWNLOAD_FAILED:
return 'DOWNLOAD_FAILED';
case grpcTypes.AppUpgradeError.Error.VERFICATION_FAILED:
case grpcTypes.AppUpgradeError.Error.VERIFICATION_FAILED:
return 'VERIFICATION_FAILED';
default:
return 'GENERAL_ERROR';

View File

@ -2,9 +2,10 @@ use std::future::Future;
use std::sync::Arc;
use http::StatusCode;
use mullvad_types::version::AppVersion;
use mullvad_update::version::{VersionInfo, VersionParameters};
type AppVersion = String;
use super::rest;
use super::APP_URL_PREFIX;
@ -18,7 +19,7 @@ pub struct AppVersionResponse {
pub supported: bool,
pub latest: AppVersion,
pub latest_stable: Option<AppVersion>,
pub latest_beta: AppVersion,
pub latest_beta: Option<AppVersion>,
}
impl AppVersionProxy {

View File

@ -21,28 +21,16 @@ pub async fn print() -> Result<()> {
.get_version_info()
.await
.context("Failed to get version info")?;
println!("{:22}: {}", "Is supported", version_info.supported);
println!(
"{:22}: {}",
"Is supported", version_info.current_version_supported
);
if let Some(suggested_upgrade) = version_info.suggested_upgrade {
println!("{:22}: {}", "Suggested upgrade", suggested_upgrade);
println!("{:22}: {}", "Suggested upgrade", suggested_upgrade.version);
} else {
println!("{:22}: none", "Suggested upgrade");
}
if !version_info.latest_stable.is_empty() {
println!(
"{:22}: {}",
"Latest stable version", version_info.latest_stable
);
}
let settings = rpc
.get_settings()
.await
.context("Failed to obtain settings")?;
if settings.show_beta_releases {
println!("{:22}: {}", "Latest beta version", version_info.latest_beta);
};
Ok(())
}

View File

@ -39,6 +39,7 @@ mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" }
mullvad-fs = { path = "../mullvad-fs" }
mullvad-paths = { path = "../mullvad-paths" }
mullvad-version = { path = "../mullvad-version" }
mullvad-update = { path = "../mullvad-update", features = ["client"] }
mullvad-leak-checker = { path = "../mullvad-leak-checker", default-features = false }
talpid-core = { path = "../talpid-core" }
talpid-future = { path = "../talpid-future" }
@ -46,6 +47,7 @@ talpid-platform-metadata = { path = "../talpid-platform-metadata" }
talpid-time = { path = "../talpid-time" }
talpid-types = { path = "../talpid-types" }
talpid-routing = { path = "../talpid-routing" }
rand = "0.8.5"
clap = { workspace = true }
log-panics = "2.0.0"

View File

@ -33,6 +33,12 @@ fn main() {
// Enable DAITA by default on desktop and android
println!("cargo::rustc-check-cfg=cfg(daita)");
println!(r#"cargo::rustc-cfg=daita"#);
// Enable in-app upgrades on macOS and Windows
println!("cargo::rustc-check-cfg=cfg(update)");
if cfg!(any(target_os = "macos", target_os = "windows")) {
println!(r#"cargo::rustc-cfg=update"#);
}
}
fn commit_date() -> String {

View File

@ -28,7 +28,6 @@ pub mod shutdown;
mod target_state;
mod tunnel;
pub mod version;
mod version_check;
use crate::target_state::PersistentTargetState;
use api::DaemonAccessMethodResolver;
@ -64,7 +63,7 @@ use mullvad_types::{
relay_list::RelayList,
settings::{DnsOptions, Settings},
states::{Secured, TargetState, TargetStateStrict, TunnelState},
version::{AppVersion, AppVersionInfo},
version::AppVersionInfo,
wireguard::{PublicKey, QuantumResistantState, RotationInterval},
};
use relay_list::{RelayListUpdater, RelayListUpdaterHandle, RELAYS_FILENAME};
@ -126,7 +125,7 @@ pub enum Error {
ApiCheckError(#[source] mullvad_api::availability::Error),
#[error("Version check failed")]
VersionCheckError(#[source] version_check::Error),
VersionCheckError(#[source] version::Error),
#[error("Unable to load account history")]
LoadAccountHistory(#[source] account_history::Error),
@ -337,7 +336,7 @@ pub enum DaemonCommand {
/// Return whether the daemon is performing post-upgrade tasks
IsPerformingPostUpgrade(oneshot::Sender<bool>),
/// Get current version of the app
GetCurrentVersion(oneshot::Sender<AppVersion>),
GetCurrentVersion(oneshot::Sender<String>),
/// Remove settings and clear the cache
#[cfg(not(target_os = "android"))]
FactoryReset(ResponseTx<(), Error>),
@ -402,6 +401,13 @@ pub enum DaemonCommand {
relay: String,
tx: oneshot::Sender<()>,
},
// App upgrade
/// Prompt the daemon to start an app version upgrade.
///
/// If an upgrade had previously been started but not completed the daemon should continue the upgrade process at the appropriate step. The client need not be notified about this detail.
AppUpgrade(ResponseTx<(), Error>),
/// Prompt the daemon to abort the current upgrade.
AppUpgradeAbort(ResponseTx<(), Error>),
}
/// All events that can happen in the daemon. Sent from various threads and exposed interfaces.
@ -621,7 +627,7 @@ pub struct Daemon {
access_mode_handler: mullvad_api::access_mode::AccessModeSelectorHandle,
api_runtime: mullvad_api::Runtime,
api_handle: mullvad_api::rest::MullvadRestHandle,
version_updater_handle: version_check::VersionUpdaterHandle,
version_handle: version::router::VersionRouterHandle,
relay_selector: RelaySelector,
relay_list_updater: RelayListUpdaterHandle,
parameters_generator: tunnel::ParametersGenerator,
@ -890,14 +896,13 @@ impl Daemon {
on_relay_list_update,
);
let version_updater_handle = version_check::VersionUpdater::spawn(
let version_handle = version::router::VersionRouter::spawn(
api_handle.clone(),
api_availability.clone(),
api_handle.availability.clone(),
config.cache_dir.clone(),
internal_event_tx.to_specialized_sender(),
settings.show_beta_releases,
)
.await;
);
// Attempt to download a fresh relay list
relay_list_updater.update().await;
@ -944,7 +949,7 @@ impl Daemon {
access_mode_handler,
api_runtime,
api_handle,
version_updater_handle,
version_handle,
relay_selector,
relay_list_updater,
parameters_generator,
@ -1473,6 +1478,8 @@ impl Daemon {
GetFeatureIndicators(tx) => self.on_get_feature_indicators(tx),
DisableRelay { relay, tx } => self.on_toggle_relay(relay, false, tx),
EnableRelay { relay, tx } => self.on_toggle_relay(relay, true, tx),
AppUpgrade(tx) => self.on_app_upgrade(tx),
AppUpgradeAbort(tx) => self.on_app_upgrade_abort(tx),
}
}
@ -1939,12 +1946,12 @@ impl Daemon {
}
fn on_get_version_info(&mut self, tx: oneshot::Sender<Result<AppVersionInfo, Error>>) {
let mut handle = self.version_updater_handle.clone();
let handle = self.version_handle.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
handle
.get_version_info()
.get_latest_version()
.await
.inspect_err(|error| {
log::error!(
@ -1958,7 +1965,7 @@ impl Daemon {
});
}
fn on_get_current_version(&mut self, tx: oneshot::Sender<AppVersion>) {
fn on_get_current_version(&mut self, tx: oneshot::Sender<String>) {
Self::oneshot_send(
tx,
mullvad_version::VERSION.to_owned(),
@ -2307,8 +2314,12 @@ impl Daemon {
Ok(settings_changed) => {
Self::oneshot_send(tx, Ok(()), "set_show_beta_releases response");
if settings_changed {
let mut handle = self.version_updater_handle.clone();
handle.set_show_beta_releases(enabled).await;
let version_handle = self.version_handle.clone();
tokio::spawn(async move {
if let Err(error) = version_handle.set_show_beta_releases(enabled).await {
log::error!("Failed to reset beta releases state: {error}");
}
});
}
}
Err(e) => {
@ -3013,9 +3024,16 @@ impl Daemon {
let dns = dns::addresses_from_options(&self.settings.tunnel_options.dns_options);
self.send_tunnel_command(TunnelCommand::Dns(dns, tx));
self.version_updater_handle
.set_show_beta_releases(self.settings.show_beta_releases)
.await;
let version_handle = self.version_handle.clone();
let show_beta_releases = self.settings.show_beta_releases;
tokio::spawn(async move {
if let Err(error) = version_handle
.set_show_beta_releases(show_beta_releases)
.await
{
log::error!("Failed to reset beta releases state: {error}");
}
});
let access_mode_handler = self.access_mode_handler.clone();
tokio::spawn(async move {
if let Err(error) = access_mode_handler.rotate().await {
@ -3205,6 +3223,18 @@ impl Daemon {
Self::oneshot_send(tx, (), "on_toggle_relay response");
}
fn on_app_upgrade(&self, tx: ResponseTx<(), Error>) {
// TODO: Call the Downloader
let result = Ok(());
Self::oneshot_send(tx, result, "on_app_upgrade response");
}
fn on_app_upgrade_abort(&self, tx: ResponseTx<(), Error>) {
// TODO: Abort the Downloader
let result = Ok(());
Self::oneshot_send(tx, result, "on_app_upgrade_abort response");
}
/// Set the target state of the client. If it changed trigger the operations needed to
/// progress towards that state.
/// Returns a bool representing whether a state change was initiated.

View File

@ -1,4 +1,4 @@
use crate::{account_history, device, version_check, DaemonCommand, DaemonCommandSender};
use crate::{account_history, device, DaemonCommand, DaemonCommandSender};
use futures::{
channel::{mpsc, oneshot},
StreamExt,
@ -41,12 +41,18 @@ pub enum Error {
struct ManagementServiceImpl {
daemon_tx: DaemonCommandSender,
subscriptions: Arc<Mutex<Vec<EventsListenerSender>>>,
app_upgrade_event_subscribers: Arc<Mutex<Vec<AppUpgradeEventListenerSender>>>,
}
pub type ServiceResult<T> = std::result::Result<Response<T>, Status>;
type EventsListenerReceiver = UnboundedReceiverStream<Result<types::DaemonEvent, Status>>;
type EventsListenerSender = tokio::sync::mpsc::UnboundedSender<Result<types::DaemonEvent, Status>>;
type AppUpgradeEventListenerReceiver =
UnboundedReceiverStream<Result<types::AppUpgradeEvent, Status>>;
type AppUpgradeEventListenerSender =
tokio::sync::mpsc::UnboundedSender<Result<types::AppUpgradeEvent, Status>>;
const INVALID_VOUCHER_MESSAGE: &str = "This voucher code is invalid";
const USED_VOUCHER_MESSAGE: &str = "This voucher code has already been used";
@ -54,8 +60,7 @@ const USED_VOUCHER_MESSAGE: &str = "This voucher code has already been used";
impl ManagementService for ManagementServiceImpl {
type GetSplitTunnelProcessesStream = UnboundedReceiverStream<Result<i32, Status>>;
type EventsListenStream = EventsListenerReceiver;
type AppUpgradeEventsListenStream =
UnboundedReceiverStream<Result<types::AppUpgradeEvent, Status>>;
type AppUpgradeEventsListenStream = AppUpgradeEventListenerReceiver;
// Control and get the tunnel state
//
@ -1121,18 +1126,38 @@ impl ManagementService for ManagementServiceImpl {
// App upgrade
async fn app_upgrade(&self, _: Request<()>) -> ServiceResult<()> {
todo!()
log::debug!("app_upgrade");
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::AppUpgrade(tx))?;
self.wait_for_result(rx).await?.map_err(map_daemon_error)?;
Ok(Response::new(()))
}
async fn app_upgrade_abort(&self, _: Request<()>) -> ServiceResult<()> {
todo!()
log::debug!("app_upgrade_abort");
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::AppUpgradeAbort(tx))?;
self.wait_for_result(rx).await?.map_err(map_daemon_error)?;
Ok(Response::new(()))
}
async fn app_upgrade_events_listen(
&self,
_: Request<()>,
) -> ServiceResult<Self::AppUpgradeEventsListenStream> {
todo!()
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut subscriptions = self.app_upgrade_event_subscribers.lock().unwrap();
subscriptions.push(tx);
let upgrade_event_stream = UnboundedReceiverStream::new(rx);
Ok(Response::new(upgrade_event_stream))
}
}
@ -1168,6 +1193,8 @@ impl ManagementInterfaceServer {
rpc_socket_path: impl AsRef<Path>,
) -> Result<ManagementInterfaceServer, Error> {
let subscriptions = Arc::<Mutex<Vec<EventsListenerSender>>>::default();
let app_upgrade_event_subscriptions =
Arc::<Mutex<Vec<AppUpgradeEventListenerSender>>>::default();
// NOTE: It is important that the channel buffer size is kept at 0. When sending a signal
// to abort the gRPC server, the sender can be awaited to know when the gRPC server has
// received and started processing the shutdown signal.
@ -1175,6 +1202,7 @@ impl ManagementInterfaceServer {
let server = ManagementServiceImpl {
daemon_tx,
subscriptions: subscriptions.clone(),
app_upgrade_event_subscribers: app_upgrade_event_subscriptions.clone(),
};
let rpc_server_join_handle = mullvad_management_interface::spawn_rpc_server(
server,
@ -1190,7 +1218,10 @@ impl ManagementInterfaceServer {
rpc_socket_path.as_ref().display()
);
let broadcast = ManagementInterfaceEventBroadcaster { subscriptions };
let broadcast = ManagementInterfaceEventBroadcaster {
subscriptions,
app_upgrade_event_subscriptions,
};
Ok(ManagementInterfaceServer {
rpc_server_join_handle,
@ -1230,6 +1261,7 @@ impl ManagementInterfaceServer {
#[derive(Clone)]
pub struct ManagementInterfaceEventBroadcaster {
subscriptions: Arc<Mutex<Vec<EventsListenerSender>>>,
app_upgrade_event_subscriptions: Arc<Mutex<Vec<AppUpgradeEventListenerSender>>>,
}
impl ManagementInterfaceEventBroadcaster {
@ -1238,6 +1270,11 @@ impl ManagementInterfaceEventBroadcaster {
subscriptions.retain(|tx| tx.send(Ok(value.clone())).is_ok());
}
pub(crate) fn notify_upgrade_event(&self, value: version::AppUpgradeEvent) {
let mut subscriptions = self.app_upgrade_event_subscriptions.lock().unwrap();
subscriptions.retain(|tx| tx.send(Ok(value.clone().into())).is_ok());
}
/// Notify that the tunnel state changed.
///
/// Sends a new state update to all `new_state` subscribers of the management interface.
@ -1412,11 +1449,11 @@ fn map_account_history_error(error: account_history::Error) -> Status {
}
}
fn map_version_check_error(error: version_check::Error) -> Status {
fn map_version_check_error(error: crate::version::Error) -> Status {
match error {
version_check::Error::Download(..)
| version_check::Error::ReadVersionCache(..)
| version_check::Error::ApiCheck(..) => Status::unavailable(error.to_string()),
crate::version::Error::Download(..)
| crate::version::Error::ReadVersionCache(..)
| crate::version::Error::ApiCheck(..) => Status::unavailable(error.to_string()),
_ => Status::unknown(error.to_string()),
}
}

View File

@ -1,19 +0,0 @@
/// Contains the date of the git commit this was built from
pub const COMMIT_DATE: &str = include_str!(concat!(env!("OUT_DIR"), "/git-commit-date.txt"));
pub fn is_beta_version() -> bool {
mullvad_version::VERSION.contains("beta")
}
pub fn is_dev_version() -> bool {
mullvad_version::VERSION.contains("dev")
}
pub fn log_version() {
log::info!(
"Starting {} - {} {}",
env!("CARGO_PKG_NAME"),
mullvad_version::VERSION,
COMMIT_DATE,
)
}

View File

@ -1,20 +1,17 @@
use crate::{version::is_beta_version, DaemonEventSender};
use futures::{
channel::{mpsc, oneshot},
future::{BoxFuture, FusedFuture},
FutureExt, SinkExt, StreamExt, TryFutureExt,
FutureExt, StreamExt, TryFutureExt,
};
use mullvad_api::{
availability::ApiAvailability,
rest::MullvadRestHandle,
version::{AppVersionProxy, AppVersionResponse},
availability::ApiAvailability, rest::MullvadRestHandle, version::AppVersionProxy,
};
use mullvad_types::version::AppVersionInfo;
use mullvad_update::version::VersionInfo;
use mullvad_version::Version;
use serde::{Deserialize, Serialize};
use std::{
future::Future,
io,
path::{Path, PathBuf},
pin::Pin,
str::FromStr,
@ -26,6 +23,8 @@ use talpid_future::retry::{retry_future, ConstantInterval};
use talpid_types::ErrorExt;
use tokio::{fs::File, io::AsyncReadExt};
use super::Error;
const VERSION_INFO_FILENAME: &str = "version-info.json";
static APP_VERSION: LazyLock<Version> =
@ -51,98 +50,38 @@ const PLATFORM: &str = "windows";
const PLATFORM: &str = "android";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct CachedAppVersionInfo {
#[serde(flatten)]
pub version_info: AppVersionInfo,
pub cached_from_version: String,
pub(super) struct VersionCache {
/// Whether the current (installed) version is supported or an upgrade is required
pub current_version_supported: bool,
/// The latest available versions
pub latest_version: mullvad_update::version::VersionInfo,
}
impl From<AppVersionInfo> for CachedAppVersionInfo {
fn from(version_info: AppVersionInfo) -> CachedAppVersionInfo {
CachedAppVersionInfo {
version_info,
cached_from_version: mullvad_version::VERSION.to_owned(),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to open app version cache file for reading")]
ReadVersionCache(#[source] io::Error),
#[error("Failed to open app version cache file for writing")]
WriteVersionCache(#[source] io::Error),
#[error("Failure in serialization of the version info")]
Serialize(#[source] serde_json::Error),
#[error("Failure in deserialization of the version info")]
Deserialize(#[source] serde_json::Error),
#[error("Failed to check the latest app version")]
Download(#[source] mullvad_api::rest::Error),
#[error("API availability check failed")]
ApiCheck(#[source] mullvad_api::availability::Error),
#[error("Clearing version check cache due to a version mismatch")]
CacheVersionMismatch,
#[error("Version updater is down")]
VersionUpdaterDown,
#[error("Version cache update was aborted")]
UpdateAborted,
}
pub(crate) struct VersionUpdater;
pub(crate) struct VersionUpdater(());
#[derive(Default)]
struct VersionUpdaterInner {
/// The last known [AppVersionInfo], along with the time it was determined.
last_app_version_info: Option<(AppVersionInfo, SystemTime)>,
show_beta_releases: bool,
last_app_version_info: Option<(VersionCache, SystemTime)>,
/// Oneshot channels for responding to [VersionUpdaterCommand::GetVersionInfo].
get_version_info_responders: Vec<oneshot::Sender<AppVersionInfo>>,
get_version_info_responders: Vec<oneshot::Sender<VersionCache>>,
}
type VersionUpdateCommand = oneshot::Sender<VersionCache>;
#[derive(Clone)]
pub(crate) struct VersionUpdaterHandle {
tx: mpsc::Sender<VersionUpdaterCommand>,
}
enum VersionUpdaterCommand {
SetShowBetaReleases(bool),
GetVersionInfo(oneshot::Sender<AppVersionInfo>),
tx: mpsc::UnboundedSender<VersionUpdateCommand>,
}
impl VersionUpdaterHandle {
pub async fn set_show_beta_releases(&mut self, show_beta_releases: bool) {
if self
.tx
.send(VersionUpdaterCommand::SetShowBetaReleases(
show_beta_releases,
))
.await
.is_err()
{
log::error!("Version updater already down, can't send new `show_beta_releases` state");
}
}
/// Get the latest cached [AppVersionInfo].
///
/// If the cache is stale or missing, this will immediately query the API for the latest
/// version. This may take a few seconds.
pub async fn get_version_info(&mut self) -> Result<AppVersionInfo, Error> {
pub(super) async fn get_version_info(&self) -> Result<VersionCache, Error> {
let (done_tx, done_rx) = oneshot::channel();
if self
.tx
.send(VersionUpdaterCommand::GetVersionInfo(done_tx))
.await
.is_err()
{
if self.tx.unbounded_send(done_tx).is_err() {
Err(Error::VersionUpdaterDown)
} else {
done_rx.await.map_err(|_| Error::UpdateAborted)
@ -151,17 +90,16 @@ impl VersionUpdaterHandle {
}
impl VersionUpdater {
pub async fn spawn(
pub(super) async fn spawn(
mut api_handle: MullvadRestHandle,
availability_handle: ApiAvailability,
cache_dir: PathBuf,
update_sender: DaemonEventSender<AppVersionInfo>,
show_beta_releases: bool,
update_sender: mpsc::UnboundedSender<VersionCache>,
) -> VersionUpdaterHandle {
// load the last known AppVersionInfo from cache
let last_app_version_info = load_cache(&cache_dir).await;
let (tx, rx) = mpsc::channel(1);
let (tx, rx) = mpsc::unbounded();
api_handle.factory = api_handle.factory.default_timeout(DOWNLOAD_TIMEOUT);
let version_proxy = AppVersionProxy::new(api_handle);
@ -171,7 +109,6 @@ impl VersionUpdater {
tokio::spawn(
VersionUpdaterInner {
last_app_version_info,
show_beta_releases,
get_version_info_responders: vec![],
}
.run(
@ -194,33 +131,16 @@ impl VersionUpdater {
impl VersionUpdaterInner {
/// Get the last known [AppVersionInfo]. May be stale.
pub fn last_app_version_info(&self) -> Option<&AppVersionInfo> {
pub fn last_app_version_info(&self) -> Option<&VersionCache> {
self.last_app_version_info.as_ref().map(|(info, _)| info)
}
/// Convert a [AppVersionResponse] to an [AppVersionInfo].
fn response_to_version_info(&self, response: AppVersionResponse) -> AppVersionInfo {
let suggested_upgrade = suggested_upgrade(
&APP_VERSION,
&response.latest_stable,
&response.latest_beta,
self.show_beta_releases || is_beta_version(),
);
AppVersionInfo {
supported: response.supported,
latest_stable: response.latest_stable.unwrap_or_else(|| "".to_owned()),
latest_beta: response.latest_beta,
suggested_upgrade,
}
}
/// Update [Self::last_app_version_info] and write it to disk cache, and notify the `update`
/// callback.
async fn update_version_info(
&mut self,
update: &impl Fn(AppVersionInfo) -> BoxFuture<'static, Result<(), Error>>,
new_version_info: AppVersionInfo,
update: &impl Fn(VersionCache) -> BoxFuture<'static, Result<(), Error>>,
new_version_info: VersionCache,
) {
if let Err(err) = update(new_version_info.clone()).await {
log::error!("Failed to save version cache to disk: {}", err);
@ -234,10 +154,7 @@ impl VersionUpdaterInner {
/// This happens [UPDATE_INTERVAL] after the last version check.
fn time_until_version_is_stale(&self) -> Duration {
let now = SystemTime::now();
self
.last_app_version_info
.as_ref()
.map(|(_, last_update_time)| last_update_time)
self.last_update_time()
.and_then(|&last_update_time| now.duration_since(last_update_time).ok())
.map(|time_since_last_update| UPDATE_INTERVAL.saturating_sub(time_since_last_update))
// if there is no last_app_version_info, or if clocks are being weird,
@ -245,6 +162,12 @@ impl VersionUpdaterInner {
.unwrap_or(Duration::ZERO)
}
fn last_update_time(&self) -> Option<&SystemTime> {
self.last_app_version_info
.as_ref()
.map(|(_, last_update_time)| last_update_time)
}
/// Is [Self::last_app_version_info] stale?
fn version_is_stale(&self) -> bool {
self.time_until_version_is_stale().is_zero()
@ -268,18 +191,16 @@ impl VersionUpdaterInner {
async fn run(
self,
mut rx: mpsc::Receiver<VersionUpdaterCommand>,
mut rx: mpsc::UnboundedReceiver<VersionUpdateCommand>,
update: UpdateContext,
api: ApiContext,
) {
// If this is a dev build, there's no need to pester the API for version checks.
if *IS_DEV_BUILD {
log::warn!("Not checking for updates because this is a development build");
while let Some(cmd) = rx.next().await {
if let VersionUpdaterCommand::GetVersionInfo(done_tx) = cmd {
log::info!("Version check is disabled in dev builds");
let _ = done_tx.send(dev_version_cache());
}
while let Some(done_tx) = rx.next().await {
log::info!("Version check is disabled in dev builds");
let _ = done_tx.send(dev_version_cache());
}
return;
}
@ -294,11 +215,10 @@ impl VersionUpdaterInner {
async fn run_inner(
mut self,
mut rx: mpsc::Receiver<VersionUpdaterCommand>,
update: impl Fn(AppVersionInfo) -> BoxFuture<'static, Result<(), Error>>,
do_version_check: impl Fn() -> BoxFuture<'static, Result<AppVersionResponse, Error>>,
do_version_check_in_background: impl Fn()
-> BoxFuture<'static, Result<AppVersionResponse, Error>>,
mut rx: mpsc::UnboundedReceiver<VersionUpdateCommand>,
update: impl Fn(VersionCache) -> BoxFuture<'static, Result<(), Error>>,
do_version_check: impl Fn() -> BoxFuture<'static, Result<VersionCache, Error>>,
do_version_check_in_background: impl Fn() -> BoxFuture<'static, Result<VersionCache, Error>>,
) {
let mut version_is_stale = self.wait_until_version_is_stale();
let mut version_check = futures::future::Fuse::terminated();
@ -306,30 +226,8 @@ impl VersionUpdaterInner {
loop {
futures::select! {
command = rx.next() => match command {
Some(VersionUpdaterCommand::SetShowBetaReleases(show_beta_releases)) => {
self.show_beta_releases = show_beta_releases;
if let Some(last_app_version_info) = self
.last_app_version_info()
.cloned()
{
let suggested_upgrade = suggested_upgrade(
&APP_VERSION,
&Some(last_app_version_info.latest_stable.clone()),
&last_app_version_info.latest_beta,
self.show_beta_releases || is_beta_version(),
);
self.update_version_info(&update, AppVersionInfo {
supported: last_app_version_info.supported,
latest_stable: last_app_version_info.latest_stable,
latest_beta: last_app_version_info.latest_beta,
suggested_upgrade,
}).await;
}
}
Some(VersionUpdaterCommand::GetVersionInfo(done_tx)) => {
Some(done_tx) => {
match (self.version_is_stale(), self.last_app_version_info()) {
(false, Some(version_info)) => {
// if the version_info isn't stale, return it immediately.
@ -361,16 +259,13 @@ impl VersionUpdaterInner {
response = version_check => {
match response {
Ok(version_info_response) => {
let new_version_info =
self.response_to_version_info(version_info_response);
Ok(version_info) => {
// Respond to all pending GetVersionInfo commands
for done_tx in self.get_version_info_responders.drain(..) {
let _ = done_tx.send(new_version_info.clone());
let _ = done_tx.send(version_info.clone());
}
self.update_version_info(&update, new_version_info).await;
self.update_version_info(&update, version_info).await;
}
Err(err) => {
@ -388,7 +283,7 @@ impl VersionUpdaterInner {
struct UpdateContext {
cache_path: PathBuf,
update_sender: DaemonEventSender<AppVersionInfo>,
update_sender: mpsc::UnboundedSender<VersionCache>,
}
impl UpdateContext {
@ -396,15 +291,14 @@ impl UpdateContext {
/// ([VERSION_INFO_FILENAME]). Also, notify `self.update_sender`
fn update(
&self,
last_app_version: AppVersionInfo,
last_app_version: VersionCache,
) -> impl Future<Output = Result<(), Error>> + use<> {
let _ = self.update_sender.send(last_app_version.clone());
let cache_path = self.cache_path.clone();
async move {
log::debug!("Writing version check cache to {}", cache_path.display());
let cached_app_version = CachedAppVersionInfo::from(last_app_version);
let buf = serde_json::to_vec_pretty(&cached_app_version).map_err(Error::Serialize)?;
let buf = serde_json::to_vec_pretty(&last_app_version).map_err(Error::Serialize)?;
tokio::fs::write(cache_path, buf)
.await
.map_err(Error::WriteVersionCache)
@ -420,24 +314,15 @@ struct ApiContext {
}
/// Immediately query the API for the latest [AppVersionInfo].
fn do_version_check(api: ApiContext) -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
let download_future_factory = move || {
api.version_proxy
.version_check(
mullvad_version::VERSION.to_owned(),
PLATFORM,
api.platform_version.clone(),
)
.map_err(Error::Download)
};
fn do_version_check(api: ApiContext) -> BoxFuture<'static, Result<VersionCache, Error>> {
let api_handle = api.api_handle.clone();
let download_future_factory = move || version_check_inner(&api);
// retry immediately on network errors (unless we're offline)
let should_retry_immediate = move |result: &Result<_, Error>| {
if let Err(Error::Download(error)) = result {
error.is_network_error() && !api.api_handle.is_offline()
} else {
false
}
!api_handle.is_offline()
&& matches!(result, Err(Error::Download(error)) if error.is_network_error())
};
Box::pin(retry_future(
@ -455,44 +340,114 @@ fn do_version_check(api: ApiContext) -> BoxFuture<'static, Result<AppVersionResp
/// On any error, this function retries repeatedly every [UPDATE_INTERVAL_ERROR] until success.
fn do_version_check_in_background(
api: ApiContext,
) -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
) -> BoxFuture<'static, Result<VersionCache, Error>> {
let download_future_factory = move || {
let when_available = api.api_handle.wait_background();
let request = api.version_proxy.version_check(
mullvad_version::VERSION.to_owned(),
PLATFORM,
api.platform_version.clone(),
);
let version_cache = version_check_inner(&api);
async move {
when_available.await.map_err(Error::ApiCheck)?;
request.await.map_err(Error::Download)
version_cache.await
}
};
Box::pin(retry_future(
download_future_factory,
|result| result.is_err(),
std::iter::repeat(UPDATE_INTERVAL_ERROR),
ConstantInterval::new(UPDATE_INTERVAL_ERROR, None),
))
}
/// Combine the old version and new version endpoint
#[cfg(any(target_os = "windows", target_os = "macos"))]
fn version_check_inner(api: &ApiContext) -> impl Future<Output = Result<VersionCache, Error>> {
let v1_endpoint = api.version_proxy.version_check(
mullvad_version::VERSION.to_owned(),
PLATFORM,
api.platform_version.clone(),
);
let v2_endpoint = api.version_proxy.version_check_2(
PLATFORM,
// TODO: get current architecture (from talpid_platform_metadata)
mullvad_update::format::Architecture::X86,
// TODO: set reasonable rollout,
0.,
// TODO: set last known metadata version + 1
0,
);
async move {
let (v1_response, v2_response) =
tokio::try_join!(v1_endpoint, v2_endpoint).map_err(Error::Download)?;
Ok(VersionCache {
current_version_supported: v1_response.supported,
latest_version: v2_response,
})
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn version_check_inner(api: &ApiContext) -> impl Future<Output = Result<VersionCache, Error>> {
let v1_endpoint = api.version_proxy.version_check(
mullvad_version::VERSION.to_owned(),
PLATFORM,
api.platform_version.clone(),
);
async move {
let response = v1_endpoint.await.map_err(Error::Download)?;
let latest_stable = response.latest_stable
.and_then(|version| version.parse().ok())
// Suggested stable must actually be stable
.filter(|version: &mullvad_version::Version| version.pre_stable.is_none())
.ok_or_else(|| Error::MissingStable)?;
let latest_beta = response.latest_beta
.and_then(|version| version.parse().ok())
// Suggested beta must actually be non-stable
.filter(|version: &mullvad_version::Version| version.pre_stable.is_some());
Ok(VersionCache {
current_version_supported: response.supported,
// Note: We're pretending that this is complete information,
// but on Android and Linux, most of the information is missing
latest_version: VersionInfo {
stable: mullvad_update::version::Version {
version: latest_stable,
changelog: "".to_owned(),
urls: vec![],
sha256: [0u8; 32],
size: 0,
},
beta: latest_beta.map(|version| mullvad_update::version::Version {
version,
changelog: "".to_owned(),
urls: vec![],
sha256: [0u8; 32],
size: 0,
}),
},
})
}
}
/// Read the app version cache from the provided directory.
///
/// Returns the [AppVersionInfo] along with the modification time of the cache file,
/// or `None` on any error.
async fn load_cache(cache_dir: &Path) -> Option<(AppVersionInfo, SystemTime)> {
async fn load_cache(cache_dir: &Path) -> Option<(VersionCache, SystemTime)> {
try_load_cache(cache_dir)
.await
.inspect_err(|error| {
log::warn!(
"{}",
error.display_chain_with_msg("Unable to load cached version info")
)
if matches!(error, Error::OutdatedVersion) {
log::trace!("Ignoring outdated version cache");
} else {
log::warn!(
"{}",
error.display_chain_with_msg("Unable to load cached version info")
);
}
})
.ok()
}
async fn try_load_cache(cache_dir: &Path) -> Result<(AppVersionInfo, SystemTime), Error> {
async fn try_load_cache(cache_dir: &Path) -> Result<(VersionCache, SystemTime), Error> {
if *IS_DEV_BUILD {
return Ok((dev_version_cache(), SystemTime::now()));
}
@ -511,61 +466,47 @@ async fn try_load_cache(cache_dir: &Path) -> Result<(AppVersionInfo, SystemTime)
.map_err(Error::ReadVersionCache)
.await?;
let version_info: CachedAppVersionInfo =
serde_json::from_str(&content).map_err(Error::Deserialize)?;
let cache: VersionCache = serde_json::from_str(&content).map_err(Error::Deserialize)?;
if version_info.cached_from_version == mullvad_version::VERSION {
Ok((version_info.version_info, mtime))
} else {
Err(Error::CacheVersionMismatch)
if cache_is_old(&cache.latest_version, &*APP_VERSION) {
return Err(Error::OutdatedVersion);
}
Ok((cache, mtime))
}
fn dev_version_cache() -> AppVersionInfo {
/// Check if the cached version is older than the current version. If so, assume the cache is stale.
/// It could in principle mean that a version has been yanked, but we do not really support this,
/// and it should not cause any real issue to delete the cache anyway.
fn cache_is_old(cached_version: &VersionInfo, current_version: &mullvad_version::Version) -> bool {
let last_version = if current_version.pre_stable.is_some() {
// Discard suggested version if current beta is newer
cached_version
.beta
.as_ref()
.unwrap_or(&cached_version.stable)
} else {
// Discard suggested version if current stable is newer
&cached_version.stable
};
current_version > &last_version.version
}
fn dev_version_cache() -> VersionCache {
assert!(*IS_DEV_BUILD);
AppVersionInfo {
supported: false,
latest_stable: mullvad_version::VERSION.to_owned(),
latest_beta: mullvad_version::VERSION.to_owned(),
suggested_upgrade: None,
}
}
/// If current_version is not the latest, return a string containing the latest version.
fn suggested_upgrade(
current_version: &Version,
latest_stable: &Option<String>,
latest_beta: &str,
show_beta: bool,
) -> Option<String> {
let stable_version = latest_stable
.as_ref()
.and_then(|stable| Version::from_str(stable).ok());
let beta_version = if show_beta {
Version::from_str(latest_beta).ok()
} else {
None
};
let latest_version = match (&stable_version, &beta_version) {
(Some(_), None) => stable_version,
(None, Some(_)) => beta_version,
(Some(stable), Some(beta)) => {
if beta > stable {
beta_version
} else {
stable_version
}
}
(None, None) => None,
}?;
if &latest_version > current_version {
Some(latest_version.to_string())
} else {
None
VersionCache {
current_version_supported: false,
latest_version: VersionInfo {
stable: mullvad_update::version::Version {
version: mullvad_version::VERSION.parse().unwrap(),
changelog: "".to_owned(),
urls: vec![],
sha256: [0u8; 32],
size: 0,
},
beta: None,
},
}
}
@ -576,8 +517,60 @@ mod test {
Arc,
};
use futures::SinkExt;
use mullvad_update::version::Version;
use super::*;
/// Test whether outdated version caches are ignored correctly.
/// This prevents old versions from being suggested as updates.
#[test]
fn test_old_cache() {
assert!(cache_is_old(
&version_info("2025.5", None),
&"2025.6".parse().unwrap()
));
assert!(!cache_is_old(
&version_info("2025.5", None),
&"2025.5".parse().unwrap()
));
assert!(!cache_is_old(
&version_info("2025.5", Some("2025.5-beta1")),
&"2025.5-beta1".parse().unwrap()
));
assert!(cache_is_old(
&version_info("2025.5", Some("2025.5-beta1")),
&"2025.5-beta2".parse().unwrap()
));
assert!(!cache_is_old(
&version_info("2025.5", None),
&"2025.5-beta2".parse().unwrap()
));
assert!(cache_is_old(
&version_info("2025.5", None),
&"2025.6-beta2".parse().unwrap()
));
}
fn version_info(stable: &str, beta: Option<&str>) -> VersionInfo {
VersionInfo {
stable: Version {
version: stable.parse().unwrap(),
urls: vec![],
size: 0,
changelog: "".to_owned(),
sha256: [0u8; 32],
},
beta: beta.map(|beta| Version {
version: beta.parse().unwrap(),
urls: vec![],
size: 0,
changelog: "".to_owned(),
sha256: [0u8; 32],
}),
}
}
/// If there's no cached version, it should count as stale
#[test]
fn test_version_unknown_is_stale() {
@ -630,7 +623,7 @@ mod test {
let updated = Arc::new(AtomicBool::new(false));
let update = fake_updater(updated.clone());
let (_tx, rx) = mpsc::channel(1);
let (_tx, rx) = mpsc::unbounded();
tokio::spawn(checker.run_inner(rx, update, fake_version_check, fake_version_check));
talpid_time::sleep(Duration::from_secs(10)).await;
@ -648,7 +641,7 @@ mod test {
let updated = Arc::new(AtomicBool::new(false));
let update = fake_updater(updated.clone());
let (_tx, rx) = mpsc::channel(1);
let (_tx, rx) = mpsc::unbounded();
tokio::spawn(checker.run_inner(rx, update, fake_version_check, fake_version_check));
assert!(!updated.load(Ordering::SeqCst));
@ -677,7 +670,7 @@ mod test {
let updated = Arc::new(AtomicBool::new(false));
let update = fake_updater(updated.clone());
let (mut tx, rx) = mpsc::channel(1);
let (mut tx, rx) = mpsc::unbounded();
tokio::spawn(checker.run_inner(rx, update, fake_version_check, fake_version_check_err));
// Fail automatic update
@ -701,27 +694,26 @@ mod test {
}
async fn send_version_request(
tx: &mut mpsc::Sender<VersionUpdaterCommand>,
tx: &mut mpsc::UnboundedSender<VersionUpdateCommand>,
) -> Result<(), futures::channel::mpsc::SendError> {
let (done_tx, _done_rx) = oneshot::channel();
tx.send(VersionUpdaterCommand::GetVersionInfo(done_tx))
.await
tx.send(done_tx).await
}
fn fake_updater(
updated: Arc<AtomicBool>,
) -> impl Fn(AppVersionInfo) -> BoxFuture<'static, Result<(), Error>> {
) -> impl Fn(VersionCache) -> BoxFuture<'static, Result<(), Error>> {
move |_new_version| {
updated.store(true, Ordering::SeqCst);
Box::pin(async { Ok(()) })
}
}
fn fake_version_check() -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
fn fake_version_check() -> BoxFuture<'static, Result<VersionCache, Error>> {
Box::pin(async { Ok(fake_version_response()) })
}
fn fake_version_check_err() -> BoxFuture<'static, Result<AppVersionResponse, Error>> {
fn fake_version_check_err() -> BoxFuture<'static, Result<VersionCache, Error>> {
Box::pin(retry_future(
|| async { Err(Error::Download(mullvad_api::rest::Error::TimeoutError)) },
|_| true,
@ -729,105 +721,20 @@ mod test {
))
}
fn fake_version_response() -> AppVersionResponse {
AppVersionResponse {
supported: true,
latest: "2024.1".to_owned(),
latest_stable: None,
latest_beta: "2024.1-beta1".to_owned(),
fn fake_version_response() -> VersionCache {
// TODO: The tests pass, but check that this is a sane fake version cache anyway
VersionCache {
current_version_supported: true,
latest_version: VersionInfo {
stable: Version {
version: "2025.5".parse::<mullvad_version::Version>().unwrap(),
urls: vec![],
size: 0,
changelog: "".to_owned(),
sha256: [0u8; 32],
},
beta: None,
},
}
}
#[test]
fn test_version_upgrade_suggestions() {
let latest_stable = Some("2020.4".to_string());
let latest_beta = "2020.5-beta3";
let older_stable = Version::from_str("2020.3").unwrap();
let current_stable = Version::from_str("2020.4").unwrap();
let newer_stable = Version::from_str("2021.5").unwrap();
let older_beta = Version::from_str("2020.3-beta3").unwrap();
let current_beta = Version::from_str("2020.5-beta3").unwrap();
let newer_beta = Version::from_str("2021.5-beta3").unwrap();
let older_alpha = Version::from_str("2020.3-alpha3").unwrap();
let current_alpha = Version::from_str("2020.5-alpha3").unwrap();
let newer_alpha = Version::from_str("2021.5-alpha3").unwrap();
assert_eq!(
suggested_upgrade(&older_stable, &latest_stable, latest_beta, false),
Some("2020.4".to_owned())
);
assert_eq!(
suggested_upgrade(&older_stable, &latest_stable, latest_beta, true),
Some("2020.5-beta3".to_owned())
);
assert_eq!(
suggested_upgrade(&current_stable, &latest_stable, latest_beta, false),
None
);
assert_eq!(
suggested_upgrade(&current_stable, &latest_stable, latest_beta, true),
Some("2020.5-beta3".to_owned())
);
assert_eq!(
suggested_upgrade(&newer_stable, &latest_stable, latest_beta, false),
None
);
assert_eq!(
suggested_upgrade(&newer_stable, &latest_stable, latest_beta, true),
None
);
assert_eq!(
suggested_upgrade(&older_beta, &latest_stable, latest_beta, false),
Some("2020.4".to_owned())
);
assert_eq!(
suggested_upgrade(&older_beta, &latest_stable, latest_beta, true),
Some("2020.5-beta3".to_owned())
);
assert_eq!(
suggested_upgrade(&current_beta, &latest_stable, latest_beta, false),
None
);
assert_eq!(
suggested_upgrade(&current_beta, &latest_stable, latest_beta, true),
None
);
assert_eq!(
suggested_upgrade(&newer_beta, &latest_stable, latest_beta, false),
None
);
assert_eq!(
suggested_upgrade(&newer_beta, &latest_stable, latest_beta, true),
None
);
assert_eq!(
suggested_upgrade(&older_alpha, &latest_stable, latest_beta, false),
Some("2020.4".to_owned())
);
assert_eq!(
suggested_upgrade(&older_alpha, &latest_stable, latest_beta, true),
Some("2020.5-beta3".to_owned())
);
assert_eq!(
suggested_upgrade(&current_alpha, &latest_stable, latest_beta, false),
None,
);
assert_eq!(
suggested_upgrade(&current_alpha, &latest_stable, latest_beta, true),
Some("2020.5-beta3".to_owned())
);
assert_eq!(
suggested_upgrade(&newer_alpha, &latest_stable, latest_beta, false),
None
);
assert_eq!(
suggested_upgrade(&newer_alpha, &latest_stable, latest_beta, true),
None
);
}
}

View File

@ -0,0 +1,142 @@
#![cfg(update)]
use futures::channel::{mpsc, oneshot};
use mullvad_update::app::{AppDownloader, AppDownloaderParameters, HttpAppDownloader};
use rand::seq::SliceRandom;
use std::time::Duration;
use std::{future::Future, path::PathBuf};
use tokio::fs;
use super::Error;
type Result<T> = std::result::Result<T, Error>;
pub struct Downloader(());
pub type AbortHandle = oneshot::Sender<()>;
/// App updater event
pub enum UpdateEvent {
/// Download progress update
Downloading {
/// Server that the app is being downloaded from
server: String,
/// A fraction in `[0,1]` that describes how much of the installer has been downloaded
complete_frac: f32,
/// Estimated time left
time_left: Duration,
},
/// Download failed due to some error
DownloadFailed,
/// Download completed, so verifying now
Verifying,
/// The verification failed due to some error
VerificationFailed,
/// There is a downloaded and verified installer available
Verified { verified_installer_path: PathBuf },
}
impl Downloader {
/// Begin or resume download of `version`
pub async fn start(
version: mullvad_update::version::Version,
event_tx: mpsc::UnboundedSender<UpdateEvent>,
) -> Result<impl Future<Output = ()>> {
let url = select_cdn_url(&version.urls)
.ok_or(Error::NoUrlFound)?
.to_owned();
let download_dir = mullvad_paths::cache_dir()?.join("mullvad-update");
fs::create_dir_all(&download_dir)
.await
.map_err(Error::CreateDownloadDir)?;
let params = AppDownloaderParameters {
app_version: version.version,
app_url: url.clone(),
app_size: version.size,
app_progress: ProgressUpdater::new(server_from_url(&url), event_tx.clone()),
app_sha256: version.sha256,
cache_dir: download_dir,
};
let mut downloader = HttpAppDownloader::from(params);
Ok(async move {
if let Err(_error) = downloader.download_executable().await {
let _ = event_tx.unbounded_send(UpdateEvent::DownloadFailed);
return;
}
let _ = event_tx.unbounded_send(UpdateEvent::Verifying);
if let Err(_error) = downloader.verify().await {
let _ = event_tx.unbounded_send(UpdateEvent::VerificationFailed);
return;
}
let _ = event_tx.unbounded_send(UpdateEvent::Verified {
verified_installer_path: downloader.bin_path(),
});
})
}
}
struct ProgressUpdater {
server: String,
event_tx: mpsc::UnboundedSender<UpdateEvent>,
complete_frac: f32,
}
impl ProgressUpdater {
fn new(server: String, event_tx: mpsc::UnboundedSender<UpdateEvent>) -> Self {
Self {
server,
event_tx,
complete_frac: 0.,
}
}
}
impl mullvad_update::fetch::ProgressUpdater for ProgressUpdater {
fn set_url(&mut self, _url: &str) {
// ignored since we already know the URL
}
fn set_progress(&mut self, fraction_complete: f32) {
if (self.complete_frac - fraction_complete).abs() < 0.01 {
return;
}
self.complete_frac = fraction_complete;
let _ = self.event_tx.unbounded_send(UpdateEvent::Downloading {
server: self.server.clone(),
complete_frac: fraction_complete,
// TODO: estimate time left based on how much was downloaded (maybe in last n seconds)
time_left: Duration::ZERO,
});
}
fn clear_progress(&mut self) {
self.complete_frac = 0.;
let _ = self.event_tx.unbounded_send(UpdateEvent::Downloading {
server: self.server.clone(),
complete_frac: 0.,
// TODO: Check if this is reasonable
time_left: Duration::ZERO,
});
}
}
/// Select a mirror to download from
/// Currently, the selection is random
fn select_cdn_url(urls: &[String]) -> Option<&str> {
urls.choose(&mut rand::thread_rng()).map(String::as_str)
}
/// Extract domain name from a URL
fn server_from_url(url: &str) -> String {
let url = url.strip_prefix("https://").unwrap_or(url);
let (server, _) = url.split_once('/').unwrap_or((url, ""));
server.to_owned()
}

View File

@ -0,0 +1,70 @@
use std::io;
pub mod check;
pub mod downloader;
pub mod router;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to open app version cache file for reading")]
ReadVersionCache(#[source] io::Error),
#[error("Failed to open app version cache file for writing")]
WriteVersionCache(#[source] io::Error),
#[error("Failure in serialization of the version info")]
Serialize(#[source] serde_json::Error),
#[error("Failure in deserialization of the version info")]
Deserialize(#[source] serde_json::Error),
#[error("Failed to check the latest app version")]
Download(#[source] mullvad_api::rest::Error),
#[error("API availability check failed")]
ApiCheck(#[source] mullvad_api::availability::Error),
#[error("Response is missing a valid stable version")]
MissingStable,
#[error("Clearing version check cache due to old version")]
OutdatedVersion,
#[error("Version updater is down")]
VersionUpdaterDown,
#[error("Version router is down")]
VersionRouterClosed,
#[error("Version cache update was aborted")]
UpdateAborted,
#[error("Failed to get download directory")]
GetDownloadDir(#[from] mullvad_paths::Error),
#[error("Failed to create download directory")]
CreateDownloadDir(#[source] io::Error),
#[error("Could not select URL for app update")]
NoUrlFound,
}
/// Contains the date of the git commit this was built from
pub const COMMIT_DATE: &str = include_str!(concat!(env!("OUT_DIR"), "/git-commit-date.txt"));
pub fn is_beta_version() -> bool {
mullvad_version::VERSION.contains("beta")
}
pub fn is_dev_version() -> bool {
mullvad_version::VERSION.contains("dev")
}
pub fn log_version() {
log::info!(
"Starting {} - {} {}",
env!("CARGO_PKG_NAME"),
mullvad_version::VERSION,
COMMIT_DATE,
)
}

View File

@ -0,0 +1,602 @@
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use futures::channel::{mpsc, oneshot};
use futures::future::{Fuse, FusedFuture};
use futures::stream::StreamExt;
use futures::FutureExt;
use mullvad_api::{availability::ApiAvailability, rest::MullvadRestHandle};
use mullvad_types::version::{AppVersionInfo, SuggestedUpgrade};
use mullvad_update::version::VersionInfo;
use talpid_core::mpsc::Sender;
use crate::DaemonEventSender;
use super::{
check::{self, VersionCache, VersionUpdater},
Error,
};
#[cfg(update)]
use super::downloader;
#[cfg(update)]
use mullvad_types::version::AppUpgradeEvent;
#[cfg(update)]
use std::mem;
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone)]
pub struct VersionRouterHandle {
tx: mpsc::UnboundedSender<Message>,
}
impl VersionRouterHandle {
pub async fn set_show_beta_releases(&self, state: bool) -> Result<()> {
let (result_tx, result_rx) = oneshot::channel();
self.tx
.send(Message::SetBetaProgram { state, result_tx })
.map_err(|_| Error::VersionRouterClosed)?;
result_rx.await.map_err(|_| Error::VersionRouterClosed)
}
pub async fn get_latest_version(&self) -> Result<mullvad_types::version::AppVersionInfo> {
let (result_tx, result_rx) = oneshot::channel();
self.tx
.send(Message::GetLatestVersion(result_tx))
.map_err(|_| Error::VersionRouterClosed)?;
result_rx.await.map_err(|_| Error::VersionRouterClosed)?
}
#[cfg(update)]
pub async fn update_application(&self) -> Result<()> {
let (result_tx, result_rx) = oneshot::channel();
self.tx
.send(Message::UpdateApplication { result_tx })
.map_err(|_| Error::VersionRouterClosed)?;
result_rx.await.map_err(|_| Error::VersionRouterClosed)
}
#[cfg(update)]
pub async fn cancel_update(&self) -> Result<()> {
let (result_tx, result_rx) = oneshot::channel();
self.tx
.send(Message::CancelUpdate { result_tx })
.map_err(|_| Error::VersionRouterClosed)?;
result_rx.await.map_err(|_| Error::VersionRouterClosed)
}
#[cfg(update)]
pub fn new_upgrade_event_listener(
&self,
) -> Result<mpsc::UnboundedReceiver<mullvad_types::version::AppUpgradeEvent>> {
let (event_tx, event_rx) = mpsc::unbounded();
self.tx
.send(Message::NewUpgradeEventListener { event_tx })
.map_err(|_| Error::VersionRouterClosed)?;
Ok(event_rx)
}
}
/// Router of version updates and update requests.
///
/// New available app version events are forwarded from the [`VersionUpdater`].
/// If an update is in progress, these events are paused until the update is completed or canceled.
/// This is done to prevent frontends from confusing which version is currently being installed,
/// in case new version info is received while the update is in progress.
pub struct VersionRouter {
rx: mpsc::UnboundedReceiver<Message>,
state: RoutingState,
beta_program: bool,
version_event_sender: DaemonEventSender<mullvad_types::version::AppVersionInfo>,
/// Version updater
version_check: check::VersionUpdaterHandle,
/// Channel used to receive updates from `version_check`
new_version_rx: mpsc::UnboundedReceiver<VersionCache>,
/// Future that resolves when `get_latest_version` resolves
version_request: Fuse<Pin<Box<dyn Future<Output = Result<VersionCache>> + Send>>>,
/// Channels that receive responses to `get_latest_version`
version_request_channels: Vec<oneshot::Sender<Result<mullvad_types::version::AppVersionInfo>>>,
#[cfg(update)]
update: Update,
}
#[cfg(update)]
struct Update {
/// Channel used to send upgrade events from [downloader::Downloader]
update_event_tx: mpsc::UnboundedSender<downloader::UpdateEvent>,
/// Channel used to receive upgrade events from [downloader::Downloader]
update_event_rx: mpsc::UnboundedReceiver<downloader::UpdateEvent>,
/// Clients that will also receive events
upgrade_listeners: Vec<mpsc::UnboundedSender<AppUpgradeEvent>>,
}
#[cfg(update)]
impl Update {
fn new() -> Self {
let (update_event_tx, update_event_rx) = mpsc::unbounded();
Self {
update_event_tx,
update_event_rx,
upgrade_listeners: Vec::default(),
}
}
}
enum Message {
/// Enable or disable beta program
SetBetaProgram {
state: bool,
result_tx: oneshot::Sender<()>,
},
/// Check for updates
GetLatestVersion(oneshot::Sender<Result<mullvad_types::version::AppVersionInfo>>),
/// Update the application
#[cfg(update)]
UpdateApplication { result_tx: oneshot::Sender<()> },
/// Cancel the ongoing update
#[cfg(update)]
CancelUpdate { result_tx: oneshot::Sender<()> },
/// Listen for events
#[cfg(update)]
NewUpgradeEventListener {
/// Channel for receiving update events
event_tx: mpsc::UnboundedSender<AppUpgradeEvent>,
},
}
#[derive(Debug)]
enum RoutingState {
/// There is no version available yet
NoVersion,
/// Running version checker, no upgrade in progress
HasVersion { version_info: VersionCache },
/// Download is in progress, so we don't forward version checks
Downloading {
/// Version info received from `HasVersion`
version_info: VersionCache,
/// The version being upgraded to (derived from `suggested_upgrade`).
/// Should be one of the versions in `version_info`.
upgrading_to_version: mullvad_update::version::Version,
/// Version check update received while paused
/// When transitioning out of `Upgrading`, this will cause `version_info` to be updated
new_version: Option<VersionCache>,
/// Tokio task for the downloader handle
downloader_handle: tokio::task::JoinHandle<()>,
},
/// Download is complete. We have a verified binary
Downloaded {
/// Version info received from `HasVersion`
version_info: VersionCache,
/// Path to verified installer
verified_installer_path: PathBuf,
},
}
impl VersionRouter {
pub(crate) fn spawn(
api_handle: MullvadRestHandle,
availability_handle: ApiAvailability,
cache_dir: PathBuf,
version_event_sender: DaemonEventSender<mullvad_types::version::AppVersionInfo>,
beta_program: bool,
) -> VersionRouterHandle {
let (tx, rx) = mpsc::unbounded();
tokio::spawn(async move {
let (new_version_tx, new_version_rx) = mpsc::unbounded();
let version_check =
VersionUpdater::spawn(api_handle, availability_handle, cache_dir, new_version_tx)
.await;
// TODO: tokio::join! here?
Self {
rx,
state: RoutingState::NoVersion,
beta_program,
version_check,
version_event_sender,
new_version_rx,
version_request: Fuse::terminated(),
version_request_channels: vec![],
#[cfg(update)]
update: Update::new(),
}
.run()
.await;
});
VersionRouterHandle { tx }
}
async fn run(mut self) {
// HACK: We can (should) only handle update events on some targets.
// Trying to cfg a branch in `tokio::select!` will not work, so creating
// a closure for conditionally responding to upgrade events will have to do.
#[cfg(update)]
let handle_update_event = || async {
// Received upgrade event from `downloader`
if let Some(update_event) = self.update.update_event_rx.next().await {
self.handle_update_event(update_event);
};
};
#[cfg(not(update))]
let handle_update_event = || async {
let () = std::future::pending().await;
};
loop {
tokio::select! {
// Respond to version info requests
update_result = &mut self.version_request => {
match update_result {
Ok(new_version) => {
self.on_new_version(new_version.clone());
}
Err(error) => {
log::error!("Failed to retrieve version: {error}");
for tx in self.version_request_channels.drain(..) {
// TODO: More appropriate error? But Error isn't Clone
let _ = tx.send(Err(Error::UpdateAborted));
}
}
}
}
// Received version event from `check`
Some(new_version) = self.new_version_rx.next() => {
self.on_new_version(new_version);
}
// Received & handled update event from `downloader`
() = handle_update_event() => { },
Some(message) = self.rx.next() => self.handle_message(message).await,
else => break,
}
}
log::info!("Version router closed");
}
/// Handle [Message] sent by user
#[cfg_attr(not(update), allow(clippy::unused_async))]
async fn handle_message(&mut self, message: Message) {
match message {
Message::SetBetaProgram { state, result_tx } => {
self.set_beta_program(state);
// We're happy as soon as the internal state has changed; no need to wait for
// version update
let _ = result_tx.send(());
}
Message::GetLatestVersion(result_tx) => {
self.get_latest_version(result_tx);
}
#[cfg(update)]
Message::UpdateApplication { result_tx } => {
self.update_application().await;
let _ = result_tx.send(());
}
#[cfg(update)]
Message::CancelUpdate { result_tx } => {
self.cancel_upgrade().await;
let _ = result_tx.send(());
}
#[cfg(update)]
Message::NewUpgradeEventListener {
event_tx: result_tx,
} => {
self.update.upgrade_listeners.push(result_tx);
}
}
}
fn set_beta_program(&mut self, new_state: bool) {
let prev_state = self.beta_program;
if new_state == prev_state {
return;
}
self.beta_program = new_state;
match &self.state {
// Emit version event if suggested upgrade changes
RoutingState::HasVersion { version_info }
| RoutingState::Downloaded { version_info, .. } => {
let prev_app_version_info = to_app_version_info(version_info, prev_state);
let new_app_version_info = to_app_version_info(version_info, new_state);
if new_app_version_info != prev_app_version_info {
let _ = self.version_event_sender.send(new_app_version_info);
// Note: If we're in the `Downloaded` state, this resets the state to `HasVersion`
self.state = RoutingState::HasVersion {
version_info: version_info.clone(),
};
self.notify_version_requesters();
}
}
// If there's no version or upgrading, do nothing
RoutingState::NoVersion | RoutingState::Downloading { .. } => (),
}
}
fn get_latest_version(
&mut self,
result_tx: oneshot::Sender<std::result::Result<AppVersionInfo, Error>>,
) {
match &self.state {
// When not upgrading, potentially fetch new version info, and append `result_tx` to
// list of channels to notify.
// We don't wait on `get_version_info` so that we don't block user commands.
RoutingState::NoVersion
| RoutingState::HasVersion { .. }
| RoutingState::Downloaded { .. } => {
// Start a version request unless already in progress
if self.version_request.is_terminated() {
let check = self.version_check.clone();
let check_fut: Pin<Box<dyn Future<Output = Result<VersionCache>> + Send>> =
Box::pin(async move { check.get_version_info().await });
self.version_request = check_fut.fuse();
}
// Append to response channels
self.version_request_channels.push(result_tx);
}
// During upgrades, just pass on the last known version
RoutingState::Downloading {
version_info,
upgrading_to_version,
new_version: _,
downloader_handle: _,
} => {
let suggested_upgrade = suggested_upgrade_for_version(upgrading_to_version);
let info = AppVersionInfo {
current_version_supported: version_info.current_version_supported,
suggested_upgrade: Some(suggested_upgrade),
};
let _ = result_tx.send(Ok(info));
}
}
}
#[cfg(update)]
async fn update_application(&mut self) {
match mem::replace(&mut self.state, RoutingState::NoVersion) {
// Checking state: start upgrade, if upgrade is available
RoutingState::HasVersion { version_info } => {
let Some(suggested_upgrade) =
suggested_upgrade(&version_info.latest_version, self.beta_program)
else {
// If there's no suggested upgrade, do nothing
log::trace!("Received update request without suggested upgrade");
self.state = RoutingState::HasVersion { version_info };
return;
};
let downloader_handle = tokio::spawn(
downloader::Downloader::start(
suggested_upgrade.clone(),
self.update_event_tx.clone(),
)
.await
.expect("TODO: handle err"),
);
log::debug!("Starting upgrade");
self.state = RoutingState::Downloading {
version_info,
upgrading_to_version: suggested_upgrade,
new_version: None,
downloader_handle,
};
// Notify callers of `get_latest_version`: cancel the version check and
// advertise the last known version as latest
self.notify_version_requesters();
}
// Already downloading/downloaded or there is no version: do nothing
state => {
self.state = state;
}
}
}
#[cfg(update)]
async fn cancel_upgrade(&mut self) {
match mem::replace(&mut self.state, RoutingState::NoVersion) {
// If we're upgrading, emit an event if a version was received during the upgrade
// Otherwise, just reset upgrade info to last known state
RoutingState::Downloading {
version_info,
upgrading_to_version: _,
new_version,
downloader_handle,
} => {
// Abort download
downloader_handle.abort();
let _ = downloader_handle.await;
// Reset app version info to last known state
self.state = RoutingState::HasVersion { version_info };
// If we also received an upgrade, emit new version event
if let Some(version) = new_version {
let app_version = to_app_version_info(&version, self.beta_program);
let _ = self.version_event_sender.send(app_version);
}
}
// No-op unless we're downloading something right now
// In the `Downloaded` state, we also do nothing
state => self.state = state,
};
}
/// Handle new version info
///
/// If the router is in the process of upgrading, it will not propagate versions, but only
/// remember it for when it transitions back into the "idle" (version check) state.
fn on_new_version(&mut self, version: VersionCache) {
match &mut self.state {
// Set app version info
RoutingState::NoVersion => {
self.state = RoutingState::HasVersion {
version_info: version.clone(),
};
// Initial version is propagated
let app_version_info = to_app_version_info(&version, self.beta_program);
let _ = self.version_event_sender.send(app_version_info);
}
// Update app version info
RoutingState::HasVersion {
version_info: prev_version,
..
}
| RoutingState::Downloaded {
version_info: prev_version,
..
} => {
// If the version changed, notify channel
// Note: Must account for beta program state
let prev_app_version = to_app_version_info(prev_version, self.beta_program);
let new_app_version = to_app_version_info(&version, self.beta_program);
if new_app_version != prev_app_version {
let _ = self.version_event_sender.send(new_app_version);
}
// Note: If we're in the `Downloaded` state, this resets the state to `HasVersion`
if prev_version != &version {
self.state = RoutingState::HasVersion {
version_info: version,
};
}
}
// If we're upgrading, remember the new version, but don't send any notification
RoutingState::Downloading {
ref mut new_version,
..
} => {
*new_version = Some(version);
}
}
// Notify callers of `get_latest_version`
self.notify_version_requesters();
}
#[cfg(update)]
fn handle_update_event(&mut self, event: downloader::UpdateEvent) {
debug_assert!(
matches!(self.state, RoutingState::Downloading { .. }),
"unexpected routing state: {:?}",
self.state
);
use downloader::UpdateEvent;
match event {
UpdateEvent::Downloading {
server,
complete_frac: f32,
time_left,
} => {
// TODO: emit version event to clients
}
UpdateEvent::DownloadFailed => {
// TODO: transition to HasVersion state
// TODO: emit version event to clients
}
UpdateEvent::Verifying => {
// TODO: emit version event to clients
}
UpdateEvent::VerificationFailed => {
// TODO: transition to HasVersion state
// TODO: emit version event to clients
}
UpdateEvent::Verified {
verified_installer_path,
} => {
// TODO: transition to Downloaded state
// TODO: emit version event to clients
}
}
}
/// Notify clients requesting a version
fn notify_version_requesters(&mut self) {
// Cancel update notifications
self.version_request = Fuse::terminated();
let version_info = match &self.state {
RoutingState::NoVersion => {
log::error!("Dropping version request channels since there's no version");
self.version_request_channels.clear();
return;
}
// Update app version info
RoutingState::HasVersion { version_info }
| RoutingState::Downloaded { version_info, .. } => {
to_app_version_info(version_info, self.beta_program)
}
// If we're upgrading, emit the version we're currently upgrading to
RoutingState::Downloading {
version_info,
upgrading_to_version,
..
} => {
let suggested_upgrade = suggested_upgrade_for_version(upgrading_to_version);
AppVersionInfo {
current_version_supported: version_info.current_version_supported,
suggested_upgrade: Some(suggested_upgrade),
}
}
};
// Notify all requesters
for tx in self.version_request_channels.drain(..) {
let _ = tx.send(Ok(version_info.clone()));
}
}
}
/// Extract [`AppVersionInfo`], containing upgrade version and `current_version_supported`
/// from [VersionCache] and beta program state.
fn to_app_version_info(cache: &VersionCache, beta_program: bool) -> AppVersionInfo {
let current_version_supported = cache.current_version_supported;
let suggested_upgrade = suggested_upgrade(&cache.latest_version, beta_program)
.as_ref()
.map(suggested_upgrade_for_version);
AppVersionInfo {
current_version_supported,
suggested_upgrade,
}
}
/// Extract upgrade version from [VersionCache] based on `beta_program`
fn suggested_upgrade(
version_info: &VersionInfo,
beta_program: bool,
) -> Option<mullvad_update::version::Version> {
let version_details = if beta_program {
version_info.beta.as_ref().unwrap_or(&version_info.stable)
} else {
&version_info.stable
};
// Set suggested upgrade if the received version is newer than the current version
let current_version = mullvad_version::VERSION.parse().unwrap();
if version_details.version > current_version {
Some(version_details.to_owned())
} else {
None
}
}
/// Convert [mullvad_update::version::Version] to [SuggestedUpgrade]
fn suggested_upgrade_for_version(
version_details: &mullvad_update::version::Version,
) -> SuggestedUpgrade {
SuggestedUpgrade {
version: version_details.version.clone(),
changelog: Some(version_details.changelog.clone()),
// TODO: This should return the downloaded & verified path, if it exists
verified_installer_path: None,
}
}

View File

@ -162,7 +162,7 @@ message AppUpgradeError {
enum Error {
GENERAL_ERROR = 0;
DOWNLOAD_FAILED = 1;
VERFICATION_FAILED = 2;
VERIFICATION_FAILED = 2;
}
Error error = 1;
}

View File

@ -67,9 +67,9 @@ impl TryFrom<types::daemon_event::Event> for DaemonEvent {
types::daemon_event::Event::RelayList(list) => RelayList::try_from(list)
.map(DaemonEvent::RelayList)
.map_err(Error::InvalidResponse),
types::daemon_event::Event::VersionInfo(info) => {
Ok(DaemonEvent::AppVersionInfo(AppVersionInfo::from(info)))
}
types::daemon_event::Event::VersionInfo(info) => AppVersionInfo::try_from(info)
.map(DaemonEvent::AppVersionInfo)
.map_err(Error::InvalidResponse),
types::daemon_event::Event::Device(event) => DeviceEvent::try_from(event)
.map(DaemonEvent::Device)
.map_err(Error::InvalidResponse),
@ -192,7 +192,7 @@ impl MullvadProxyClient {
.await
.map_err(Error::Rpc)?
.into_inner();
Ok(AppVersionInfo::from(version_info))
AppVersionInfo::try_from(version_info).map_err(Error::InvalidResponse)
}
pub async fn get_relay_locations(&mut self) -> Result<RelayList> {

View File

@ -1,23 +1,188 @@
use std::path::PathBuf;
use crate::types::proto;
use mullvad_types::version::*;
impl From<mullvad_types::version::AppVersionInfo> for proto::AppVersionInfo {
fn from(version_info: mullvad_types::version::AppVersionInfo) -> Self {
use super::FromProtobufTypeError;
impl From<AppVersionInfo> for proto::AppVersionInfo {
fn from(version_info: AppVersionInfo) -> Self {
Self {
supported: version_info.supported,
latest_stable: version_info.latest_stable,
latest_beta: version_info.latest_beta,
suggested_upgrade: version_info.suggested_upgrade,
supported: version_info.current_version_supported,
suggested_upgrade: version_info
.suggested_upgrade
.map(proto::SuggestedUpgrade::from),
}
}
}
impl From<proto::AppVersionInfo> for mullvad_types::version::AppVersionInfo {
fn from(version_info: proto::AppVersionInfo) -> Self {
impl TryFrom<proto::AppVersionInfo> for AppVersionInfo {
type Error = FromProtobufTypeError;
fn try_from(version_info: proto::AppVersionInfo) -> Result<Self, Self::Error> {
Ok(Self {
current_version_supported: version_info.supported,
suggested_upgrade: version_info
.suggested_upgrade
.map(SuggestedUpgrade::try_from)
.transpose()?,
})
}
}
impl From<SuggestedUpgrade> for proto::SuggestedUpgrade {
fn from(suggested_upgrade: SuggestedUpgrade) -> Self {
Self {
supported: version_info.supported,
latest_stable: version_info.latest_stable,
latest_beta: version_info.latest_beta,
suggested_upgrade: version_info.suggested_upgrade,
version: suggested_upgrade.version.to_string(),
changelog: suggested_upgrade.changelog,
verified_installer_path: suggested_upgrade
.verified_installer_path
.and_then(|path| path.to_str().map(str::to_owned)),
}
}
}
impl TryFrom<proto::SuggestedUpgrade> for SuggestedUpgrade {
type Error = FromProtobufTypeError;
fn try_from(suggested_upgrade: proto::SuggestedUpgrade) -> Result<Self, Self::Error> {
// TODO: we probably don't need to convert in this direction
let version = suggested_upgrade.version.parse().map_err(|_err| {
FromProtobufTypeError::InvalidArgument("invalid Mullvad app version")
})?;
let verified_installer_path = suggested_upgrade
.verified_installer_path
.map(|path| PathBuf::from(&path));
Ok(Self {
version,
changelog: suggested_upgrade.changelog,
verified_installer_path,
})
}
}
impl From<AppUpgradeEvent> for proto::AppUpgradeEvent {
fn from(upgrade_event: AppUpgradeEvent) -> Self {
type ProtoEvent = proto::app_upgrade_event::Event;
let event = match upgrade_event {
AppUpgradeEvent::DownloadStarting => {
ProtoEvent::DownloadStarting(proto::AppUpgradeDownloadStarting {})
}
AppUpgradeEvent::DownloadProgress(progress) => {
ProtoEvent::DownloadProgress(progress.into())
}
AppUpgradeEvent::VerifyingInstaller => {
ProtoEvent::VerifyingInstaller(proto::AppUpgradeVerifyingInstaller {})
}
AppUpgradeEvent::VerifiedInstaller => {
ProtoEvent::VerifiedInstaller(proto::AppUpgradeVerifiedInstaller {})
}
AppUpgradeEvent::Aborted => ProtoEvent::UpgradeAborted(proto::AppUpgradeAborted {}),
AppUpgradeEvent::Error(app_upgrade_error) => {
ProtoEvent::Error(app_upgrade_error.into())
}
};
Self { event: Some(event) }
}
}
impl TryFrom<proto::AppUpgradeEvent> for AppUpgradeEvent {
type Error = FromProtobufTypeError;
fn try_from(upgrade_event: proto::AppUpgradeEvent) -> Result<Self, FromProtobufTypeError> {
type ProtoEvent = proto::app_upgrade_event::Event;
let event = upgrade_event
.event
.ok_or(FromProtobufTypeError::InvalidArgument(
"Non-existent AppUpgradeEvent",
))?;
let event = match event {
ProtoEvent::DownloadStarting(_starting) => AppUpgradeEvent::DownloadStarting,
ProtoEvent::DownloadProgress(progress) => {
let progress = AppUpgradeDownloadProgress::try_from(progress)?;
AppUpgradeEvent::DownloadProgress(progress)
}
ProtoEvent::VerifyingInstaller(_verifying) => AppUpgradeEvent::VerifyingInstaller,
ProtoEvent::VerifiedInstaller(_verified) => AppUpgradeEvent::VerifiedInstaller,
ProtoEvent::UpgradeAborted(_aborted) => AppUpgradeEvent::Aborted,
ProtoEvent::Error(error) => {
let error = AppUpgradeError::try_from(error)?;
AppUpgradeEvent::Error(error)
}
};
Ok(event)
}
}
impl From<AppUpgradeDownloadProgress> for proto::AppUpgradeDownloadProgress {
fn from(value: AppUpgradeDownloadProgress) -> Self {
// From the docs: Converts a std::time::Duration to a Duration, failing if the duration is too large.
let time_left = prost_types::Duration::try_from(value.time_left)
.expect("Failed to convert duration to protobuf");
proto::AppUpgradeDownloadProgress {
server: value.server,
progress: value.progress,
time_left: Some(time_left),
}
}
}
impl TryFrom<proto::AppUpgradeDownloadProgress> for AppUpgradeDownloadProgress {
type Error = FromProtobufTypeError;
fn try_from(value: proto::AppUpgradeDownloadProgress) -> Result<Self, Self::Error> {
let Some(time_left) = value.time_left else {
return Err(FromProtobufTypeError::InvalidArgument(
"Non-existent AppUpgradeDownloadProgress::time_left",
));
};
// From the docs: Converts a Duration to a std::time::Duration, failing if the duration is negative.
let time_left = std::time::Duration::try_from(time_left)
.expect("Failed to convert duration to std::time::Duration");
let progress = AppUpgradeDownloadProgress {
server: value.server,
progress: value.progress,
time_left,
};
Ok(progress)
}
}
impl From<AppUpgradeError> for proto::AppUpgradeError {
fn from(value: AppUpgradeError) -> Self {
type ProtoError = proto::app_upgrade_error::Error;
match value {
AppUpgradeError::GeneralError => proto::AppUpgradeError {
error: ProtoError::GeneralError as i32,
},
AppUpgradeError::DownloadFailed => proto::AppUpgradeError {
error: ProtoError::DownloadFailed as i32,
},
AppUpgradeError::VerificationFailed => proto::AppUpgradeError {
error: ProtoError::VerificationFailed as i32,
},
}
}
}
impl TryFrom<proto::AppUpgradeError> for AppUpgradeError {
type Error = FromProtobufTypeError;
fn try_from(value: proto::AppUpgradeError) -> Result<Self, Self::Error> {
type ProtoError = proto::app_upgrade_error::Error;
let Ok(error) = ProtoError::try_from(value.error) else {
return Err(FromProtobufTypeError::InvalidArgument(
"invalid AppUpgradeError",
));
};
match error {
ProtoError::GeneralError => Ok(AppUpgradeError::GeneralError),
ProtoError::DownloadFailed => Ok(AppUpgradeError::DownloadFailed),
ProtoError::VerificationFailed => Ok(AppUpgradeError::VerificationFailed),
}
}
}

View File

@ -23,6 +23,6 @@ uuid = { version = "1.4.1", features = ["v4", "serde" ] }
talpid-types = { path = "../talpid-types" }
intersection-derive = { path = "intersection-derive" }
clap = { workspace = true , optional = true }
mullvad-version = { path = "../mullvad-version", features = ["serde"] }

View File

@ -1,3 +1,5 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// AppVersionInfo represents the current stable and the current latest release versions of the
@ -11,14 +13,41 @@ pub struct AppVersionInfo {
/// issues, so using it is no longer recommended.
///
/// The user should really upgrade when this is false.
pub supported: bool,
/// Latest stable version
pub latest_stable: AppVersion,
/// Equal to `latest_stable` when the newest release is a stable release. But will contain
/// beta versions when those are out for testing.
pub latest_beta: AppVersion,
/// Whether should update to newer version
pub suggested_upgrade: Option<AppVersion>,
pub current_version_supported: bool,
/// A newer version that may be upgraded to
pub suggested_upgrade: Option<SuggestedUpgrade>,
}
pub type AppVersion = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SuggestedUpgrade {
/// Version available for update
pub version: mullvad_version::Version,
/// Changelog
pub changelog: Option<String>,
/// Path to the available installer, iff it has been verified
pub verified_installer_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AppUpgradeDownloadProgress {
pub server: String,
pub progress: u32,
pub time_left: std::time::Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AppUpgradeEvent {
DownloadStarting,
DownloadProgress(AppUpgradeDownloadProgress),
Aborted,
VerifyingInstaller,
VerifiedInstaller,
Error(AppUpgradeError),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AppUpgradeError {
GeneralError,
DownloadFailed,
VerificationFailed,
}

View File

@ -35,9 +35,8 @@ pub const FULLY_ROLLED_OUT: Rollout = 1.;
/// Installer architecture
pub type VersionArchitecture = format::Architecture;
/// Version information derived from querying a [format::Response] using [VersionParameters]
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(serde::Serialize))]
/// Version update information derived from querying a [format::Response] and filtering with [VersionParameters]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct VersionInfo {
/// Stable version info
pub stable: Version,
@ -47,8 +46,7 @@ pub struct VersionInfo {
}
/// Contains information about a version for the current target
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct Version {
/// Version
pub version: mullvad_version::Version,

View File

@ -20,11 +20,13 @@ enum Target {
impl Target {
fn current_target() -> Result<Self, String> {
println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS");
let s = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS should be set");
match s.as_str() {
match env::var("CARGO_CFG_TARGET_OS")
.expect("CARGO_CFG_TARGET_OS should be set")
.as_str()
{
"android" => Ok(Self::Android),
"linux" | "windows" | "macos" => Ok(Self::Desktop),
_ => Err(s),
other => Err(other.to_owned()),
}
}
}