diff options
| author | Linus Färnstrand <linus@mullvad.net> | 2019-10-07 16:25:58 +0200 |
|---|---|---|
| committer | Linus Färnstrand <linus@mullvad.net> | 2019-10-07 16:25:58 +0200 |
| commit | 48d9e233e6bbe9c56372ee59324b786f770363d0 (patch) | |
| tree | 00ed94738e56b11e77023e5d95ddc54a1a5b7116 | |
| parent | 2db11de43396abfe1f6a295d9fd22511898f7a07 (diff) | |
| parent | 4c8ff3f8ec7baf674b95536a94e18e79440b428f (diff) | |
| download | mullvadvpn-48d9e233e6bbe9c56372ee59324b786f770363d0.tar.xz mullvadvpn-48d9e233e6bbe9c56372ee59324b786f770363d0.zip | |
Merge branch 'cache-version-check-in-daemon'
| -rw-r--r-- | android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt | 5 | ||||
| -rw-r--r-- | android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt | 4 | ||||
| -rw-r--r-- | android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt | 93 | ||||
| -rw-r--r-- | android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoFetcher.kt | 69 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 3 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 26 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 3 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/status.rs | 5 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 91 | ||||
| -rw-r--r-- | mullvad-daemon/src/main.rs | 9 | ||||
| -rw-r--r-- | mullvad-daemon/src/management_interface.rs | 18 | ||||
| -rw-r--r-- | mullvad-daemon/src/version_check.rs | 177 | ||||
| -rw-r--r-- | mullvad-jni/src/daemon_interface.rs | 5 | ||||
| -rw-r--r-- | mullvad-jni/src/jni_event_listener.rs | 39 | ||||
| -rw-r--r-- | mullvad-jni/src/lib.rs | 1 | ||||
| -rw-r--r-- | mullvad-types/src/lib.rs | 3 | ||||
| -rw-r--r-- | mullvad-types/src/version.rs | 2 |
17 files changed, 351 insertions, 202 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt index fe1ba2bc71..02596f6996 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadDaemon.kt @@ -14,6 +14,7 @@ import net.mullvad.mullvadvpn.util.EventNotifier class MullvadDaemon(val vpnService: MullvadVpnService) { val onSettingsChange = EventNotifier<Settings?>(null) + var onAppVersionInfoChange: ((AppVersionInfo) -> Unit)? = null var onKeygenEvent: ((KeygenEvent) -> Unit)? = null var onRelayListChange: ((RelayList) -> Unit)? = null var onTunnelStateChange: ((TunnelState) -> Unit)? = null @@ -43,6 +44,10 @@ class MullvadDaemon(val vpnService: MullvadVpnService) { private external fun initialize(vpnService: MullvadVpnService) + private fun notifyAppVersionInfoEvent(appVersionInfo: AppVersionInfo) { + onAppVersionInfoChange?.invoke(appVersionInfo) + } + private fun notifyKeygenEvent(event: KeygenEvent) { onKeygenEvent?.invoke(event) } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt index e36c184bfe..388e4c0a5d 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/MullvadVpnService.kt @@ -10,7 +10,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking -import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoFetcher import net.mullvad.mullvadvpn.dataproxy.ConnectionProxy import net.mullvad.mullvadvpn.model.TunConfig @@ -23,7 +22,6 @@ class MullvadVpnService : VpnService() { private lateinit var daemon: Deferred<MullvadDaemon> private lateinit var connectionProxy: ConnectionProxy private lateinit var notificationManager: ForegroundNotificationManager - private lateinit var versionInfoFetcher: AppVersionInfoFetcher override fun onCreate() { setUp() @@ -95,7 +93,6 @@ class MullvadVpnService : VpnService() { daemon = startDaemon() connectionProxy = ConnectionProxy(this, daemon) notificationManager = startNotificationManager() - versionInfoFetcher = AppVersionInfoFetcher(daemon, this) } private fun startDaemon() = GlobalScope.async(Dispatchers.Default) { @@ -130,6 +127,5 @@ class MullvadVpnService : VpnService() { private fun tearDown() { connectionProxy.onDestroy() notificationManager.onDestroy() - versionInfoFetcher.stop() } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt index 9bc6011fbc..cf62ffed0b 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt @@ -1,27 +1,27 @@ package net.mullvad.mullvadvpn.dataproxy import android.content.Context -import android.content.SharedPreferences -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.MainActivity +import net.mullvad.mullvadvpn.model.AppVersionInfo class AppVersionInfoCache(val parentActivity: MainActivity) { companion object { - val KEY_CURRENT_IS_SUPPORTED = "current_is_supported" - val KEY_CURRENT_IS_OUTDATED = "current_is_outdated" - val KEY_LAST_UPDATED = "last_updated" - val KEY_LATEST_STABLE = "latest_stable" - val KEY_LATEST = "latest" - val SHARED_PREFERENCES = "app_version_info_cache" + val LEGACY_SHARED_PREFERENCES = "app_version_info_cache" } - private val preferences: SharedPreferences - get() = parentActivity.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE) + private val daemon = parentActivity.daemon + private val setUpJob = setUp() - private val fetchCurrentVersionJob = fetchCurrentVersion() + private var appVersionInfo: AppVersionInfo? = null + set(value) { + synchronized(this) { + field = value + updateUpgradeVersion() + } + } var onUpdate: (() -> Unit)? = null set(value) { @@ -29,62 +29,59 @@ class AppVersionInfoCache(val parentActivity: MainActivity) { value?.invoke() } + val latestStable + get() = appVersionInfo?.latestStable + val latest + get() = appVersionInfo?.latest + val isSupported + get() = appVersionInfo?.currentIsSupported ?: true + var isOutdated = false + get() = appVersionInfo?.currentIsOutdated ?: false + var version: String? = null private set var isStable = true private set - var lastUpdated = 0L - private set - var isSupported = true - private set - var isOutdated = false - private set - var latestStable: String? = null - private set - var latest: String? = null - private set - var upgradeVersion: String? = null private set - private val listener = object : OnSharedPreferenceChangeListener { - override fun onSharedPreferenceChanged(preferences: SharedPreferences, key: String) { - when (key) { - KEY_CURRENT_IS_SUPPORTED -> isSupported = preferences.getBoolean(key, isSupported) - KEY_CURRENT_IS_OUTDATED -> isOutdated = preferences.getBoolean(key, isOutdated) - KEY_LAST_UPDATED -> lastUpdated = preferences.getLong(key, lastUpdated) - KEY_LATEST_STABLE -> latestStable = preferences.getString(key, latestStable) - KEY_LATEST -> latest = preferences.getString(key, latest) - else -> return - } - - updateUpgradeVersion() - } - } - fun onCreate() { - preferences.registerOnSharedPreferenceChangeListener(listener) - - lastUpdated = preferences.getLong(KEY_LAST_UPDATED, 0L) - isSupported = preferences.getBoolean(KEY_CURRENT_IS_SUPPORTED, true) - isOutdated = preferences.getBoolean(KEY_CURRENT_IS_OUTDATED, false) - latestStable = preferences.getString(KEY_LATEST_STABLE, null) - latest = preferences.getString(KEY_LATEST, null) + parentActivity.getSharedPreferences(LEGACY_SHARED_PREFERENCES, Context.MODE_PRIVATE) + .edit() + .clear() + .commit() } fun onDestroy() { - fetchCurrentVersionJob.cancel() - preferences.unregisterOnSharedPreferenceChangeListener(listener) + setUpJob.cancel() + tearDown() } - private fun fetchCurrentVersion() = GlobalScope.launch(Dispatchers.Default) { - val currentVersion = parentActivity.daemon.await().getCurrentVersion() + private fun setUp() = GlobalScope.launch(Dispatchers.Default) { + val daemon = this@AppVersionInfoCache.daemon.await() + val currentVersion = daemon.getCurrentVersion() version = currentVersion isStable = !currentVersion.contains("-") updateUpgradeVersion() + + daemon.onAppVersionInfoChange = { newAppVersionInfo -> + appVersionInfo = newAppVersionInfo + } + + synchronized(this@AppVersionInfoCache) { + val initialVersionInfo = daemon.getVersionInfo() + + if (appVersionInfo == null) { + appVersionInfo = initialVersionInfo + } + } + } + + private fun tearDown() = GlobalScope.launch(Dispatchers.Default) { + daemon.await().onAppVersionInfoChange = null } private fun updateUpgradeVersion() { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoFetcher.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoFetcher.kt deleted file mode 100644 index f067660ee5..0000000000 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoFetcher.kt +++ /dev/null @@ -1,69 +0,0 @@ -package net.mullvad.mullvadvpn.dataproxy - -import android.content.Context -import java.util.Calendar -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.MullvadDaemon - -val ONE_DAY_IN_MILLISECONDS = 24L * 60L * 60L * 1000L -val ONE_MINUTE_IN_MILLISECONDS = 60L * 1000L - -class AppVersionInfoFetcher(val daemon: Deferred<MullvadDaemon>, val context: Context) { - private val preferences = - context.getSharedPreferences(AppVersionInfoCache.SHARED_PREFERENCES, Context.MODE_PRIVATE) - - private val mainLoop = run() - - fun stop() { - mainLoop.cancel() - } - - private fun run() = GlobalScope.launch(Dispatchers.Default) { - while (true) { - delay(calculateDelay()) - fetch() - } - } - - private fun calculateDelay(): Long { - val now = Calendar.getInstance().timeInMillis - val lastUpdated = preferences.getLong(AppVersionInfoCache.KEY_LAST_UPDATED, 0) - val delta = now - lastUpdated - - if (delta < 0 || delta >= ONE_DAY_IN_MILLISECONDS) { - return 0 - } else { - return ONE_DAY_IN_MILLISECONDS - delta - } - } - - private suspend fun fetch() { - var now = Calendar.getInstance().timeInMillis - var versionInfo = daemon.await().getVersionInfo() - var attempt = 0 - - while (attempt < 5 && versionInfo == null) { - delay(ONE_MINUTE_IN_MILLISECONDS) - now = Calendar.getInstance().timeInMillis - versionInfo = daemon.await().getVersionInfo() - attempt += 1 - } - - if (versionInfo != null) { - preferences.edit().apply { - with(AppVersionInfoCache) { - putLong(KEY_LAST_UPDATED, now) - putBoolean(KEY_CURRENT_IS_SUPPORTED, versionInfo.currentIsSupported) - putBoolean(KEY_CURRENT_IS_OUTDATED, versionInfo.currentIsOutdated) - putString(KEY_LATEST_STABLE, versionInfo.latestStable) - putString(KEY_LATEST, versionInfo.latest) - commit() - } - } - } - } -} diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 6105f75c56..5fcc9171e0 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -368,6 +368,9 @@ const daemonEventSchema = oneOf( object({ wireguard_key: keygenEventSchema, }), + object({ + app_version_info: appVersionInfoSchema, + }), ); export class ResponseParseError extends Error { diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 9397d45467..0672effcca 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -48,8 +48,6 @@ import ReconnectionBackoff from './reconnection-backoff'; import TrayIconController, { TrayIconType } from './tray-icon-controller'; import WindowController from './window-controller'; -const VERSION_UPDATE_INTERVAL = 24 * 60 * 60 * 1000; - const DAEMON_RPC_PATH = process.platform === 'win32' ? '//./pipe/Mullvad VPN' : '/var/run/mullvad-vpn'; @@ -141,7 +139,6 @@ class ApplicationMain { latest: '', nextUpgrade: undefined, }; - private latestVersionInterval?: NodeJS.Timeout; // The UI locale which is set once from onReady handler private locale = 'en'; @@ -446,9 +443,6 @@ class ApplicationMain { // fetch the latest version info in background this.fetchLatestVersion(); - // start periodic updates - this.startLatestVersionPeriodicUpdates(); - // notify user about inconsistent version if ( process.env.NODE_ENV !== 'development' && @@ -478,9 +472,6 @@ class ApplicationMain { if (wasConnected) { this.connectedToDaemon = false; - // stop periodic updates - this.stopLatestVersionPeriodicUpdates(); - // update the tray icon to indicate that the computer is not secure anymore this.updateTrayIcon({ state: 'disconnected' }, false); @@ -540,6 +531,8 @@ class ApplicationMain { ); } else if ('wireguardKey' in daemonEvent) { this.handleWireguardKeygenEvent(daemonEvent.wireguardKey); + } else if ('appVersionInfo' in daemonEvent) { + this.setLatestVersion(daemonEvent.appVersionInfo); } }, (error: Error) => { @@ -795,21 +788,6 @@ class ApplicationMain { } } - private startLatestVersionPeriodicUpdates() { - const handler = () => { - this.fetchLatestVersion(); - }; - this.latestVersionInterval = global.setInterval(handler, VERSION_UPDATE_INTERVAL); - } - - private stopLatestVersionPeriodicUpdates() { - if (this.latestVersionInterval) { - clearInterval(this.latestVersionInterval); - - this.latestVersionInterval = undefined; - } - } - private shouldSuppressNotifications(isCriticalNotification: boolean): boolean { const isVisible = this.windowController ? this.windowController.isVisible() : false; diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 346b766e1b..7ce5e93111 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -86,7 +86,8 @@ export type DaemonEvent = | { tunnelState: TunnelState } | { settings: ISettings } | { relayList: IRelayList } - | { wireguardKey: KeygenEvent }; + | { wireguardKey: KeygenEvent } + | { appVersionInfo: IAppVersionInfo }; export interface ITunnelStateRelayInfo { endpoint: ITunnelEndpoint; diff --git a/mullvad-cli/src/cmds/status.rs b/mullvad-cli/src/cmds/status.rs index f3750962f6..bd68ae64ae 100644 --- a/mullvad-cli/src/cmds/status.rs +++ b/mullvad-cli/src/cmds/status.rs @@ -70,6 +70,11 @@ impl Command for Status { println!("New relay list: {:#?}", relay_list); } } + DaemonEvent::AppVersionInfo(app_version_info) => { + if verbose { + println!("New app version info: {:#?}", app_version_info); + } + } DaemonEvent::WireguardKey(key_event) => { if verbose { println!("{}", key_event); diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 783239f5ea..b4ca11d32e 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -20,6 +20,7 @@ mod relays; mod rpc_uniqueness_check; mod settings; pub mod version; +mod version_check; pub use crate::management_interface::ManagementCommand; use crate::management_interface::{ @@ -31,7 +32,7 @@ use futures::{ Future, Sink, }; use log::{debug, error, info, warn}; -use mullvad_rpc::{AccountsProxy, AppVersionProxy, HttpHandle, WireguardKeyProxy}; +use mullvad_rpc::{AccountsProxy, HttpHandle, WireguardKeyProxy}; use mullvad_types::{ account::{AccountData, AccountToken}, endpoint::MullvadEndpoint, @@ -155,6 +156,8 @@ pub(crate) enum InternalDaemonEvent { AccountToken, oneshot::Sender<std::result::Result<String, mullvad_rpc::Error>>, ), + /// The background job fetching new `AppVersionInfo`s got a new info object. + NewAppVersionInfo(AppVersionInfo), } impl From<TunnelStateTransition> for InternalDaemonEvent { @@ -235,6 +238,10 @@ pub trait EventListener { /// Notify that the relay list changed. fn notify_relay_list(&self, relay_list: RelayList); + /// Notify that info about the latest available app version changed. + /// Or some flag about the currently running version is changed. + fn notify_app_version(&self, app_version_info: AppVersionInfo); + /// Notify clients of a key generation event. fn notify_key_event(&self, key_event: KeygenEvent); } @@ -252,14 +259,13 @@ pub struct Daemon<L: EventListener = ManagementInterfaceEventBroadcaster> { account_history: account_history::AccountHistory, wg_key_proxy: WireguardKeyProxy<HttpHandle>, accounts_proxy: AccountsProxy<HttpHandle>, - version_proxy: AppVersionProxy<HttpHandle>, https_handle: mullvad_rpc::rest::RequestSender, wireguard_key_manager: wireguard::KeyManager, tokio_remote: tokio_core::reactor::Remote, relay_selector: relays::RelaySelector, last_generated_relay: Option<Relay>, last_generated_bridge_relay: Option<Relay>, - version: String, + app_version_info: AppVersionInfo, shutdown_callbacks: Vec<Box<dyn FnOnce()>>, } @@ -268,7 +274,6 @@ impl Daemon<ManagementInterfaceEventBroadcaster> { log_dir: Option<PathBuf>, resource_dir: PathBuf, cache_dir: PathBuf, - version: String, ) -> Result<Self> { if rpc_uniqueness_check::is_another_instance_running() { return Err(Error::DaemonIsAlreadyRunning); @@ -284,7 +289,6 @@ impl Daemon<ManagementInterfaceEventBroadcaster> { log_dir, resource_dir, cache_dir, - version, ) } @@ -332,7 +336,6 @@ where log_dir: Option<PathBuf>, resource_dir: PathBuf, cache_dir: PathBuf, - version: String, ) -> Result<Self> { let (tx, rx) = mpsc::channel(); @@ -344,7 +347,6 @@ where log_dir, resource_dir, cache_dir, - version, ) } @@ -356,7 +358,6 @@ where log_dir: Option<PathBuf>, resource_dir: PathBuf, cache_dir: PathBuf, - version: String, ) -> Result<Self> { let ca_path = resource_dir.join(mullvad_paths::resources::API_CA_FILENAME); @@ -379,8 +380,6 @@ where let on_relay_list_update = move |relay_list: &RelayList| { relay_list_listener.notify_relay_list(relay_list.clone()); }; - - let relay_selector = relays::RelaySelector::new( rpc_handle.clone(), on_relay_list_update, @@ -388,6 +387,36 @@ where &cache_dir, ); + let version_check_internal_event_tx = internal_event_tx.clone(); + let on_version_check_update = move |app_version_info: &AppVersionInfo| { + let _ = version_check_internal_event_tx.send(InternalDaemonEvent::NewAppVersionInfo( + app_version_info.clone(), + )); + }; + let app_version_info = match version_check::load_cache(&cache_dir) { + Ok(app_version_info) => app_version_info, + Err(error) => { + log::warn!( + "{}", + error.display_chain_with_msg("Unable to load cached version info") + ); + // If we don't have a cache, start out with sane defaults. + AppVersionInfo { + current_is_supported: true, + current_is_outdated: false, + latest_stable: version::PRODUCT_VERSION.to_owned(), + latest: version::PRODUCT_VERSION.to_owned(), + } + } + }; + let version_check_future = version_check::VersionUpdater::new( + rpc_handle.clone(), + cache_dir.clone(), + on_version_check_update, + app_version_info.clone(), + ); + tokio_remote.spawn(|_| version_check_future); + let settings = settings::load(); let account_history = @@ -431,14 +460,13 @@ where account_history, wg_key_proxy: WireguardKeyProxy::new(rpc_handle.clone()), accounts_proxy: AccountsProxy::new(rpc_handle.clone()), - version_proxy: AppVersionProxy::new(rpc_handle), https_handle, + wireguard_key_manager, tokio_remote, relay_selector, last_generated_relay: None, last_generated_bridge_relay: None, - version, - wireguard_key_manager, + app_version_info, shutdown_callbacks: vec![], }; @@ -505,6 +533,9 @@ where TriggerShutdown => self.trigger_shutdown_event(), WgKeyEvent(key_event) => self.handle_wireguard_key_event(key_event), NewAccountEvent(account_token, tx) => self.handle_new_account_event(account_token, tx), + NewAppVersionInfo(app_version_info) => { + self.handle_new_app_version_info(app_version_info) + } } Ok(()) } @@ -862,6 +893,11 @@ where }; } + fn handle_new_app_version_info(&mut self, app_version_info: AppVersionInfo) { + self.app_version_info = app_version_info.clone(); + self.event_listener.notify_app_version(app_version_info); + } + fn on_set_target_state( &mut self, tx: oneshot::Sender<std::result::Result<(), ()>>, @@ -1056,27 +1092,20 @@ where } } - fn on_get_version_info( - &mut self, - tx: oneshot::Sender<BoxFuture<AppVersionInfo, mullvad_rpc::Error>>, - ) { - #[cfg(target_os = "linux")] - const PLATFORM: &str = "linux"; - #[cfg(target_os = "macos")] - const PLATFORM: &str = "macos"; - #[cfg(target_os = "windows")] - const PLATFORM: &str = "windows"; - #[cfg(target_os = "android")] - const PLATFORM: &str = "android"; - - let fut = self - .version_proxy - .app_version_check(&self.version, PLATFORM); - Self::oneshot_send(tx, Box::new(fut), "get_version_info response"); + fn on_get_version_info(&mut self, tx: oneshot::Sender<AppVersionInfo>) { + Self::oneshot_send( + tx, + self.app_version_info.clone(), + "get_version_info response", + ); } fn on_get_current_version(&mut self, tx: oneshot::Sender<AppVersion>) { - Self::oneshot_send(tx, self.version.clone(), "get_current_version response"); + Self::oneshot_send( + tx, + version::PRODUCT_VERSION.to_owned(), + "get_current_version response", + ); } #[cfg(not(target_os = "android"))] diff --git a/mullvad-daemon/src/main.rs b/mullvad-daemon/src/main.rs index 9d4ca0fb56..26e463ef70 100644 --- a/mullvad-daemon/src/main.rs +++ b/mullvad-daemon/src/main.rs @@ -110,13 +110,8 @@ fn create_daemon(log_dir: Option<PathBuf>) -> Result<Daemon, String> { let cache_dir = mullvad_paths::cache_dir() .map_err(|e| e.display_chain_with_msg("Unable to get cache dir"))?; - Daemon::start( - log_dir, - resource_dir, - cache_dir, - version::PRODUCT_VERSION.to_owned(), - ) - .map_err(|e| e.display_chain_with_msg("Unable to initialize daemon")) + Daemon::start(log_dir, resource_dir, cache_dir) + .map_err(|e| e.display_chain_with_msg("Unable to initialize daemon")) } #[cfg(unix)] diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index aa0ae1bb37..afa6ad165e 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -233,7 +233,7 @@ pub enum ManagementCommand { /// Verify if the currently set wireguard key is valid. VerifyWireguardKey(OneshotSender<bool>), /// Get information about the currently running and latest app versions - GetVersionInfo(OneshotSender<BoxFuture<version::AppVersionInfo, mullvad_rpc::Error>>), + GetVersionInfo(OneshotSender<version::AppVersionInfo>), /// Get current version of the app GetCurrentVersion(OneshotSender<version::AppVersion>), #[cfg(not(target_os = "android"))] @@ -315,6 +315,11 @@ impl EventListener for ManagementInterfaceEventBroadcaster { self.notify(DaemonEvent::RelayList(relay_list)); } + fn notify_app_version(&self, app_version_info: version::AppVersionInfo) { + log::debug!("Broadcasting new app version info"); + self.notify(DaemonEvent::AppVersionInfo(app_version_info)); + } + fn notify_key_event(&self, key_event: mullvad_types::wireguard::KeygenEvent) { log::debug!("Broadcasting new wireguard key event"); self.notify(DaemonEvent::WireguardKey(key_event)); @@ -713,16 +718,7 @@ impl<T: From<ManagementCommand> + 'static + Send> ManagementInterfaceApi let (tx, rx) = sync::oneshot::channel(); let future = self .send_command_to_daemon(ManagementCommand::GetVersionInfo(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|version_future| { - version_future.map_err(|error| { - log::error!( - "Unable to get version data from API: {}", - error.display_chain() - ); - Self::map_rpc_error(&error) - }) - }); + .and_then(|_| rx.map_err(|_| Error::internal_error())); Box::new(future) } diff --git a/mullvad-daemon/src/version_check.rs b/mullvad-daemon/src/version_check.rs new file mode 100644 index 0000000000..65530ae20f --- /dev/null +++ b/mullvad-daemon/src/version_check.rs @@ -0,0 +1,177 @@ +use futures::{Async, Future, Poll}; +use mullvad_rpc::{AppVersionProxy, HttpHandle}; +use mullvad_types::version::AppVersionInfo; +use std::{ + fs::File, + io, + path::{Path, PathBuf}, + time::{Duration, Instant}, +}; +use talpid_types::ErrorExt; +use tokio_timer::{TimeoutError, Timer}; + +const VERSION_INFO_FILENAME: &str = "version-info.json"; + +const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(15); +/// How often the updater should wake up to check the in-memory cache. +/// This exist to prevent problems around sleeping. If you set it to sleep +/// for `UPDATE_INTERVAL` directly and the computer is suspended, that clock +/// won't tick, and the next update will be after 24 hours of the computer being *on*. +const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 5); +/// Wait this long until next check after a successful check +const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); +/// Wait this long until next try if an update failed +const UPDATE_INTERVAL_ERROR: Duration = Duration::from_secs(60 * 60 * 6); + +#[cfg(target_os = "linux")] +const PLATFORM: &str = "linux"; +#[cfg(target_os = "macos")] +const PLATFORM: &str = "macos"; +#[cfg(target_os = "windows")] +const PLATFORM: &str = "windows"; +#[cfg(target_os = "android")] +const PLATFORM: &str = "android"; + + +#[derive(err_derive::Error, Debug)] +pub enum Error { + #[error(display = "Failed to open app version cache file for reading")] + ReadCachedRelays(#[error(cause)] io::Error), + + #[error(display = "Failed to open app version cache file for writing")] + WriteRelayCache(#[error(cause)] io::Error), + + #[error(display = "Failure in serialization of the version info")] + Serialize(#[error(cause)] serde_json::Error), + + #[error(display = "Timed out when trying to check the latest app version")] + DownloadTimeout, + + #[error(display = "Failed to check the latest app version")] + Download(#[error(cause)] mullvad_rpc::Error), +} + +impl<F> From<TimeoutError<F>> for Error { + fn from(_: TimeoutError<F>) -> Error { + Error::DownloadTimeout + } +} + + +pub struct VersionUpdater<F: Fn(&AppVersionInfo) + Send + 'static> { + version_proxy: AppVersionProxy<HttpHandle>, + cache_path: PathBuf, + on_version_update: F, + last_app_version_info: AppVersionInfo, + next_update_time: Instant, + state: VersionUpdaterState, +} + +enum VersionUpdaterState { + Sleeping(tokio_timer::Sleep), + Updating(Box<dyn Future<Item = AppVersionInfo, Error = Error> + Send + 'static>), +} + +impl<F: Fn(&AppVersionInfo) + Send + 'static> VersionUpdater<F> { + pub fn new( + rpc_handle: HttpHandle, + cache_dir: PathBuf, + on_version_update: F, + last_app_version_info: AppVersionInfo, + ) -> Self { + let version_proxy = AppVersionProxy::new(rpc_handle); + let cache_path = cache_dir.join(VERSION_INFO_FILENAME); + Self { + version_proxy, + cache_path, + on_version_update, + last_app_version_info, + next_update_time: Instant::now(), + state: VersionUpdaterState::Sleeping(Self::create_sleep_future()), + } + } + + fn create_sleep_future() -> tokio_timer::Sleep { + Timer::default().sleep(UPDATE_CHECK_INTERVAL) + } + + fn create_update_future( + &mut self, + ) -> Box<dyn Future<Item = AppVersionInfo, Error = Error> + Send + 'static> { + let download_future = self + .version_proxy + .app_version_check(&crate::version::PRODUCT_VERSION.to_owned(), PLATFORM) + .map_err(Error::Download); + let future = Timer::default().timeout(download_future, DOWNLOAD_TIMEOUT); + Box::new(future) + } + + fn write_cache(&self) -> Result<(), Error> { + log::debug!( + "Writing version check cache to {}", + self.cache_path.display() + ); + let file = File::create(&self.cache_path).map_err(Error::WriteRelayCache)?; + serde_json::to_writer_pretty(io::BufWriter::new(file), &self.last_app_version_info) + .map_err(Error::Serialize) + } +} + +impl<F: Fn(&AppVersionInfo) + Send + 'static> Future for VersionUpdater<F> { + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll<Self::Item, Self::Error> { + loop { + let next_state = match &mut self.state { + VersionUpdaterState::Sleeping(timer) => match timer.poll() { + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(e) => { + log::error!("Version check sleep error: {}", e); + return Err(()); + } + Ok(Async::Ready(())) => { + if Instant::now() > self.next_update_time { + VersionUpdaterState::Updating(self.create_update_future()) + } else { + VersionUpdaterState::Sleeping(Self::create_sleep_future()) + } + } + }, + VersionUpdaterState::Updating(future) => match future.poll() { + Ok(Async::NotReady) => return Ok(Async::NotReady), + Err(error) => { + log::error!("{}", error.display_chain_with_msg("Version check failed")); + self.next_update_time = Instant::now() + UPDATE_INTERVAL_ERROR; + VersionUpdaterState::Sleeping(Self::create_sleep_future()) + } + Ok(Async::Ready(app_version_info)) => { + if app_version_info != self.last_app_version_info { + self.next_update_time = Instant::now() + UPDATE_INTERVAL; + log::debug!("Got new version check: {:?}", app_version_info); + (self.on_version_update)(&app_version_info); + self.last_app_version_info = app_version_info; + if let Err(e) = self.write_cache() { + log::error!( + "{}", + e.display_chain_with_msg( + "Unable to cache version check response" + ) + ); + } + } + VersionUpdaterState::Sleeping(Self::create_sleep_future()) + } + }, + }; + self.state = next_state; + } + } +} + +pub fn load_cache(cache_dir: &Path) -> Result<AppVersionInfo, Error> { + let path = cache_dir.join(VERSION_INFO_FILENAME); + log::debug!("Loading version check cache from {}", path.display()); + let file = File::open(path).map_err(Error::ReadCachedRelays)?; + serde_json::from_reader(io::BufReader::new(file)).map_err(Error::Serialize) +} diff --git a/mullvad-jni/src/daemon_interface.rs b/mullvad-jni/src/daemon_interface.rs index 69d16d4034..2820315c74 100644 --- a/mullvad-jni/src/daemon_interface.rs +++ b/mullvad-jni/src/daemon_interface.rs @@ -133,10 +133,7 @@ impl DaemonInterface { self.send_command(ManagementCommand::GetVersionInfo(tx))?; - rx.wait() - .map_err(|_| Error::NoResponse)? - .wait() - .map_err(Error::RpcError) + rx.wait().map_err(|_| Error::NoResponse) } pub fn get_wireguard_key(&self) -> Result<Option<wireguard::PublicKey>> { diff --git a/mullvad-jni/src/jni_event_listener.rs b/mullvad-jni/src/jni_event_listener.rs index cb0e14fb40..737fa1b9b2 100644 --- a/mullvad-jni/src/jni_event_listener.rs +++ b/mullvad-jni/src/jni_event_listener.rs @@ -6,7 +6,8 @@ use jni::{ }; use mullvad_daemon::EventListener; use mullvad_types::{ - relay_list::RelayList, settings::Settings, states::TunnelState, wireguard::KeygenEvent, + relay_list::RelayList, settings::Settings, states::TunnelState, version::AppVersionInfo, + wireguard::KeygenEvent, }; use std::{sync::mpsc, thread}; use talpid_types::ErrorExt; @@ -28,6 +29,7 @@ enum Event { RelayList(RelayList), Settings(Settings), Tunnel(TunnelState), + AppVersionInfo(AppVersionInfo), } #[derive(Clone, Debug)] @@ -55,11 +57,16 @@ impl EventListener for JniEventListener { fn notify_relay_list(&self, relay_list: RelayList) { let _ = self.0.send(Event::RelayList(relay_list)); } + + fn notify_app_version(&self, app_version_info: AppVersionInfo) { + let _ = self.0.send(Event::AppVersionInfo(app_version_info)); + } } struct JniEventHandler<'env> { env: AttachGuard<'env>, mullvad_ipc_client: JObject<'env>, + notify_app_version_info_event: JMethodID<'env>, notify_keygen_event: JMethodID<'env>, notify_relay_list_event: JMethodID<'env>, notify_settings_event: JMethodID<'env>, @@ -104,6 +111,12 @@ impl<'env> JniEventHandler<'env> { events: mpsc::Receiver<Event>, ) -> Result<Self, Error> { let class = get_class("net/mullvad/mullvadvpn/MullvadDaemon"); + let notify_app_version_info_event = Self::get_method_id( + &env, + &class, + "notifyAppVersionInfoEvent", + "(Lnet/mullvad/mullvadvpn/model/AppVersionInfo;)V", + )?; let notify_keygen_event = Self::get_method_id( &env, &class, @@ -132,6 +145,7 @@ impl<'env> JniEventHandler<'env> { Ok(JniEventHandler { env, mullvad_ipc_client, + notify_app_version_info_event, notify_keygen_event, notify_relay_list_event, notify_settings_event, @@ -157,6 +171,9 @@ impl<'env> JniEventHandler<'env> { Event::RelayList(relay_list) => self.handle_relay_list_event(relay_list), Event::Settings(settings) => self.handle_settings(settings), Event::Tunnel(tunnel_event) => self.handle_tunnel_event(tunnel_event), + Event::AppVersionInfo(app_version_info) => { + self.handle_app_version_info_event(app_version_info) + } } } } @@ -232,4 +249,24 @@ impl<'env> JniEventHandler<'env> { ); } } + + fn handle_app_version_info_event(&self, app_version_info: AppVersionInfo) { + let java_app_version_info = self.env.auto_local(app_version_info.into_java(&self.env)); + + let result = self.env.call_method_unchecked( + self.mullvad_ipc_client, + self.notify_app_version_info_event, + JavaType::Primitive(Primitive::Void), + &[JValue::Object(java_app_version_info.as_obj())], + ); + + if let Err(error) = result { + log::error!( + "{}", + error.display_chain_with_msg( + "Failed to call MullvadDaemon.notifyAppVersionInfoEvent" + ) + ); + } + } } diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs index c1d85a5a6c..e4ed6c83a5 100644 --- a/mullvad-jni/src/lib.rs +++ b/mullvad-jni/src/lib.rs @@ -228,7 +228,6 @@ fn create_daemon( Some(log_dir), resource_dir, cache_dir, - version::PRODUCT_VERSION.to_owned(), ) .map_err(Error::InitializeDaemon)?; diff --git a/mullvad-types/src/lib.rs b/mullvad-types/src/lib.rs index 448befd16f..3456c5579b 100644 --- a/mullvad-types/src/lib.rs +++ b/mullvad-types/src/lib.rs @@ -35,6 +35,9 @@ pub enum DaemonEvent { /// The daemon got an updated relay list. RelayList(relay_list::RelayList), + /// The daemon got update version info. + AppVersionInfo(version::AppVersionInfo), + /// Key event WireguardKey(wireguard::KeygenEvent), } diff --git a/mullvad-types/src/version.rs b/mullvad-types/src/version.rs index 695c686fc4..6e626104d7 100644 --- a/mullvad-types/src/version.rs +++ b/mullvad-types/src/version.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; /// AppVersionInfo represents the current stable and the current latest release versions of the /// Mullvad VPN app. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct AppVersionInfo { /// False if Mullvad has stopped supporting the currently running version. This could mean /// a number of things. For example: |
