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:
parent
26b6b2567d
commit
17d57b983a
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
@ -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(¤t_stable, &latest_stable, latest_beta, false),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
suggested_upgrade(¤t_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(¤t_beta, &latest_stable, latest_beta, false),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
suggested_upgrade(¤t_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(¤t_alpha, &latest_stable, latest_beta, false),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
suggested_upgrade(¤t_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
|
||||
);
|
||||
}
|
||||
}
|
142
mullvad-daemon/src/version/downloader.rs
Normal file
142
mullvad-daemon/src/version/downloader.rs
Normal 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()
|
||||
}
|
70
mullvad-daemon/src/version/mod.rs
Normal file
70
mullvad-daemon/src/version/mod.rs
Normal 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,
|
||||
)
|
||||
}
|
602
mullvad-daemon/src/version/router.rs
Normal file
602
mullvad-daemon/src/version/router.rs
Normal 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,
|
||||
}
|
||||
}
|
@ -162,7 +162,7 @@ message AppUpgradeError {
|
||||
enum Error {
|
||||
GENERAL_ERROR = 0;
|
||||
DOWNLOAD_FAILED = 1;
|
||||
VERFICATION_FAILED = 2;
|
||||
VERIFICATION_FAILED = 2;
|
||||
}
|
||||
Error error = 1;
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"] }
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user