diff options
| author | Emīls <emils@mullvad.net> | 2020-06-25 15:22:23 +0100 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2020-06-25 15:22:23 +0100 |
| commit | c586258b52f0d14aedc333d877484da4f3805e05 (patch) | |
| tree | 9492cde4c155fb83cef7d63627e8647af1194015 | |
| parent | 010d7a9771a980491959bbddf97214928c9e4e93 (diff) | |
| parent | c147d3b36bcd1a4be37da0aaef9d49f5ce40bcee (diff) | |
| download | mullvadvpn-c586258b52f0d14aedc333d877484da4f3805e05.tar.xz mullvadvpn-c586258b52f0d14aedc333d877484da4f3805e05.zip | |
Merge branch 'version-check-supress-notification'
51 files changed, 823 insertions, 352 deletions
diff --git a/.travis.yml b/.travis.yml index 63b40a26f2..24365c718d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,7 @@ matrix: - sudo /opt/android/android-ndk-r20/build/tools/make-standalone-toolchain.sh --platform=android-21 --arch=arm64 --install-dir=/opt/android/toolchains/android21-aarch64 - sudo apt install tidy - | - curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && + curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.37.0/ktlint && chmod a+x ktlint && sudo mv ktlint /usr/local/bin/ - | diff --git a/CHANGELOG.md b/CHANGELOG.md index a037270df1..353de5818e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ Line wrap the file at 100 chars. Th #### Android - Show a system notification when the account time will soon run out. +### Changed +- Change version string parsing to never suggest the user to upgrade to an older version. + ### Fixed #### Windows - Fix window flickering by disabling window animations. diff --git a/Cargo.lock b/Cargo.lock index bb0f1c8344..9364d2d038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1390,6 +1390,7 @@ dependencies = [ "err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "fern 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "ipnetwork 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-client-core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", @@ -1414,6 +1415,7 @@ dependencies = [ "talpid-core 0.1.0", "talpid-ipc 0.1.0", "talpid-types 0.1.0", + "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-retry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1520,6 +1522,7 @@ dependencies = [ "ipnetwork 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "mullvad-types 0.1.0", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "talpid-types 0.1.0", @@ -2117,6 +2120,27 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "quickcheck" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quickcheck_macros" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "quote" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2819,6 +2843,8 @@ dependencies = [ "pfctl 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "pnet_packet 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "prost 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "quickcheck 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "quickcheck_macros 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4048,6 +4074,8 @@ dependencies = [ "checksum prost-derive 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" "checksum prost-types 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1834f67c0697c001304b75be76f67add9c89742eda3a085ad8ee0bb38c3417aa" "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" +"checksum quickcheck 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" +"checksum quickcheck_macros 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" "checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 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 4b578f9a9b..4b73f9a878 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt @@ -27,38 +27,14 @@ class AppVersionInfoCache( } } - val latestStable - get() = appVersionInfo?.latestStable - val latest - get() = appVersionInfo?.latest val isSupported get() = appVersionInfo?.supported ?: true - val isOutdated: Boolean - get() { - if (showBetaReleases) { - return version != null && latest != null && latest != version - } else { - return version != null && latestStable != null && latestStable != version - } - } + val isOutdated + get() = appVersionInfo?.suggestedUpgrade != null - val upgradeVersion: String? - get() { - if (showBetaReleases) { - if (version == latest) { - return null - } else { - return latest - } - } else { - if (version == latestStable) { - return null - } else { - return latestStable - } - } - } + val upgradeVersion + get() = appVersionInfo?.suggestedUpgrade var onUpdate: (() -> Unit)? = null set(value) { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt index 97e6559bd4..4bbc29c8c3 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt @@ -63,11 +63,11 @@ class MullvadProblemReport { if (currentJob == null || currentJob.isCompleted) { currentJob = GlobalScope.async(Dispatchers.Default) { val result = (collectJob?.await() ?: false) && - sendProblemReport( - userEmail, - userMessage, - problemReportPath.await().absolutePath - ) + sendProblemReport( + userEmail, + userMessage, + problemReportPath.await().absolutePath + ) if (result) { deleteReportFile() diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt index a28c06a505..5f985189d5 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt @@ -2,7 +2,5 @@ package net.mullvad.mullvadvpn.model data class AppVersionInfo( val supported: Boolean, - val latest: String, - val latestStable: String, - val latestBeta: String + val suggestedUpgrade: String? ) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ConnectionProxy.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ConnectionProxy.kt index 17099078d8..3f1efab066 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ConnectionProxy.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ConnectionProxy.kt @@ -116,7 +116,8 @@ class ConnectionProxy(val context: Context, val daemon: MullvadDaemon) { val currentState = uiState if (currentState is TunnelState.Disconnecting && - currentState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect) { + currentState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect + ) { return false } else { scheduleToResetAnticipatedState() diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt index 5312af91f9..9d76a0be52 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt @@ -35,10 +35,10 @@ class ForegroundNotificationManager( } private var tunnelStateEvents - by autoSubscribable<TunnelState>(this, TunnelState.Disconnected()) { newState -> - tunnelStateNotification.tunnelState = newState - updateNotification() - } + by autoSubscribable<TunnelState>(this, TunnelState.Disconnected()) { newState -> + tunnelStateNotification.tunnelState = newState + updateNotification() + } private var deviceIsUnlocked by observable(!keyguardManager.isDeviceLocked) { _, _, _ -> updateNotificationAction() @@ -63,10 +63,13 @@ class ForegroundNotificationManager( } service.apply { - registerReceiver(deviceLockListener, IntentFilter().apply { - addAction(Intent.ACTION_USER_PRESENT) - addAction(Intent.ACTION_SCREEN_OFF) - }) + registerReceiver( + deviceLockListener, + IntentFilter().apply { + addAction(Intent.ACTION_USER_PRESENT) + addAction(Intent.ACTION_SCREEN_OFF) + } + ) } updateNotification() diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/KeyStatusListener.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/KeyStatusListener.kt index 595c97220e..8d44be4d36 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/KeyStatusListener.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/KeyStatusListener.kt @@ -26,9 +26,11 @@ class KeyStatusListener(val daemon: MullvadDaemon) { val newStatus = daemon.generateWireguardKey() val newFailure = newStatus?.failure() if (oldStatus is KeygenEvent.NewKey && newFailure != null) { - keyStatus = KeygenEvent.NewKey(oldStatus.publicKey, - oldStatus.verified, - newFailure) + keyStatus = KeygenEvent.NewKey( + oldStatus.publicKey, + oldStatus.verified, + newFailure + ) } else { keyStatus = newStatus ?: KeygenEvent.GenerationFailure() } @@ -39,9 +41,11 @@ class KeyStatusListener(val daemon: MullvadDaemon) { // Only update verification status if the key is actually there when (val state = keyStatus) { is KeygenEvent.NewKey -> { - keyStatus = KeygenEvent.NewKey(state.publicKey, - verified, - state.replacementFailure) + keyStatus = KeygenEvent.NewKey( + state.publicKey, + verified, + state.replacementFailure + ) } } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt index 795f5596df..14e62091cd 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -69,9 +69,9 @@ class MullvadVpnService : TalpidVpnService() { } private var accountExpiryNotification - by observable<AccountExpiryNotification?>(null) { _, oldNotification, _ -> - oldNotification?.accountExpiry = null - } + by observable<AccountExpiryNotification?>(null) { _, oldNotification, _ -> + oldNotification?.accountExpiry = null + } private lateinit var keyguardManager: KeyguardManager private lateinit var notificationManager: ForegroundNotificationManager diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt index e91908f088..7f95cf5b39 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt @@ -59,8 +59,10 @@ class TunnelStateNotification(val context: Context) { var tunnelState by observable<TunnelState>(TunnelState.Disconnected()) { _, _, newState -> reconnecting = - (newState is TunnelState.Disconnecting && - newState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect) || + ( + newState is TunnelState.Disconnecting && + newState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect + ) || (newState is TunnelState.Connecting && reconnecting) update() diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/tunnelstate/Persistence.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/tunnelstate/Persistence.kt index f1366abc66..73b5c6de7e 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/tunnelstate/Persistence.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/tunnelstate/Persistence.kt @@ -12,10 +12,12 @@ private const val SHARED_PREFERENCES = "tunnel_state" private const val KEY_TUNNEL_STATE = "tunnel_state" // TODO: Maybe replace using this with actually persisting the endpoint information -private val dummyTunnelEndpoint = TunnelEndpoint(Endpoint( - InetSocketAddress.createUnresolved("dummy", 53), - TransportProtocol.Tcp -)) +private val dummyTunnelEndpoint = TunnelEndpoint( + Endpoint( + InetSocketAddress.createUnresolved("dummy", 53), + TransportProtocol.Tcp + ) +) internal class Persistence(context: Context) { val sharedPreferences = diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt index 9b46b50610..d67a878b25 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt @@ -81,13 +81,15 @@ class AccountInput( input.apply { addTextChangedListener(InputWatcher()) - setOnTouchListener(OnTouchListener { - _, event -> - if (MotionEvent.ACTION_UP == event.getAction()) { - shouldShowAccountHistory = true + setOnTouchListener( + OnTouchListener { + _, event -> + if (MotionEvent.ACTION_UP == event.getAction()) { + shouldShowAccountHistory = true + } + false } - false - }) + ) } container.apply { @@ -155,10 +157,14 @@ class AccountInput( private fun updateAccountHistory() { accountHistory?.let { history -> accountHistoryList.apply { - setAdapter(ArrayAdapter(context, - R.layout.account_history_entry, - R.id.account_history_entry_text_view, - history)) + setAdapter( + ArrayAdapter( + context, + R.layout.account_history_entry, + R.id.account_history_entry_text_view, + history + ) + ) setOnItemClickListener { _, _, idx, _ -> val accountNumber = history[idx] diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt index 0aba6ce947..1b8c0d7c73 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt @@ -167,10 +167,13 @@ class CellSwitch : LinearLayout { init { setBackground(resources.getDrawable(R.drawable.cell_switch_background, null)) - addView(knobView, LinearLayout.LayoutParams(knobSize, knobSize).apply { - gravity = Gravity.CENTER_VERTICAL - leftMargin = resources.getDimensionPixelSize(R.dimen.cell_switch_knob_margin) - }) + addView( + knobView, + LinearLayout.LayoutParams(knobSize, knobSize).apply { + gravity = Gravity.CENTER_VERTICAL + leftMargin = resources.getDimensionPixelSize(R.dimen.cell_switch_knob_margin) + } + ) } override fun onTouchEvent(event: MotionEvent): Boolean { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt index ab0b01c92e..ec5a4549b1 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt @@ -156,9 +156,9 @@ class CollapsibleTitleController(val parentView: View) { private fun update() { val shouldUpdate = scrollOffsetUpdated || - scaleInterpolation.updated || - xOffsetInterpolation.updated || - yOffsetInterpolation.updated + scaleInterpolation.updated || + xOffsetInterpolation.updated || + yOffsetInterpolation.updated if (shouldUpdate) { val progress = maxOf(0.0f, minOf(1.0f, scrollInterpolation.progress(scrollOffset))) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/NotificationBanner.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/NotificationBanner.kt index 82a45cb347..1aeed5e5ca 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/NotificationBanner.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/NotificationBanner.kt @@ -132,11 +132,11 @@ class NotificationBanner( null -> return false is KeygenEvent.NewKey -> return false is KeygenEvent.TooManyKeys -> { - externalLink = ExternalLink.KeyManagement - showError(R.string.wireguard_error, R.string.too_many_keys) + externalLink = ExternalLink.KeyManagement + showError(R.string.wireguard_error, R.string.too_many_keys) } is KeygenEvent.GenerationFailure -> { - showError(R.string.wireguard_error, R.string.failed_to_generate_key) + showError(R.string.wireguard_error, R.string.failed_to_generate_key) } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt index 0dbed7eb19..9c2573664f 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt @@ -81,8 +81,8 @@ open class Button : FrameLayout { constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) : super(context, attributes, defaultStyleAttribute) { - loadAttributes(attributes) - } + loadAttributes(attributes) + } constructor( context: Context, diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/CopyableInformationView.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/CopyableInformationView.kt index ac1b7e8125..33530283cb 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/CopyableInformationView.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/CopyableInformationView.kt @@ -24,8 +24,8 @@ class CopyableInformationView : InformationView { constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) : super(context, attributes, defaultStyleAttribute) { - loadAttributes(attributes) - } + loadAttributes(attributes) + } constructor( context: Context, diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/InformationView.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/InformationView.kt index 8395d6ad27..d4376cfd62 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/InformationView.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/InformationView.kt @@ -99,8 +99,8 @@ open class InformationView : LinearLayout { constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) : super(context, attributes, defaultStyleAttribute) { - loadAttributes(attributes) - } + loadAttributes(attributes) + } constructor( context: Context, diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt index 95fdeebc63..303527c3d4 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt @@ -13,7 +13,7 @@ class ListenableScrollView : ScrollView { constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) : super(context, attributes, defaultStyleAttribute) { - } + } constructor( context: Context, diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt index c2a7839748..8bcd6c3648 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt @@ -27,8 +27,8 @@ class UrlButton : Button { constructor(context: Context, attributes: AttributeSet, defaultStyleAttribute: Int) : super(context, attributes, defaultStyleAttribute) { - loadAttributes(attributes) - } + loadAttributes(attributes) + } constructor( context: Context, diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt index 6e279452da..0b950d9a55 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt @@ -21,13 +21,16 @@ class JobTracker { jobs.put(jobId, job) - reaperJobs.put(jobId, GlobalScope.launch(Dispatchers.Default) { - job.join() + reaperJobs.put( + jobId, + GlobalScope.launch(Dispatchers.Default) { + job.join() - synchronized(jobs) { - jobs.remove(jobId) + synchronized(jobs) { + jobs.remove(jobId) + } } - }) + ) return jobId } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt index 09fca7908b..61050bc894 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt @@ -12,9 +12,11 @@ class SmartDeferred<T>(private val deferred: Deferred<T>) { fun awaitThen(action: T.() -> Unit): Long? { if (active) { - return jobTracker.newJob(GlobalScope.launch(Dispatchers.Default) { - deferred.await().action() - }) + return jobTracker.newJob( + GlobalScope.launch(Dispatchers.Default) { + deferred.await().action() + } + ) } else { return null } diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 7740dfa26c..656efd760b 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -294,9 +294,7 @@ const tunnelStateSchema = oneOf( const appVersionInfoSchema = partialObject({ supported: boolean, - latest: string, - latest_stable: string, - latest_beta: string, + suggested_upgrade: maybe(string), }); export class ConnectionObserver { diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 0bdc71a2ef..d6a01e6fd1 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -64,6 +64,9 @@ const DAEMON_RPC_PATH = const AUTO_CONNECT_FALLBACK_DELAY = 6000; +/// Mirrors the beta check regex in the daemon. Matches only well formed beta versions +const IS_BETA = /^(\d{4})\.(\d+)-beta(\d+)$/; + enum AppQuitStage { unready, initiated, @@ -77,11 +80,6 @@ export interface ICurrentAppVersionInfo { isBeta: boolean; } -export interface IAppUpgradeInfo extends IAppVersionInfo { - // Null is used since undefined properties get filtered out when sending through IPC. - nextUpgrade: string | null; -} - type AccountVerification = { status: 'verified' } | { status: 'deferred'; error: Error }; class ApplicationMain { @@ -154,12 +152,9 @@ class ApplicationMain { isBeta: false, }; - private upgradeVersion: IAppUpgradeInfo = { + private upgradeVersion: IAppVersionInfo = { supported: true, - latestStable: '', - latestBeta: '', - latest: '', - nextUpgrade: null, + suggestedUpgrade: undefined, }; // The UI locale which is set once from onReady handler @@ -773,7 +768,7 @@ class ApplicationMain { daemon: daemonVersion, gui: guiVersion, isConsistent: daemonVersion === guiVersion, - isBeta: guiVersion.includes('beta'), + isBeta: IS_BETA.test(guiVersion), }; this.currentVersion = versionInfo; @@ -785,45 +780,23 @@ class ApplicationMain { } private setLatestVersion(latestVersionInfo: IAppVersionInfo) { - const settings = this.settings; - - function nextUpgrade(current: string, latest: string, latestStable: string): string | null { - if (settings.showBetaReleases) { - return current === latest ? null : latest; - } else { - return current === latestStable ? null : latestStable; - } - } - - const currentVersionInfo = this.currentVersion; - const latestVersion = latestVersionInfo.latest; - const latestStableVersion = latestVersionInfo.latestStable; - - const upgradeVersion = nextUpgrade( - currentVersionInfo.daemon, - latestVersion, - latestStableVersion, - ); - - const upgradeInfo = { - ...latestVersionInfo, - nextUpgrade: upgradeVersion, - }; - - this.upgradeVersion = upgradeInfo; + this.upgradeVersion = latestVersionInfo; // notify user to update the app if it became unsupported const notificationProvider = new UnsupportedVersionNotificationProvider({ supported: latestVersionInfo.supported, - consistent: currentVersionInfo.isConsistent, - nextUpgrade: upgradeVersion, + consistent: this.currentVersion.isConsistent, + suggestedUpgrade: latestVersionInfo.suggestedUpgrade, }); if (notificationProvider.mayDisplay()) { this.notificationController.notify(notificationProvider.getSystemNotification()); } if (this.windowController) { - IpcMainEventChannel.upgradeVersion.notify(this.windowController.webContents, upgradeInfo); + IpcMainEventChannel.upgradeVersion.notify( + this.windowController.webContents, + latestVersionInfo, + ); } } diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index f794216206..0dc076378a 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -22,7 +22,7 @@ import configureStore from './redux/store'; import userInterfaceActions from './redux/userinterface/actions'; import versionActions from './redux/version/actions'; -import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main'; +import { ICurrentAppVersionInfo } from '../main'; import { loadTranslations, messages, relayLocations } from '../shared/gettext'; import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-channel'; @@ -34,6 +34,7 @@ import { BridgeSettings, BridgeState, IAccountData, + IAppVersionInfo, ILocation, IRelayList, ISettings, @@ -166,7 +167,7 @@ export default class AppRenderer { this.setCurrentVersion(currentVersion); }); - IpcRendererEventChannel.upgradeVersion.listen((upgradeVersion: IAppUpgradeInfo) => { + IpcRendererEventChannel.upgradeVersion.listen((upgradeVersion: IAppVersionInfo) => { this.setUpgradeVersion(upgradeVersion); }); @@ -730,7 +731,7 @@ export default class AppRenderer { ); } - private setUpgradeVersion(upgradeVersion: IAppUpgradeInfo) { + private setUpgradeVersion(upgradeVersion: IAppVersionInfo) { this.reduxActions.version.updateLatest(upgradeVersion); } diff --git a/gui/src/renderer/containers/SettingsPage.tsx b/gui/src/renderer/containers/SettingsPage.tsx index 563b9136fb..fda21f1b7d 100644 --- a/gui/src/renderer/containers/SettingsPage.tsx +++ b/gui/src/renderer/containers/SettingsPage.tsx @@ -15,9 +15,7 @@ const mapStateToProps = (state: IReduxState, props: IAppContext) => ({ expiryLocale: state.userInterface.locale, appVersion: state.version.current, consistentVersion: state.version.consistent, - upToDateVersion: state.settings.showBetaReleases - ? state.version.current === state.version.latest - : state.version.current === state.version.latestStable, + upToDateVersion: state.version.suggestedUpgrade ? false : true, isOffline: state.connection.isBlocked, }); const mapDispatchToProps = (dispatch: ReduxDispatch) => { diff --git a/gui/src/renderer/containers/SupportPage.tsx b/gui/src/renderer/containers/SupportPage.tsx index d8227dd4ae..7382b9d694 100644 --- a/gui/src/renderer/containers/SupportPage.tsx +++ b/gui/src/renderer/containers/SupportPage.tsx @@ -12,9 +12,7 @@ const mapStateToProps = (state: IReduxState) => ({ defaultMessage: state.support.message, accountHistory: state.account.accountHistory, isOffline: state.connection.isBlocked, - outdatedVersion: state.settings.showBetaReleases - ? state.version.current !== state.version.latest - : state.version.current !== state.version.latestStable, + outdatedVersion: state.version.suggestedUpgrade ? true : false, }); const mapDispatchToProps = (dispatch: ReduxDispatch) => { diff --git a/gui/src/renderer/redux/version/actions.ts b/gui/src/renderer/redux/version/actions.ts index e2bb4a6513..8b3f1461f4 100644 --- a/gui/src/renderer/redux/version/actions.ts +++ b/gui/src/renderer/redux/version/actions.ts @@ -1,12 +1,8 @@ import { IAppVersionInfo } from '../../../shared/daemon-rpc-types'; -interface IUpdateLatestActionPayload extends IAppVersionInfo { - nextUpgrade: string | null; -} - export interface IUpdateLatestAction { type: 'UPDATE_LATEST'; - latestInfo: IUpdateLatestActionPayload; + latestInfo: IAppVersionInfo; } export interface IUpdateVersionAction { @@ -18,7 +14,7 @@ export interface IUpdateVersionAction { export type VersionAction = IUpdateLatestAction | IUpdateVersionAction; -function updateLatest(latestInfo: IUpdateLatestActionPayload): IUpdateLatestAction { +function updateLatest(latestInfo: IAppVersionInfo): IUpdateLatestAction { return { type: 'UPDATE_LATEST', latestInfo, diff --git a/gui/src/renderer/redux/version/reducers.ts b/gui/src/renderer/redux/version/reducers.ts index fcc23c7f3d..96e8ab0aa3 100644 --- a/gui/src/renderer/redux/version/reducers.ts +++ b/gui/src/renderer/redux/version/reducers.ts @@ -4,9 +4,7 @@ export interface IVersionReduxState { current: string; supported: boolean; isBeta: boolean; - latest?: string; - latestStable?: string; - nextUpgrade: string | null; + suggestedUpgrade?: string; consistent: boolean; } @@ -14,9 +12,7 @@ const initialState: IVersionReduxState = { current: '', supported: true, isBeta: false, - latest: undefined, - latestStable: undefined, - nextUpgrade: null, + suggestedUpgrade: undefined, consistent: true, }; @@ -28,10 +24,8 @@ export default function ( case 'UPDATE_LATEST': return { ...state, - nextUpgrade: action.latestInfo.nextUpgrade, supported: action.latestInfo.supported, - latest: action.latestInfo.latest, - latestStable: action.latestInfo.latestStable, + suggestedUpgrade: action.latestInfo.suggestedUpgrade, }; case 'UPDATE_VERSION': diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 7d357a3b48..bec0275e9b 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -279,9 +279,7 @@ export interface IShadowsocksProxySettings { export interface IAppVersionInfo { supported: boolean; - latest: string; - latestStable: string; - latestBeta: string; + suggestedUpgrade?: string; } export interface ISettings { diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index c09b8d2a84..b020808e76 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -4,13 +4,14 @@ import * as uuid from 'uuid'; import { IGuiSettingsState } from './gui-settings-state'; -import { IAppUpgradeInfo, ICurrentAppVersionInfo } from '../main/index'; +import { ICurrentAppVersionInfo } from '../main/index'; import { IWindowShapeParameters } from '../main/window-controller'; import { AccountToken, BridgeSettings, BridgeState, IAccountData, + IAppVersionInfo, ILocation, IRelayList, ISettings, @@ -32,7 +33,7 @@ export interface IAppStateSnapshot { location?: ILocation; relayListPair: IRelayListPair; currentVersion: ICurrentAppVersionInfo; - upgradeVersion: IAppUpgradeInfo; + upgradeVersion: IAppVersionInfo; guiSettings: IGuiSettingsState; wireguardPublicKey?: IWireguardPublicKey; } @@ -267,7 +268,7 @@ export class IpcRendererEventChannel { listen: listen(CURRENT_VERSION_CHANGED), }; - public static upgradeVersion: IReceiver<IAppUpgradeInfo> = { + public static upgradeVersion: IReceiver<IAppVersionInfo> = { listen: listen(UPGRADE_VERSION_CHANGED), }; @@ -364,7 +365,7 @@ export class IpcMainEventChannel { notify: sender(CURRENT_VERSION_CHANGED), }; - public static upgradeVersion: ISender<IAppUpgradeInfo> = { + public static upgradeVersion: ISender<IAppVersionInfo> = { notify: sender(UPGRADE_VERSION_CHANGED), }; diff --git a/gui/src/shared/notifications/unsupported-version.ts b/gui/src/shared/notifications/unsupported-version.ts index ed471bc586..fd2e0b5c72 100644 --- a/gui/src/shared/notifications/unsupported-version.ts +++ b/gui/src/shared/notifications/unsupported-version.ts @@ -11,7 +11,7 @@ import { interface UnsupportedVersionNotificationContext { supported: boolean; consistent: boolean; - nextUpgrade: string | null; + suggestedUpgrade?: string; } export class UnsupportedVersionNotificationProvider @@ -19,21 +19,11 @@ export class UnsupportedVersionNotificationProvider public constructor(private context: UnsupportedVersionNotificationContext) {} public mayDisplay() { - return this.context.consistent && !this.context.supported && this.context.nextUpgrade !== null; + return this.context.consistent && !this.context.supported; } public getSystemNotification(): SystemNotification { - const message = sprintf( - // TRANSLATORS: The system notification displayed to the user when the running app becomes unsupported. - // TRANSLATORS: Available placeholder: - // TRANSLATORS: %(version) - the newest available version of the app - messages.pgettext( - 'notifications', - 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security', - ), - { version: this.context.nextUpgrade }, - ); - + const message = this.getMessage(); return { message, critical: true, @@ -44,16 +34,7 @@ export class UnsupportedVersionNotificationProvider } public getInAppNotification(): InAppNotification { - const subtitle = sprintf( - // TRANSLATORS: The in-app banner displayed to the user when the running app becomes unsupported. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(version)s - the newest available version of the app - messages.pgettext( - 'in-app-notifications', - 'You are running an unsupported app version. Please upgrade to %(version)s now to ensure your security', - ), - { version: this.context.nextUpgrade }, - ); + const subtitle = this.getMessage(); return { indicator: 'error', @@ -62,4 +43,23 @@ export class UnsupportedVersionNotificationProvider action: { type: 'open-url', url: links.download }, }; } + + private getMessage(): string { + // TRANSLATORS: The in-app banner and system notification which are displayed to the user when the running app becomes unsupported. + let message = messages.pgettext('notifications', 'You are running an unsupported app version.'); + if (this.context.suggestedUpgrade) { + message += ' '; + message += sprintf( + // TRANSLATORS: Appendix to the system notification and in-app banner about the app becoming unsupported with the suggested supported version. + // TRANSLATORS: Available placeholder: + // TRANSLATORS: %(version) - the newest available version of the app + messages.pgettext( + 'notifications', + 'Please upgrade to %(version)s now to ensure your security', + ), + { version: this.context.suggestedUpgrade }, + ); + } + return message; + } } diff --git a/gui/src/shared/notifications/update-available.ts b/gui/src/shared/notifications/update-available.ts index 4d449bff28..d386765087 100644 --- a/gui/src/shared/notifications/update-available.ts +++ b/gui/src/shared/notifications/update-available.ts @@ -4,15 +4,14 @@ import { messages } from '../../shared/gettext'; import { InAppNotification, InAppNotificationProvider } from './notification'; interface UpdateAvailableNotificationContext { - current: string; - nextUpgrade: string | null; + suggestedUpgrade?: string; } export class UpdateAvailableNotificationProvider implements InAppNotificationProvider { public constructor(private context: UpdateAvailableNotificationContext) {} public mayDisplay() { - return this.context.nextUpgrade !== null && this.context.nextUpgrade !== this.context.current; + return this.context.suggestedUpgrade ? true : false; } public getInAppNotification(): InAppNotification { @@ -24,7 +23,7 @@ export class UpdateAvailableNotificationProvider implements InAppNotificationPro 'in-app-notifications', 'Install Mullvad VPN (%(version)s) to stay up to date', ), - { version: this.context.nextUpgrade }, + { version: this.context.suggestedUpgrade }, ); return { diff --git a/mullvad-cli/src/cmds/version.rs b/mullvad-cli/src/cmds/version.rs index b502a4809b..4e0f3a0d8b 100644 --- a/mullvad-cli/src/cmds/version.rs +++ b/mullvad-cli/src/cmds/version.rs @@ -19,22 +19,20 @@ impl Command for Version { let version_info = rpc.get_version_info()?; println!("\tIs supported: {}", version_info.supported); + match version_info.suggested_upgrade { + Some(version) => println!("\tSuggested update: {}", version), + None => println!("\tNo newer version is available"), + } + + if !version_info.latest_stable.is_empty() { + println!("\tLatest stable version: {}", version_info.latest_stable); + } + let settings = rpc.get_settings()?; - let is_updated = if settings.show_beta_releases { - version_info.latest == current_version - } else { - version_info.latest_stable == current_version + if settings.show_beta_releases { + println!("\t Latest beta version: {}", version_info.latest_beta); }; - println!("\tIs up to date: {}", is_updated); - if version_info.latest_stable != version_info.latest { - println!( - "Latest version: {} (latest stable: {})", - version_info.latest, version_info.latest_stable - ); - } else { - println!("Latest version: {}", version_info.latest_stable); - } Ok(()) } } diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index c69cf1cbd4..0ebffd12ea 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -13,7 +13,8 @@ chrono = { version = "0.4", features = ["serde"] } clap = "2.25" err-derive = "0.2.1" fern = { version = "0.5", features = ["colored"] } -futures = "0.1" +futures01 = { package = "futures", version = "0.1" } +futures = { package = "futures", version = "0.3", features = [ "compat" ]} ipnetwork = "0.16" jsonrpc-client-core = "0.5" jsonrpc-core = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } @@ -28,6 +29,7 @@ rand = "0.7" regex = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +tokio02 = { package = "tokio", version = "0.2", features = [ "io-util", "process", "rt-core", "rt-threaded", "stream", "fs"] } tokio-core = "0.1" tokio-retry = "0.2" tokio-timer = "0.1" diff --git a/mullvad-daemon/src/account_history.rs b/mullvad-daemon/src/account_history.rs index 23dd85a487..4a96428d94 100644 --- a/mullvad-daemon/src/account_history.rs +++ b/mullvad-daemon/src/account_history.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "android")] -use futures::future::{Executor, Future}; +use futures01::future::{Executor, Future}; #[cfg(not(target_os = "android"))] -use futures::{ +use futures01::{ future::{self, Executor, Future}, sync::oneshot, }; diff --git a/mullvad-daemon/src/event_loop.rs b/mullvad-daemon/src/event_loop.rs index 238b9f4eef..58f638a2ac 100644 --- a/mullvad-daemon/src/event_loop.rs +++ b/mullvad-daemon/src/event_loop.rs @@ -1,4 +1,4 @@ -use futures::{sync::oneshot, Future}; +use futures01::{sync::oneshot, Future}; use std::thread; use tokio_core::reactor::{Core, Remote}; diff --git a/mullvad-daemon/src/geoip.rs b/mullvad-daemon/src/geoip.rs index 5cc3085bfe..991ced7e5f 100644 --- a/mullvad-daemon/src/geoip.rs +++ b/mullvad-daemon/src/geoip.rs @@ -1,4 +1,4 @@ -use futures::{self, Future}; +use futures01::{self, Future}; use mullvad_rpc::{self, rest::RequestServiceHandle}; use mullvad_types::location::{AmIMullvad, GeoIpLocation}; diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 63dca09237..a3a856f792 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -1,4 +1,5 @@ #![deny(rust_2018_idioms)] +#![recursion_limit = "256"] #[macro_use] extern crate serde; @@ -17,7 +18,7 @@ mod settings; pub mod version; mod version_check; -use futures::{ +use futures01::{ future::{self, Executor}, stream::Wait, sync::{ @@ -337,7 +338,7 @@ pub struct DaemonCommandChannel { impl DaemonCommandChannel { pub fn new() -> Self { - let (untracked_sender, receiver) = futures::sync::mpsc::unbounded(); + let (untracked_sender, receiver) = futures01::sync::mpsc::unbounded(); let sender = DaemonCommandSender(Arc::new(untracked_sender)); Self { sender, receiver } @@ -464,6 +465,7 @@ pub struct Daemon<L: EventListener> { rpc_runtime: mullvad_rpc::MullvadRpcRuntime, rpc_handle: mullvad_rpc::rest::MullvadRestHandle, wireguard_key_manager: wireguard::KeyManager, + version_updater_handle: version_check::VersionUpdaterHandle, core_handle: event_loop::CoreHandle, relay_selector: relays::RelaySelector, last_generated_relay: Option<Relay>, @@ -510,14 +512,6 @@ where let (internal_event_tx, internal_event_rx) = command_channel.destructure(); - let app_version_info = version_check::load_cache(&cache_dir); - let version_check_future = version_check::VersionUpdater::new( - rpc_handle.clone(), - cache_dir.clone(), - internal_event_tx.to_specialized_sender(), - app_version_info.clone(), - ); - core_handle.remote.spawn(|_| version_check_future); let mut settings = SettingsPersister::load(&settings_dir); @@ -525,6 +519,15 @@ where let _ = settings.set_show_beta_releases(true); } + let app_version_info = version_check::load_cache(&cache_dir); + let (version_updater, version_updater_handle) = version_check::VersionUpdater::new( + rpc_handle.clone(), + cache_dir.clone(), + internal_event_tx.to_specialized_sender(), + app_version_info.clone(), + settings.show_beta_releases, + ); + rpc_runtime.runtime().spawn(version_updater.run()); let account_history = account_history::AccountHistory::new( &cache_dir, &settings_dir, @@ -609,6 +612,7 @@ where accounts_proxy: AccountsProxy::new(rpc_handle.clone()), rpc_handle, wireguard_key_manager, + version_updater_handle, core_handle, relay_selector, last_generated_relay: None, @@ -1458,6 +1462,9 @@ where if settings_changed { self.event_listener .notify_settings(self.settings.to_settings()); + let runtime = self.rpc_runtime.runtime(); + let mut handle = self.version_updater_handle.clone(); + runtime.block_on(async { handle.set_show_beta_releases(enabled).await }); } } Err(e) => error!("{}", e.display_chain_with_msg("Unable to save settings")), diff --git a/mullvad-daemon/src/relays.rs b/mullvad-daemon/src/relays.rs index 44c1d7ef90..2f8e7a7c61 100644 --- a/mullvad-daemon/src/relays.rs +++ b/mullvad-daemon/src/relays.rs @@ -2,7 +2,7 @@ //! updated as well. use chrono::{DateTime, Local}; -use futures::Future; +use futures01::Future; use mullvad_rpc::{rest::MullvadRestHandle, RelayListProxy}; use mullvad_types::{ endpoint::MullvadEndpoint, diff --git a/mullvad-daemon/src/version_check.rs b/mullvad-daemon/src/version_check.rs index 7a4446708d..c08238d23f 100644 --- a/mullvad-daemon/src/version_check.rs +++ b/mullvad-daemon/src/version_check.rs @@ -1,20 +1,33 @@ -use crate::{version::PRODUCT_VERSION, DaemonEventSender}; -use futures::{Async, Future, Poll}; +use crate::{ + version::{is_beta_version, PRODUCT_VERSION}, + DaemonEventSender, +}; +use futures::{channel::mpsc, stream::FusedStream, FutureExt, SinkExt, StreamExt, TryFutureExt}; use mullvad_rpc::{rest::MullvadRestHandle, AppVersionProxy}; use mullvad_types::version::AppVersionInfo; +use regex::Regex; use serde::{Deserialize, Serialize}; use std::{ - fs::File, + cmp::{Ord, Ordering, PartialOrd}, + fs, + future::Future, io, path::{Path, PathBuf}, time::{Duration, Instant}, }; use talpid_core::mpsc::Sender; use talpid_types::ErrorExt; -use tokio_timer::{TimeoutError, Timer}; +use tokio02::fs::File; const VERSION_INFO_FILENAME: &str = "version-info.json"; +lazy_static::lazy_static! { + static ref STABLE_REGEX: Regex = Regex::new(r"^(\d{4})\.(\d+)$").unwrap(); + static ref BETA_REGEX: Regex = Regex::new(r"^(\d{4})\.(\d+)-beta(\d+)$").unwrap(); + static ref APP_VERSION: Option<AppVersion> = AppVersion::from_str(PRODUCT_VERSION); + static ref IS_DEV_BUILD: bool = APP_VERSION.is_some(); +} + 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 @@ -56,17 +69,14 @@ impl From<AppVersionInfo> for CachedAppVersionInfo { #[error(no_from)] pub enum Error { #[error(display = "Failed to open app version cache file for reading")] - ReadCachedRelays(#[error(source)] io::Error), + ReadVersionCache(#[error(source)] io::Error), #[error(display = "Failed to open app version cache file for writing")] - WriteRelayCache(#[error(source)] io::Error), + WriteVersionCache(#[error(source)] io::Error), #[error(display = "Failure in serialization of the version info")] Serialize(#[error(source)] 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(source)] mullvad_rpc::rest::Error), @@ -74,12 +84,6 @@ pub enum Error { CacheVersionMismatch, } -impl<T> From<TimeoutError<T>> for Error { - fn from(_: TimeoutError<T>) -> Error { - Error::DownloadTimeout - } -} - pub(crate) struct VersionUpdater { version_proxy: AppVersionProxy, @@ -87,117 +91,199 @@ pub(crate) struct VersionUpdater { update_sender: DaemonEventSender<AppVersionInfo>, last_app_version_info: AppVersionInfo, next_update_time: Instant, - state: VersionUpdaterState, + show_beta_releases: bool, + rx: Option<mpsc::Receiver<bool>>, +} + +#[derive(Clone)] +pub(crate) struct VersionUpdaterHandle { + tx: mpsc::Sender<bool>, } -enum VersionUpdaterState { - Sleeping(tokio_timer::Sleep), - Updating(Box<dyn Future<Item = AppVersionInfo, Error = Error> + Send + 'static>), +impl VersionUpdaterHandle { + pub async fn set_show_beta_releases(&mut self, show_beta_releases: bool) { + if self.tx.send(show_beta_releases).await.is_err() { + log::error!("Version updater already down, can't send new `show_beta_releases` state"); + } + } } impl VersionUpdater { pub fn new( - rpc_handle: MullvadRestHandle, + mut rpc_handle: MullvadRestHandle, cache_dir: PathBuf, update_sender: DaemonEventSender<AppVersionInfo>, last_app_version_info: AppVersionInfo, - ) -> Self { + show_beta_releases: bool, + ) -> (Self, VersionUpdaterHandle) { + rpc_handle.factory.timeout = DOWNLOAD_TIMEOUT; let version_proxy = AppVersionProxy::new(rpc_handle); let cache_path = cache_dir.join(VERSION_INFO_FILENAME); - Self { - version_proxy, - cache_path, - update_sender, - last_app_version_info, - next_update_time: Instant::now(), - state: VersionUpdaterState::Sleeping(Self::create_sleep_future()), - } - } + let (tx, rx) = mpsc::channel(1); - fn create_sleep_future() -> tokio_timer::Sleep { - Timer::default().sleep(UPDATE_CHECK_INTERVAL) + ( + Self { + version_proxy, + cache_path, + update_sender, + last_app_version_info, + next_update_time: Instant::now(), + show_beta_releases, + rx: Some(rx), + }, + VersionUpdaterHandle { tx }, + ) } fn create_update_future( - &mut self, - ) -> Box<dyn Future<Item = AppVersionInfo, Error = Error> + Send + 'static> { - let download_future = self - .version_proxy - .version_check(PRODUCT_VERSION.to_owned(), PLATFORM) - .map_err(Error::Download); - let future = Timer::default().timeout(download_future, DOWNLOAD_TIMEOUT); - Box::new(future) + &self, + ) -> impl Future<Output = Result<mullvad_rpc::AppVersionResponse, Error>> + Send + 'static { + let version_proxy = self.version_proxy.clone(); + let download_future_factory = move || { + let response = version_proxy.version_check(PRODUCT_VERSION.to_owned(), PLATFORM); + response.map_err(Error::Download) + }; + + let should_retry = |result: &Result<_, _>| -> bool { result.is_err() }; + + Box::pin(talpid_core::future_retry::retry_future_with_backoff( + download_future_factory, + should_retry, + std::iter::repeat(UPDATE_INTERVAL_ERROR), + )) } - fn write_cache(&self) -> Result<(), Error> { + async 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)?; + let mut file = File::create(&self.cache_path) + .await + .map_err(Error::WriteVersionCache)?; let cached_app_version = CachedAppVersionInfo::from(self.last_app_version_info.clone()); - serde_json::to_writer_pretty(io::BufWriter::new(file), &cached_app_version) - .map_err(Error::Serialize) + let mut buf = serde_json::to_vec_pretty(&cached_app_version).map_err(Error::Serialize)?; + let mut read_buf: &[u8] = buf.as_mut(); + + let _ = tokio02::io::copy(&mut read_buf, &mut file) + .await + .map_err(Error::WriteVersionCache)?; + Ok(()) } -} -impl Future for VersionUpdater { - type Item = (); - type Error = (); + fn response_to_version_info( + &mut self, + response: mullvad_rpc::AppVersionResponse, + ) -> AppVersionInfo { + let suggested_upgrade = APP_VERSION.and_then(|current_version| { + Self::suggested_upgrade( + ¤t_version, + &response, + 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, + } + } + + fn suggested_upgrade( + current_version: &AppVersion, + response: &mullvad_rpc::AppVersionResponse, + show_beta: bool, + ) -> Option<String> { + let stable_version = response + .latest_stable + .as_ref() + .and_then(|stable| AppVersion::from_str(stable)); + + let beta_version = if show_beta { + AppVersion::from_str(&response.latest_beta) + } else { + None + }; + + let latest_version = stable_version.iter().chain(beta_version.iter()).max()?; + + if current_version < latest_version { + Some(latest_version.to_string()) + } else { + None + } + } + + pub async fn run(mut self) { + let mut rx = self.rx.take().unwrap().fuse(); + let next_delay = || tokio02::time::delay_for(UPDATE_CHECK_INTERVAL).fuse(); + let mut check_delay = next_delay(); + let mut version_check = futures::future::Fuse::terminated(); + + // If this is a dev build ,there's no need to pester the API for version checks. + if *IS_DEV_BUILD { + while let Some(_) = rx.next().await {} + return; + } - fn poll(&mut self) -> Poll<Self::Item, Self::Error> { loop { - if self.update_sender.is_closed() { - log::warn!("Version update receiver is closed, stopping version updater"); - return Ok(Async::Ready(())); - } - 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(()); + futures::select! { + show_beta_releases = rx.next() => { + match show_beta_releases { + Some(show_beta_releases ) => { + self.show_beta_releases = show_beta_releases; + }, + // time to shut down + None => { + return; + }, + } + }, + + _sleep = check_delay => { + if rx.is_terminated() || self.update_sender.is_closed() { + return; } - Ok(Async::Ready(())) => { - if Instant::now() > self.next_update_time { - VersionUpdaterState::Updating(self.create_update_future()) - } else { - VersionUpdaterState::Sleeping(Self::create_sleep_future()) - } + + if Instant::now() > self.next_update_time { + let download_future = self.create_update_future().fuse(); + version_check = download_future; + } else { + check_delay = next_delay(); } + }, - 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()) + + response = version_check => { + if rx.is_terminated() || self.update_sender.is_closed() { + return; } - Ok(Async::Ready(app_version_info)) => { - log::debug!("Got new version check: {:?}", app_version_info); - self.next_update_time = Instant::now() + UPDATE_INTERVAL; - if app_version_info != self.last_app_version_info { - if self.update_sender.send(app_version_info.clone()).is_err() { - log::warn!( - "Version update receiver is closed, stopping version updater" - ); - return Ok(Async::Ready(())); + self.next_update_time = Instant::now() + UPDATE_INTERVAL; + + match response { + Ok(version_info_response) => { + let new_version_info = self.response_to_version_info(version_info_response); + // if daemon can't be reached, return immediately + if self.update_sender.send(new_version_info.clone()).is_err() { + return; } - 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" - ) - ); + + self.last_app_version_info = new_version_info; + if let Err(err) = self.write_cache().await { + log::error!("Failed to save version cache to disk: {}", err); + } - } - VersionUpdaterState::Sleeping(Self::create_sleep_future()) + }, + Err(err) => { + log::error!("Failed to get fetch version info - {}", err); + }, } + + check_delay = next_delay(); }, - }; - self.state = next_state; + } } } } @@ -205,7 +291,7 @@ impl Future for VersionUpdater { fn try_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)?; + let file = fs::File::open(&path).map_err(Error::ReadVersionCache)?; let version_info: CachedAppVersionInfo = serde_json::from_reader(io::BufReader::new(file)).map_err(Error::Serialize)?; @@ -226,11 +312,179 @@ pub fn load_cache(cache_dir: &Path) -> AppVersionInfo { ); // If we don't have a cache, start out with sane defaults. AppVersionInfo { - supported: true, + supported: *IS_DEV_BUILD, latest_stable: PRODUCT_VERSION.to_owned(), latest_beta: PRODUCT_VERSION.to_owned(), - latest: PRODUCT_VERSION.to_owned(), + suggested_upgrade: None, + } + } + } +} + +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +enum AppVersion { + Stable(u32, u32), + Beta(u32, u32, u32), +} + +impl AppVersion { + fn from_str(version: &str) -> Option<Self> { + let get_int = |cap: ®ex::Captures<'_>, idx| cap.get(idx)?.as_str().parse().ok(); + + if let Some(caps) = STABLE_REGEX.captures(version) { + let year = get_int(&caps, 1)?; + let version = get_int(&caps, 2)?; + Some(Self::Stable(year, version)) + } else if let Some(caps) = BETA_REGEX.captures(version) { + let year = get_int(&caps, 1)?; + let version = get_int(&caps, 2)?; + let beta_version = get_int(&caps, 3)?; + Some(Self::Beta(year, version, beta_version)) + } else { + None + } + } +} + +impl Ord for AppVersion { + fn cmp(&self, other: &Self) -> Ordering { + use AppVersion::*; + match (self, other) { + (Stable(year, version), Stable(other_year, other_version)) => { + year.cmp(other_year).then(version.cmp(other_version)) } + // A stable version of the same year and version is always greater than a beta + (Stable(year, version), Beta(other_year, other_version, _)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Greater), + ( + Beta(year, version, beta_version), + Beta(other_year, other_version, other_beta_version), + ) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(beta_version.cmp(other_beta_version)), + (Beta(year, version, _beta_version), Stable(other_year, other_version)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Less), + } + } +} + +impl PartialOrd for AppVersion { + fn partial_cmp(&self, other: &AppVersion) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl ToString for AppVersion { + fn to_string(&self) -> String { + match self { + Self::Stable(year, version) => format!("{}.{}", year, version), + Self::Beta(year, version, beta_version) => { + format!("{}.{}-beta{}", year, version, beta_version) + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_version_regex() { + assert!(STABLE_REGEX.is_match("2020.4")); + assert!(!STABLE_REGEX.is_match("2020.4-beta3")); + assert!(BETA_REGEX.is_match("2020.4-beta3")); + assert!(!STABLE_REGEX.is_match("2020.5-beta1-dev-f16be4")); + assert!(!STABLE_REGEX.is_match("2020.5-dev-f16be4")); + assert!(!BETA_REGEX.is_match("2020.5-beta1-dev-f16be4")); + assert!(!BETA_REGEX.is_match("2020.5-dev-f16be4")); + assert!(!BETA_REGEX.is_match("2020.4")); + } + + #[test] + fn test_version_parsing() { + let tests = vec![ + ("2020.4", Some(AppVersion::Stable(2020, 4))), + ("2020.4-beta3", Some(AppVersion::Beta(2020, 4, 3))), + ("2020.15-beta1-dev-f16be4", None), + ("2020.15-dev-f16be4", None), + ("", None), + ]; + + for (input, expected_output) in tests { + assert_eq!(AppVersion::from_str(&input), expected_output,); } } + + #[test] + fn test_version_upgrade_suggestions() { + let app_version_info = mullvad_rpc::AppVersionResponse { + supported: true, + latest: "2020.5-beta3".to_owned(), + latest_stable: Some("2020.4".to_string()), + latest_beta: "2020.5-beta3".to_string(), + }; + + let older_stable = AppVersion::from_str("2020.3").unwrap(); + let current_stable = AppVersion::from_str("2020.4").unwrap(); + let newer_stable = AppVersion::from_str("2021.5").unwrap(); + + let older_beta = AppVersion::from_str("2020.3-beta3").unwrap(); + let current_beta = AppVersion::from_str("2020.5-beta3").unwrap(); + let newer_beta = AppVersion::from_str("2021.5-beta3").unwrap(); + + assert_eq!( + VersionUpdater::suggested_upgrade(&older_stable, &app_version_info, false), + Some("2020.4".to_owned()) + ); + assert_eq!( + VersionUpdater::suggested_upgrade(&older_stable, &app_version_info, true), + Some("2020.5-beta3".to_owned()) + ); + assert_eq!( + VersionUpdater::suggested_upgrade(¤t_stable, &app_version_info, false), + None + ); + assert_eq!( + VersionUpdater::suggested_upgrade(¤t_stable, &app_version_info, true), + Some("2020.5-beta3".to_owned()) + ); + assert_eq!( + VersionUpdater::suggested_upgrade(&newer_stable, &app_version_info, false), + None + ); + assert_eq!( + VersionUpdater::suggested_upgrade(&newer_stable, &app_version_info, true), + None + ); + assert_eq!( + VersionUpdater::suggested_upgrade(&older_beta, &app_version_info, false), + Some("2020.4".to_owned()) + ); + assert_eq!( + VersionUpdater::suggested_upgrade(&older_beta, &app_version_info, true), + Some("2020.5-beta3".to_owned()) + ); + assert_eq!( + VersionUpdater::suggested_upgrade(¤t_beta, &app_version_info, false), + None + ); + assert_eq!( + VersionUpdater::suggested_upgrade(¤t_beta, &app_version_info, true), + None + ); + assert_eq!( + VersionUpdater::suggested_upgrade(&newer_beta, &app_version_info, false), + None + ); + assert_eq!( + VersionUpdater::suggested_upgrade(&newer_beta, &app_version_info, true), + None + ); + } } diff --git a/mullvad-daemon/src/wireguard.rs b/mullvad-daemon/src/wireguard.rs index 1b5913699f..7cf1209429 100644 --- a/mullvad-daemon/src/wireguard.rs +++ b/mullvad-daemon/src/wireguard.rs @@ -1,6 +1,6 @@ use crate::{account_history::AccountHistory, DaemonEventSender, InternalDaemonEvent}; use chrono::offset::Utc; -use futures::{future::Executor, stream::Stream, sync::oneshot, Async, Future, Poll}; +use futures01::{future::Executor, stream::Stream, sync::oneshot, Async, Future, Poll}; use mullvad_rpc::rest::{Error as RestError, MullvadRestHandle}; use mullvad_types::account::AccountToken; pub use mullvad_types::wireguard::*; diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs index 15b72f8f6b..1fd30f9661 100644 --- a/mullvad-jni/src/lib.rs +++ b/mullvad-jni/src/lib.rs @@ -570,7 +570,7 @@ pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_getCurr if let Some(daemon_interface) = get_daemon_interface(daemon_interface_address) { match daemon_interface.get_current_version() { - Ok(location) => location.into_java(&env).forget(), + Ok(version) => version.into_java(&env).forget(), Err(error) => { log::error!( "{}", diff --git a/mullvad-rpc/Cargo.toml b/mullvad-rpc/Cargo.toml index 3bd6a78fe4..3c5a29a617 100644 --- a/mullvad-rpc/Cargo.toml +++ b/mullvad-rpc/Cargo.toml @@ -16,6 +16,7 @@ http = "0.2" hyper = "0.13" ipnetwork = "0.16" log = "0.4" +regex = "1" serde = "1" serde_json = "1.0" hyper-rustls = "0.20" diff --git a/mullvad-rpc/src/lib.rs b/mullvad-rpc/src/lib.rs index aaea9aad4e..bc6a71a5ba 100644 --- a/mullvad-rpc/src/lib.rs +++ b/mullvad-rpc/src/lib.rs @@ -5,10 +5,11 @@ use futures01::future::Future as Future01; use hyper::Method; use mullvad_types::{ account::{AccountToken, VoucherSubmission}, - version::{AppVersion, AppVersionInfo}, + version::AppVersion, }; use std::{ collections::BTreeMap, + future::Future, net::{IpAddr, Ipv4Addr}, path::Path, }; @@ -99,6 +100,10 @@ impl MullvadRpcRuntime { pub fn rest_handle(&mut self) -> rest::RequestServiceHandle { self.new_request_service(None) } + + pub fn runtime(&mut self) -> &mut tokio::runtime::Runtime { + &mut self.runtime + } } impl Drop for MullvadRpcRuntime { @@ -269,16 +274,17 @@ impl ProblemReportProxy { } } +#[derive(Clone)] pub struct AppVersionProxy { handle: rest::MullvadRestHandle, } -#[derive(serde::Deserialize)] -struct AppVersionResponse { - supported: bool, - latest: AppVersion, - latest_stable: Option<AppVersion>, - latest_beta: AppVersion, +#[derive(serde::Deserialize, Debug)] +pub struct AppVersionResponse { + pub supported: bool, + pub latest: AppVersion, + pub latest_stable: Option<AppVersion>, + pub latest_beta: AppVersion, } impl AppVersionProxy { @@ -290,7 +296,7 @@ impl AppVersionProxy { &self, version: AppVersion, platform: &str, - ) -> impl Future01<Item = AppVersionInfo, Error = rest::Error> { + ) -> impl Future<Output = Result<AppVersionResponse, rest::Error>> { let service = self.handle.service.clone(); let request = rest::send_request( @@ -302,20 +308,7 @@ impl AppVersionProxy { StatusCode::OK, ); - let future = async move { - let response: AppVersionResponse = rest::deserialize_body(request.await?).await?; - - let version_info = AppVersionInfo { - supported: response.supported, - latest: response.latest, - latest_stable: response.latest_stable.unwrap_or_else(|| "".to_owned()), - latest_beta: response.latest_beta, - }; - - Ok(version_info) - }; - - self.handle.service.compat_spawn(future) + async move { rest::deserialize_body(request.await?).await } } } diff --git a/mullvad-rpc/src/rest.rs b/mullvad-rpc/src/rest.rs index c36f1d894e..d29feee9f0 100644 --- a/mullvad-rpc/src/rest.rs +++ b/mullvad-rpc/src/rest.rs @@ -329,6 +329,7 @@ pub struct RequestFactory { host: String, address: Option<IpAddr>, path_prefix: Option<String>, + pub timeout: Duration, } @@ -338,20 +339,26 @@ impl RequestFactory { host, address, path_prefix, + timeout: DEFAULT_TIMEOUT, } } pub fn request(&self, path: &str, method: Method) -> Result<RestRequest> { - self.hyper_request(path, method).map(RestRequest::from) + self.hyper_request(path, method) + .map(RestRequest::from) + .map(|req| self.set_request_timeout(req)) } pub fn get(&self, path: &str) -> Result<RestRequest> { - self.hyper_request(path, Method::GET).map(RestRequest::from) + self.hyper_request(path, Method::GET) + .map(RestRequest::from) + .map(|req| self.set_request_timeout(req)) } pub fn post(&self, path: &str) -> Result<RestRequest> { self.hyper_request(path, Method::POST) .map(RestRequest::from) + .map(|req| self.set_request_timeout(req)) } pub fn post_json<S: serde::Serialize>(&self, path: &str, body: &S) -> Result<RestRequest> { @@ -399,6 +406,11 @@ impl RequestFactory { let uri = format!("https://{}/{}{}", host, prefix, path); hyper::Uri::from_str(&uri).map_err(Error::UriError) } + + fn set_request_timeout(&self, mut request: RestRequest) -> RestRequest { + request.timeout = self.timeout; + request + } } @@ -535,7 +547,7 @@ pub async fn handle_error_response<T>(response: Response) -> Result<T> { #[derive(Clone)] pub struct MullvadRestHandle { pub(crate) service: RequestServiceHandle, - pub(crate) factory: RequestFactory, + pub factory: RequestFactory, } impl MullvadRestHandle { diff --git a/mullvad-types/src/version.rs b/mullvad-types/src/version.rs index 1603c7752b..47f26f2de0 100644 --- a/mullvad-types/src/version.rs +++ b/mullvad-types/src/version.rs @@ -15,13 +15,15 @@ pub struct AppVersionInfo { /// issues, so using it is no longer recommended. /// The user should really upgrade when this is false. pub supported: bool, - /// Latest version - pub latest: AppVersion, /// Latest stable version + #[cfg_attr(target_os = "android", jnix(skip))] 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. + #[cfg_attr(target_os = "android", jnix(skip))] pub latest_beta: AppVersion, + /// Whether should update to newer version + pub suggested_upgrade: Option<AppVersion>, } pub type AppVersion = String; diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index f18c867cfe..ecaf5f8fcb 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -38,6 +38,7 @@ tokio02 = { package = "tokio", version = "0.2", features = [ "io-util", "proces triggered = "0.1.1" tonic = "0.2" prost = "0.6" +rand = "0.7" [target.'cfg(unix)'.dependencies] @@ -87,3 +88,5 @@ tonic-build = { version = "0.2", default-features = false, features = ["transpor [dev-dependencies] tempfile = "3.0" +quickcheck = "0.9" +quickcheck_macros = "0.9" diff --git a/talpid-core/src/future_retry.rs b/talpid-core/src/future_retry.rs new file mode 100644 index 0000000000..74dc6f2f8c --- /dev/null +++ b/talpid-core/src/future_retry.rs @@ -0,0 +1,209 @@ +use rand::{distributions::OpenClosed01, Rng}; +use std::{future::Future, marker::Unpin, time::Duration}; + +/// Since timers often exhibit weird behavior if they are running for too long, a workaround is +/// required - run a timer for 60 seconds until a delay is shorter than 5 minutes. +const MAX_SINGLE_DELAY: Duration = Duration::from_secs(5 * 60); + +/// Retries a future until it should stop as determined by the retry function. +pub fn retry_future_with_backoff< + F: FnMut() -> O + 'static, + R: FnMut(&T) -> bool + 'static, + D: Iterator<Item = Duration> + 'static, + O: Future<Output = T>, + T: Unpin, +>( + mut factory: F, + mut should_retry: R, + mut delays: D, +) -> impl Future<Output = T> + 'static { + async move { + loop { + let current_result = factory().await; + if should_retry(¤t_result) { + if let Some(delay) = delays.next() { + sleep(delay).await; + } + } else { + return current_result; + } + } + } +} + +async fn sleep(mut delay: Duration) { + while delay > MAX_SINGLE_DELAY { + delay -= MAX_SINGLE_DELAY; + tokio02::time::delay_for(MAX_SINGLE_DELAY).await; + } + + tokio02::time::delay_for(delay).await; +} + +/// Provides an exponential back-off timer to delay the next retry of a failed operation. +pub struct ExponentialBackoff { + current: u64, + base: u64, + factor: u64, + max_delay: Option<Duration>, +} + +impl ExponentialBackoff { + /// Creates a `ExponentialBackoff` with the provided number of milliseconds as a base. + /// + /// All else staying the same, the first delay will be `millis` milliseconds long, the second + /// one will be `millis^2`, third `millis^3` and so on. + pub fn from_millis(millis: u64) -> ExponentialBackoff { + ExponentialBackoff { + current: millis, + base: millis, + factor: 1u64, + max_delay: None, + } + } + + /// Sets the constant factor of the delays. The default value is 1. + pub fn factor(mut self, factor: u64) -> ExponentialBackoff { + self.factor = factor; + self + } + + /// Set the maximum delay. By default, there is no maximum value set, but the practical limit + /// is `std::u64::MAX`. + pub fn max_delay(mut self, duration: Duration) -> ExponentialBackoff { + self.max_delay = Some(duration); + self + } + + /// Returns the value of the delay and advances the next back-off delay. + fn next_delay(&mut self) -> Duration { + let delay_msec = self + .current + .checked_mul(self.factor) + .unwrap_or(std::u64::MAX); + let delay = Duration::from_millis(delay_msec); + + if let Some(max_delay) = self.max_delay { + if delay > max_delay { + return max_delay; + } + } + + self.current = self.current.checked_mul(self.base).unwrap_or(std::u64::MAX); + delay + } + + /// Resets the delay to it's initial state. + pub fn reset(&mut self) { + self.current = self.base; + } +} + +impl Iterator for ExponentialBackoff { + type Item = Duration; + fn next(&mut self) -> Option<Duration> { + Some(self.next_delay()) + } +} + +/// Adds jitter to a duration iterator +pub struct Jittered<I: Iterator<Item = Duration>> { + inner: I, +} + +impl<I: Iterator<Item = Duration>> Jittered<I> { + /// Create an iterator of jittered durations + pub fn jitter(inner: I) -> Self { + Self { inner } + } +} + +impl<I: Iterator<Item = Duration>> Iterator for Jittered<I> { + type Item = Duration; + fn next(&mut self) -> Option<Self::Item> { + let next_value = self.inner.next()?; + Some(jitter(next_value)) + } +} + +/// Apply a jitter to a duration. +fn jitter(dur: Duration) -> Duration { + apply_jitter(dur, rand::thread_rng().sample(OpenClosed01)) +} + +fn apply_jitter(duration: Duration, jitter: f64) -> Duration { + let secs = (duration.as_secs() as f64) * jitter; + let nanos = (duration.subsec_nanos() as f64) * jitter; + let millis = (secs * 1000f64) + (nanos / 1000000f64); + Duration::from_millis(millis as u64) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_exponetnial_backoff() { + let mut backoff = ExponentialBackoff::from_millis(2).factor(1000); + + assert_eq!(backoff.next(), Some(Duration::from_secs(2))); + assert_eq!(backoff.next(), Some(Duration::from_secs(4))); + assert_eq!(backoff.next(), Some(Duration::from_secs(8))); + backoff.reset(); + assert_eq!(backoff.next(), Some(Duration::from_secs(2))); + } + + #[test] + fn test_at_maximum_value() { + let mut backoff = ExponentialBackoff::from_millis(std::u64::MAX - 1); + + assert_eq!( + backoff.next(), + Some(Duration::from_millis(std::u64::MAX - 1)) + ); + assert_eq!(backoff.next(), Some(Duration::from_millis(std::u64::MAX))); + assert_eq!(backoff.next(), Some(Duration::from_millis(std::u64::MAX))); + } + + #[test] + fn test_maximum_bound() { + let mut backoff = ExponentialBackoff::from_millis(2).max_delay(Duration::from_millis(4)); + + assert_eq!(backoff.next(), Some(Duration::from_millis(2))); + assert_eq!(backoff.next(), Some(Duration::from_millis(4))); + assert_eq!(backoff.next(), Some(Duration::from_millis(4))); + } + + #[test] + fn test_minimum_value() { + let mut backoff = ExponentialBackoff::from_millis(0); + + assert_eq!(backoff.next(), Some(Duration::from_millis(0))); + assert_eq!(backoff.next(), Some(Duration::from_millis(0))); + } + + #[test] + fn test_rounding() { + let second = Duration::from_secs(1); + assert_eq!(apply_jitter(second, 1.0), second); + } + + #[derive(Clone, Debug)] + struct ArbitraryJitter(f64); + impl quickcheck::Arbitrary for ArbitraryJitter { + fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self { + let jitter = g.sample(OpenClosed01); + ArbitraryJitter(jitter) + } + } + + #[quickcheck_macros::quickcheck] + fn test_jitter(millis: u64, jitter: ArbitraryJitter) { + let jitter = jitter.0; + let unjittered_duration = Duration::from_millis(millis); + let expected_duration = Duration::from_millis(((millis as f64) * jitter) as u64); + let jittered_duration = apply_jitter(unjittered_duration, jitter); + assert_eq!(expected_duration, jittered_duration); + assert!(jittered_duration <= unjittered_duration); + } +} diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index 24bdc7deee..fbf06c8815 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -45,6 +45,9 @@ pub mod dns; /// State machine to handle tunnel configuration. pub mod tunnel_state_machine; +/// Future utilities +pub mod future_retry; + #[cfg(not(target_os = "android"))] /// Internal code for managing bundled proxy software. mod proxy; |
