summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2020-06-25 15:22:23 +0100
committerEmīls <emils@mullvad.net>2020-06-25 15:22:23 +0100
commitc586258b52f0d14aedc333d877484da4f3805e05 (patch)
tree9492cde4c155fb83cef7d63627e8647af1194015
parent010d7a9771a980491959bbddf97214928c9e4e93 (diff)
parentc147d3b36bcd1a4be37da0aaef9d49f5ce40bcee (diff)
downloadmullvadvpn-c586258b52f0d14aedc333d877484da4f3805e05.tar.xz
mullvadvpn-c586258b52f0d14aedc333d877484da4f3805e05.zip
Merge branch 'version-check-supress-notification'
-rw-r--r--.travis.yml2
-rw-r--r--CHANGELOG.md3
-rw-r--r--Cargo.lock28
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/AppVersionInfoCache.kt32
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt10
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/ConnectionProxy.kt3
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt19
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/KeyStatusListener.kt16
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/tunnelstate/Persistence.kt10
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountInput.kt26
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CellSwitch.kt11
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/NotificationBanner.kt6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/Button.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/CopyableInformationView.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/InformationView.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/ListenableScrollView.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/UrlButton.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/util/JobTracker.kt13
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/util/SmartDeferred.kt8
-rw-r--r--gui/src/main/daemon-rpc.ts4
-rw-r--r--gui/src/main/index.ts53
-rw-r--r--gui/src/renderer/app.tsx7
-rw-r--r--gui/src/renderer/containers/SettingsPage.tsx4
-rw-r--r--gui/src/renderer/containers/SupportPage.tsx4
-rw-r--r--gui/src/renderer/redux/version/actions.ts8
-rw-r--r--gui/src/renderer/redux/version/reducers.ts12
-rw-r--r--gui/src/shared/daemon-rpc-types.ts4
-rw-r--r--gui/src/shared/ipc-event-channel.ts9
-rw-r--r--gui/src/shared/notifications/unsupported-version.ts46
-rw-r--r--gui/src/shared/notifications/update-available.ts7
-rw-r--r--mullvad-cli/src/cmds/version.rs24
-rw-r--r--mullvad-daemon/Cargo.toml4
-rw-r--r--mullvad-daemon/src/account_history.rs4
-rw-r--r--mullvad-daemon/src/event_loop.rs2
-rw-r--r--mullvad-daemon/src/geoip.rs2
-rw-r--r--mullvad-daemon/src/lib.rs27
-rw-r--r--mullvad-daemon/src/relays.rs2
-rw-r--r--mullvad-daemon/src/version_check.rs444
-rw-r--r--mullvad-daemon/src/wireguard.rs2
-rw-r--r--mullvad-jni/src/lib.rs2
-rw-r--r--mullvad-rpc/Cargo.toml1
-rw-r--r--mullvad-rpc/src/lib.rs37
-rw-r--r--mullvad-rpc/src/rest.rs18
-rw-r--r--mullvad-types/src/version.rs6
-rw-r--r--talpid-core/Cargo.toml3
-rw-r--r--talpid-core/src/future_retry.rs209
-rw-r--r--talpid-core/src/lib.rs3
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(
+ &current_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: &regex::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(&current_stable, &app_version_info, false),
+ None
+ );
+ assert_eq!(
+ VersionUpdater::suggested_upgrade(&current_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(&current_beta, &app_version_info, false),
+ None
+ );
+ assert_eq!(
+ VersionUpdater::suggested_upgrade(&current_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(&current_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;