summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-04-05 09:13:46 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-04-05 09:13:46 +0200
commitf758f894da5e368e54c83bd59013074192ce12be (patch)
tree172961804441441ac6800d0c463b1e99feec5912
parent0f313bf16aae17e43981ad9716d9c73c780ea8db (diff)
parent25ed6a8ea14ab9ef2442abb08d4ef2de0a5de8a9 (diff)
downloadmullvadvpn-f758f894da5e368e54c83bd59013074192ce12be.tar.xz
mullvadvpn-f758f894da5e368e54c83bd59013074192ce12be.zip
Merge branch 'result-operation-subclass'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/Account.swift33
-rw-r--r--ios/MullvadVPN/AddressCache/AddressCacheTracker.swift26
-rw-r--r--ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift102
-rw-r--r--ios/MullvadVPN/AppDelegate.swift4
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift88
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift86
-rw-r--r--ios/MullvadVPN/AppStoreReceipt.swift22
-rw-r--r--ios/MullvadVPN/Operations/ProductsRequestOperation.swift21
-rw-r--r--ios/MullvadVPN/ProblemReportViewController.swift11
-rw-r--r--ios/MullvadVPN/REST/RESTClient.swift51
-rw-r--r--ios/MullvadVPN/REST/RESTNetworkOperation.swift42
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift20
-rw-r--r--ios/MullvadVPN/Result+UIBackgroundFetchResult.swift12
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift67
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift13
-rw-r--r--ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift29
-rw-r--r--ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift24
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift21
-rw-r--r--ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift52
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift79
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift67
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift5
23 files changed, 416 insertions, 463 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 65cf355107..a030a5546b 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -135,6 +135,7 @@
585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; };
585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; };
585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
+ 585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendAppStoreReceiptOperation.swift */; };
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; };
58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; };
58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; };
@@ -424,6 +425,7 @@
585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCRequest.swift; sourceTree = "<group>"; };
585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCResponse.swift; sourceTree = "<group>"; };
585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelStatus.swift; sourceTree = "<group>"; };
+ 585E820227F3285E00939F0E /* SendAppStoreReceiptOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppStoreReceiptOperation.swift; sourceTree = "<group>"; };
5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; };
58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorConfiguration.swift; sourceTree = "<group>"; };
5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; };
@@ -715,6 +717,7 @@
58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */,
5846227226E22A160035F7C2 /* AppStorePaymentObserver.swift */,
5846227026E229F20035F7C2 /* AppStoreSubscription.swift */,
+ 585E820227F3285E00939F0E /* SendAppStoreReceiptOperation.swift */,
);
path = AppStorePaymentManager;
sourceTree = "<group>";
@@ -1367,6 +1370,7 @@
58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */,
584E96BC240FD4DA00D3334F /* Location.swift in Sources */,
581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */,
+ 585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */,
584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */,
5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */,
585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */,
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
index eade0fb45d..2ad08b0e49 100644
--- a/ios/MullvadVPN/Account.swift
+++ b/ios/MullvadVPN/Account.swift
@@ -121,6 +121,9 @@ class Account {
completionHandler(.failure(.createAccount(error)))
operation.finish()
+
+ case .cancelled:
+ operation.finish()
}
}
}
@@ -150,6 +153,9 @@ class Account {
case .failure(let error):
completionHandler(.failure(.verifyAccount(error)))
operation.finish()
+
+ case .cancelled:
+ operation.finish()
}
}
}
@@ -200,22 +206,25 @@ class Account {
return
}
- _ = REST.Client.shared.getAccountExpiry(token: token, retryStrategy: .default) { result in
- switch result {
- case .success(let response):
- if self.expiry != response.expires {
- self.expiry = response.expires
- self.observerList.forEach { (observer) in
- observer.account(self, didUpdateExpiry: response.expires)
- }
+ _ = REST.Client.shared.getAccountExpiry(token: token, retryStrategy: .default) { completion in
+ switch completion {
+ case .success(let response):
+ if self.expiry != response.expires {
+ self.expiry = response.expires
+ self.observerList.forEach { (observer) in
+ observer.account(self, didUpdateExpiry: response.expires)
}
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to update account expiry.")
}
- operation.finish()
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to update account expiry.")
+
+ case .cancelled:
+ self.logger.debug("Account expiry update was cancelled.")
}
+
+ operation.finish()
+ }
}
}
diff --git a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
index 8502d12ffc..de84265b1e 100644
--- a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
+++ b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
@@ -83,16 +83,16 @@ extension AddressCache {
}
}
- func updateEndpoints(completionHandler: ((_ result: CacheUpdateResult) -> Void)? = nil) -> Cancellable {
+ func updateEndpoints(completionHandler: ((_ completion: OperationCompletion<CacheUpdateResult, Error>) -> Void)? = nil) -> Cancellable {
let operation = UpdateAddressCacheOperation(
queue: stateQueue,
restClient: restClient,
store: store,
updateInterval: Self.updateInterval,
- completionHandler: { [weak self] result in
- self?.handleCacheUpdateResult(result)
+ completionHandler: { [weak self] completion in
+ self?.handleCacheUpdateCompletion(completion)
- completionHandler?(result)
+ completionHandler?(completion)
}
)
@@ -144,20 +144,22 @@ extension AddressCache {
}
}
- private func handleCacheUpdateResult(_ result: AddressCache.CacheUpdateResult) {
- switch result {
- case .success:
- logger.debug("Finished updating address cache")
+ private func handleCacheUpdateCompletion(_ completion: OperationCompletion<AddressCache.CacheUpdateResult, Error>) {
+ switch completion {
+ case .success(let updateResult):
+ switch updateResult {
+ case .finished:
+ logger.debug("Finished updating address cache")
+ case .throttled:
+ logger.debug("Address cache update was throttled")
+ }
+
lastFailureAttemptDate = nil
case .failure(let error):
logger.error(chainedError: AnyChainedError(error), message: "Failed to update address cache")
lastFailureAttemptDate = Date()
- case .throttled:
- logger.debug("Address cache update was throttled")
- lastFailureAttemptDate = nil
-
case .cancelled:
logger.debug("Address cache update was cancelled")
lastFailureAttemptDate = Date()
diff --git a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift b/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
index 8fcd41b7dd..81940acd21 100644
--- a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
+++ b/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
@@ -11,63 +11,48 @@ import Foundation
extension AddressCache {
enum CacheUpdateResult {
- /// Operation was cancelled.
- case cancelled
-
/// Address cache update was throttled as it was requested too early.
case throttled(_ lastUpdateDate: Date)
- /// Failure to update address cache.
- case failure(Error)
-
/// Address cache is successfully updated.
- case success
-
- var isTaskCompleted: Bool {
- switch self {
- case .cancelled, .failure:
- return false
- case .success, .throttled:
- return true
- }
- }
+ case finished
}
- class UpdateAddressCacheOperation: AsyncOperation {
- typealias CompletionHandler = (_ result: CacheUpdateResult) -> Void
-
+ class UpdateAddressCacheOperation: ResultOperation<CacheUpdateResult, Error> {
private let queue: DispatchQueue
private let restClient: REST.Client
private let store: AddressCache.Store
private let updateInterval: TimeInterval
- private var completionHandler: CompletionHandler?
- private var restCancellationHandle: Cancellable?
+ private var requestTask: Cancellable?
init(queue: DispatchQueue, restClient: REST.Client, store: AddressCache.Store, updateInterval: TimeInterval, completionHandler: CompletionHandler?) {
self.queue = queue
self.restClient = restClient
self.store = store
self.updateInterval = updateInterval
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
- override func cancel() {
+ override func main() {
queue.async {
- super.cancel()
- self.restCancellationHandle?.cancel()
+ self.startUpdate()
}
}
- override func main() {
+ override func cancel() {
+ super.cancel()
+
queue.async {
- self.startUpdate()
+ self.requestTask?.cancel()
+ self.requestTask = nil
}
}
private func startUpdate() {
guard !isCancelled else {
- completeOperation(with: .cancelled)
+ finish(completion: .cancelled)
return
}
@@ -75,40 +60,49 @@ extension AddressCache {
let nextUpdate = Date(timeInterval: updateInterval, since: lastUpdate)
guard nextUpdate <= Date() else {
- completeOperation(with: .throttled(lastUpdate))
+ finish(completion: .success(.throttled(lastUpdate)))
return
}
- restCancellationHandle = restClient.getAddressList(retryStrategy: .default) { restResult in
- self.queue.async {
- switch restResult {
- case .success(let newEndpoints):
- self.store.setEndpoints(newEndpoints) { error in
- self.queue.async {
- if let error = error {
- self.completeOperation(with: .failure(error))
- } else {
- self.completeOperation(with: .success)
- }
- }
- }
+ requestTask = restClient.getAddressList(retryStrategy: .default) { completion in
+ self.queue.async {
+ self.handleResponse(completion)
+ }
+ }
+ }
- case .failure(let error):
- if case URLError.cancelled = error {
- self.completeOperation(with: .cancelled)
- } else {
- self.completeOperation(with: .failure(error))
- }
- }
+ private func handleResponse(_ completion: OperationCompletion<[AnyIPEndpoint], REST.Error>) {
+ switch completion {
+ case .success(let newEndpoints):
+ self.store.setEndpoints(newEndpoints) { error in
+ if let error = error {
+ self.finish(completion: .failure(error))
+ } else {
+ self.finish(completion: .success(.finished))
}
}
- }
- private func completeOperation(with result: CacheUpdateResult) {
- completionHandler?(result)
- completionHandler = nil
+ case .failure(let error):
+ if case URLError.cancelled = error {
+ self.finish(completion: .cancelled)
+ } else {
+ self.finish(completion: .failure(error))
+ }
+
+ case .cancelled:
+ self.finish(completion: .cancelled)
+ }
+ }
+ }
+}
- finish()
+extension OperationCompletion where Success == AddressCache.CacheUpdateResult {
+ var isTaskCompleted: Bool {
+ switch self {
+ case .success:
+ return true
+ case .cancelled, .failure:
+ return false
}
}
}
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 2d719ebc0b..bbac93fd1a 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -192,7 +192,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
let updateRelaysOperation = AsyncBlockOperation { operation in
- let cancellable = RelayCache.Tracker.shared.updateRelays { completion in
+ let handle = RelayCache.Tracker.shared.updateRelays { completion in
switch completion {
case .success(let result):
self.logger?.debug("Finished updating relays: \(result)")
@@ -208,7 +208,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
operation.addCancellationBlock {
- cancellable.cancel()
+ handle.cancel()
}
}
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
index 8c0567c777..7bdc1ce106 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
@@ -133,6 +133,17 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
didFailWithError: .validateAccount(error)
)
}
+
+ case .cancelled:
+ self.observerList.forEach { observer in
+ observer.appStorePaymentManager(
+ self,
+ transaction: nil,
+ payment: payment,
+ accountToken: accountToken,
+ didFailWithError: .validateAccount(.network(URLError(.cancelled)))
+ )
+ }
}
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
@@ -284,80 +295,3 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
}
-
-private class SendAppStoreReceiptOperation: AsyncOperation {
- typealias CompletionHandler = (OperationCompletion<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error>) -> Void
-
- private let restClient: REST.Client
- private let accountToken: String
- private let forceRefresh: Bool
- private let receiptProperties: [String: Any]?
- private var completionHandler: CompletionHandler?
- private var fetchReceiptCancellable: Cancellable?
- private var submitReceiptCancellable: Cancellable?
-
- private let logger = Logger(label: "AppStorePaymentManager.SendAppStoreReceiptOperation")
-
- init(restClient: REST.Client, accountToken: String, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping CompletionHandler) {
- self.restClient = restClient
- self.accountToken = accountToken
- self.forceRefresh = forceRefresh
- self.receiptProperties = receiptProperties
- self.completionHandler = completionHandler
- }
-
- override func cancel() {
- super.cancel()
-
- DispatchQueue.main.async {
- self.fetchReceiptCancellable?.cancel()
- self.fetchReceiptCancellable = nil
-
- self.submitReceiptCancellable?.cancel()
- self.submitReceiptCancellable = nil
- }
- }
-
- override func main() {
- DispatchQueue.main.async {
- self.fetchReceiptCancellable = AppStoreReceipt.fetch(forceRefresh: self.forceRefresh, receiptProperties: self.receiptProperties) { completion in
- switch completion {
- case .success(let receiptData):
- self.sendReceipt(receiptData)
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to fetch the AppStore receipt.")
- self.finish(completion: .failure(.readReceipt(error)))
-
- case .cancelled:
- self.finish(completion: .cancelled)
- }
- }
- }
- }
-
- private func sendReceipt(_ receiptData: Data) {
- submitReceiptCancellable = restClient.createApplePayment(
- token: self.accountToken,
- receiptString: receiptData,
- retryStrategy: .noRetry) { result in
- switch result {
- case .success(let response):
- self.logger.info("AppStore receipt was processed. Time added: \(response.timeAdded), New expiry: \(response.newExpiry.logFormatDate())")
- self.finish(completion: .success(response))
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to send the AppStore receipt.")
- self.finish(completion: .failure(.sendReceipt(error)))
- }
- }
- }
-
- private func finish(completion: OperationCompletion<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error>) {
- let block = completionHandler
- completionHandler = nil
-
- block?(completion)
- finish()
- }
-}
diff --git a/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift b/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
new file mode 100644
index 0000000000..69ddf4459c
--- /dev/null
+++ b/ios/MullvadVPN/AppStorePaymentManager/SendAppStoreReceiptOperation.swift
@@ -0,0 +1,86 @@
+//
+// SendAppStoreReceiptOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 29/03/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+
+class SendAppStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse, AppStorePaymentManager.Error> {
+ private let restClient: REST.Client
+ private let accountToken: String
+ private let forceRefresh: Bool
+ private let receiptProperties: [String: Any]?
+ private var fetchReceiptTask: Cancellable?
+ private var submitReceiptTask: Cancellable?
+
+ private let logger = Logger(label: "AppStorePaymentManager.SendAppStoreReceiptOperation")
+
+ init(restClient: REST.Client, accountToken: String, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping CompletionHandler) {
+ self.restClient = restClient
+ self.accountToken = accountToken
+ self.forceRefresh = forceRefresh
+ self.receiptProperties = receiptProperties
+
+ super.init(completionQueue: .main, completionHandler: completionHandler)
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ DispatchQueue.main.async {
+ self.fetchReceiptTask?.cancel()
+ self.fetchReceiptTask = nil
+
+ self.submitReceiptTask?.cancel()
+ self.submitReceiptTask = nil
+ }
+ }
+
+ override func main() {
+ DispatchQueue.main.async {
+ guard !self.isCancelled else {
+ self.finish(completion: .cancelled)
+ return
+ }
+
+ self.fetchReceiptTask = AppStoreReceipt.fetch(forceRefresh: self.forceRefresh, receiptProperties: self.receiptProperties) { completion in
+ switch completion {
+ case .success(let receiptData):
+ self.sendReceipt(receiptData)
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to fetch the AppStore receipt.")
+ self.finish(completion: .failure(.readReceipt(error)))
+
+ case .cancelled:
+ self.finish(completion: .cancelled)
+ }
+ }
+ }
+ }
+
+ private func sendReceipt(_ receiptData: Data) {
+ submitReceiptTask = restClient.createApplePayment(
+ token: self.accountToken,
+ receiptString: receiptData,
+ retryStrategy: .noRetry) { result in
+ switch result {
+ case .success(let response):
+ self.logger.info("AppStore receipt was processed. Time added: \(response.timeAdded), New expiry: \(response.newExpiry.logFormatDate())")
+ self.finish(completion: .success(response))
+
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to send the AppStore receipt.")
+ self.finish(completion: .failure(.sendReceipt(error)))
+
+ case .cancelled:
+ self.logger.debug("Receipt submission cancelled.")
+ self.finish(completion: .cancelled)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/AppStoreReceipt.swift b/ios/MullvadVPN/AppStoreReceipt.swift
index ff61ffd281..849ef7ed0c 100644
--- a/ios/MullvadVPN/AppStoreReceipt.swift
+++ b/ios/MullvadVPN/AppStoreReceipt.swift
@@ -67,19 +67,18 @@ enum AppStoreReceipt {
}
}
-fileprivate class FetchAppStoreReceiptOperation: AsyncOperation, SKRequestDelegate {
+fileprivate class FetchAppStoreReceiptOperation: ResultOperation<Data, AppStoreReceipt.Error>, SKRequestDelegate {
+ private let dispatchQueue: DispatchQueue
private var request: SKReceiptRefreshRequest?
private let receiptProperties: [String: Any]?
private let forceRefresh: Bool
- private let dispatchQueue: DispatchQueue
- private var completionHandler: ((OperationCompletion<Data, AppStoreReceipt.Error>) -> Void)?
-
- init(dispatchQueue: DispatchQueue, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping (OperationCompletion<Data, AppStoreReceipt.Error>) -> Void) {
+ init(dispatchQueue: DispatchQueue, forceRefresh: Bool, receiptProperties: [String: Any]?, completionHandler: @escaping (Completion) -> Void) {
self.dispatchQueue = dispatchQueue
self.forceRefresh = forceRefresh
self.receiptProperties = receiptProperties
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: .main, completionHandler: completionHandler)
}
override func main() {
@@ -172,15 +171,4 @@ fileprivate class FetchAppStoreReceiptOperation: AsyncOperation, SKRequestDelega
}
}
- private func finish(completion: OperationCompletion<Data, AppStoreReceipt.Error>) {
- let block = completionHandler
- completionHandler = nil
-
- DispatchQueue.main.async {
- block?(completion)
- }
-
- finish()
- }
-
}
diff --git a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift
index 953fc28dab..ea1269c49b 100644
--- a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift
+++ b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift
@@ -9,9 +9,8 @@
import Foundation
import StoreKit
-class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate {
+class ProductsRequestOperation: ResultOperation<SKProductsResponse, Error>, SKProductsRequestDelegate {
private let productIdentifiers: Set<String>
- private var completionHandler: ((OperationCompletion<SKProductsResponse, Error>) -> Void)?
private let maxRetryCount = 10
private let retryDelay: DispatchTimeInterval = .seconds(2)
@@ -20,11 +19,10 @@ class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate {
private var retryTimer: DispatchSourceTimer?
private var request: SKProductsRequest?
- init(productIdentifiers: Set<String>, completionHandler: @escaping (OperationCompletion<SKProductsResponse, Error>) -> Void) {
+ init(productIdentifiers: Set<String>, completionHandler: @escaping CompletionHandler) {
self.productIdentifiers = productIdentifiers
- self.completionHandler = completionHandler
- super.init()
+ super.init(completionQueue: .main, completionHandler: completionHandler)
}
override func main() {
@@ -65,9 +63,7 @@ class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate {
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
- DispatchQueue.main.async {
- self.finish(completion: .success(response))
- }
+ finish(completion: .success(response))
}
// MARK: - Private
@@ -92,13 +88,4 @@ class ProductsRequestOperation: AsyncOperation, SKProductsRequestDelegate {
retryTimer?.schedule(wallDeadline: .now() + self.retryDelay)
retryTimer?.activate()
}
-
- private func finish(completion: OperationCompletion<SKProductsResponse, Error>) {
- assert(Thread.isMainThread)
-
- completionHandler?(completion)
- completionHandler = nil
-
- finish()
- }
}
diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift
index e7370e76cf..b988d34cdb 100644
--- a/ios/MullvadVPN/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/ProblemReportViewController.swift
@@ -568,8 +568,8 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
navigationItem.setHidesBackButton(true, animated: true)
}
- private func didSendProblemReport(viewModel: ViewModel, result: Result<(), REST.Error>) {
- switch result {
+ private func didSendProblemReport(viewModel: ViewModel, completion: OperationCompletion<(), REST.Error>) {
+ switch completion {
case .success:
submissionOverlayView.state = .sent(viewModel.email)
@@ -578,6 +578,9 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
case .failure(let error):
submissionOverlayView.state = .failure(error)
+
+ case .cancelled:
+ submissionOverlayView.state = .failure(.network(URLError(.cancelled)))
}
navigationItem.setHidesBackButton(false, animated: true)
@@ -597,8 +600,8 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
willSendProblemReport()
- _ = REST.Client.shared.sendProblemReport(request, retryStrategy: .default) { result in
- self.didSendProblemReport(viewModel: viewModel, result: result)
+ _ = REST.Client.shared.sendProblemReport(request, retryStrategy: .default) { completion in
+ self.didSendProblemReport(viewModel: viewModel, completion: completion)
}
}
diff --git a/ios/MullvadVPN/REST/RESTClient.swift b/ios/MullvadVPN/REST/RESTClient.swift
index 0530f332e5..3d9ba2fb31 100644
--- a/ios/MullvadVPN/REST/RESTClient.swift
+++ b/ios/MullvadVPN/REST/RESTClient.swift
@@ -18,27 +18,33 @@ extension REST {
return Client(addressCacheStore: AddressCache.Store.shared)
}()
- /// URL session
+ /// URL session.
private let session: URLSession
- /// URL session delegate
+ /// URL session delegate.
private let sessionDelegate: SSLPinningURLSessionDelegate
- /// REST API hostname
+ /// REST API hostname.
private let apiHostname = "api.mullvad.net"
- /// REST API base path
+ /// REST API base path.
private let apiBasePath = "/app/v1"
- /// Network request timeout in seconds
+ /// Network request timeout in seconds.
private let networkTimeout: TimeInterval = 10
- /// Address cache store
+ /// Address cache store.
private let addressCacheStore: AddressCache.Store
- /// Operation queue used for running network requests
+ /// Operation queue used for running network requests.
private let operationQueue = OperationQueue()
+ /// Network task counter.
+ private var networkTaskCounter: UInt32 = 0
+
+ /// Lock used for internal synchronization.
+ private var nslock = NSLock()
+
/// Returns array of trusted root certificates
private static var trustedRootCertificates: [SecCertificate] {
let rootCertificate = Bundle.main.path(forResource: "le_root_cert", ofType: "cer")!
@@ -57,7 +63,7 @@ extension REST {
// MARK: - Public
- func createAccount(retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<AccountResponse, REST.Error>) -> Void) -> Cancellable {
+ func createAccount(retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<AccountResponse, REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "create-account", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
let request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "accounts")
@@ -79,7 +85,7 @@ extension REST {
}
}
- func getAddressList(retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<[AnyIPEndpoint], REST.Error>) -> Void) -> Cancellable {
+ func getAddressList(retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<[AnyIPEndpoint], REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "get-api-addrs", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
let request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .get, path: "api-addrs")
@@ -100,7 +106,7 @@ extension REST {
}
}
- func getRelays(etag: String?, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<ServerRelaysCacheResponse, REST.Error>) -> Void) -> Cancellable {
+ func getRelays(etag: String?, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<ServerRelaysCacheResponse, REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "get-relays", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .get, path: "relays")
if let etag = etag {
@@ -130,7 +136,7 @@ extension REST {
}
}
- func getAccountExpiry(token: String, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<AccountResponse, REST.Error>) -> Void) -> Cancellable {
+ func getAccountExpiry(token: String, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<AccountResponse, REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "get-account-expiry", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .get, path: "me")
@@ -153,7 +159,7 @@ extension REST {
}
}
- func getWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
+ func getWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "get-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
let urlEncodedPublicKey = publicKey.base64Key
.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
@@ -179,7 +185,7 @@ extension REST {
}
}
- func pushWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
+ func pushWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "push-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "wireguard-keys")
let body = PushWireguardKeyRequest(pubkey: publicKey.rawValue)
@@ -209,7 +215,7 @@ extension REST {
}
}
- func replaceWireguardKey(token: String, oldPublicKey: PublicKey, newPublicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
+ func replaceWireguardKey(token: String, oldPublicKey: PublicKey, newPublicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<WireguardAddressesResponse, REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "replace-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "replace-wireguard-key")
let body = ReplaceWireguardKeyRequest(old: oldPublicKey.rawValue, new: newPublicKey.rawValue)
@@ -239,7 +245,7 @@ extension REST {
}
}
- func deleteWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<(), REST.Error>) -> Void) -> Cancellable {
+ func deleteWireguardKey(token: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<(), REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "delete-wireguard-key", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
let urlEncodedPublicKey = publicKey.base64Key
.addingPercentEncoding(withAllowedCharacters: .alphanumerics)!
@@ -266,7 +272,7 @@ extension REST {
}
}
- func createApplePayment(token: String, receiptString: Data, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<CreateApplePaymentResponse, REST.Error>) -> Void) -> Cancellable {
+ func createApplePayment(token: String, receiptString: Data, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<CreateApplePaymentResponse, REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "create-apple-payment", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "create-apple-payment")
let body = CreateApplePaymentRequest(receiptString: receiptString)
@@ -302,7 +308,7 @@ extension REST {
}
}
- func sendProblemReport(_ body: ProblemReportRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (Result<(), REST.Error>) -> Void) -> Cancellable {
+ func sendProblemReport(_ body: ProblemReportRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping (OperationCompletion<(), REST.Error>) -> Void) -> Cancellable {
return scheduleOperation(name: "send-problem-report", retryStrategy: retryStrategy, completionHandler: completionHandler) { endpoint, finishOperation in
var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "problem-report")
@@ -330,8 +336,19 @@ extension REST {
// MARK: - Private
+ private func nextTaskIdentifier() -> UInt32 {
+ nslock.lock()
+ let (partialValue, isOverflow) = networkTaskCounter.addingReportingOverflow(1)
+ let nextValue = isOverflow ? 1 : partialValue
+ networkTaskCounter = nextValue
+ nslock.unlock()
+
+ return nextValue
+ }
+
private func scheduleOperation<Response>(name: String, retryStrategy: REST.RetryStrategy, completionHandler: @escaping NetworkOperation<Response>.CompletionHandler, taskGenerator: @escaping NetworkOperation<Response>.Generator) -> Cancellable {
let operation = NetworkOperation(
+ taskIdentifier: nextTaskIdentifier(),
name: name,
networkTaskGenerator: taskGenerator,
addressCacheStore: addressCacheStore,
diff --git a/ios/MullvadVPN/REST/RESTNetworkOperation.swift b/ios/MullvadVPN/REST/RESTNetworkOperation.swift
index 9231837f50..cc20c526b0 100644
--- a/ios/MullvadVPN/REST/RESTNetworkOperation.swift
+++ b/ios/MullvadVPN/REST/RESTNetworkOperation.swift
@@ -22,13 +22,11 @@ extension REST {
case failImmediately
}
- class NetworkOperation<Success>: AsyncOperation {
- typealias CompletionHandler = (Result<Success, REST.Error>) -> Void
- typealias Generator = (AnyIPEndpoint, @escaping CompletionHandler) -> Result<URLSessionTask, REST.Error>
+ class NetworkOperation<Success>: ResultOperation<Success, REST.Error> {
+ typealias Generator = (AnyIPEndpoint, @escaping (Result<Success, REST.Error>) -> Void) -> Result<URLSessionTask, REST.Error>
private let networkTaskGenerator: Generator
private let addressCacheStore: AddressCache.Store
- private var completionHandler: CompletionHandler?
private var sessionTask: URLSessionTask?
private let retryStrategy: RetryStrategy
@@ -38,13 +36,14 @@ extension REST {
private let logger = Logger(label: "REST.NetworkOperation")
private let loggerMetadata: Logger.Metadata
- init(name: String, networkTaskGenerator: @escaping Generator, addressCacheStore: AddressCache.Store, retryStrategy: RetryStrategy, completionHandler: @escaping CompletionHandler) {
+ init(taskIdentifier: UInt32, name: String, networkTaskGenerator: @escaping Generator, addressCacheStore: AddressCache.Store, retryStrategy: RetryStrategy, completionHandler: @escaping CompletionHandler) {
self.networkTaskGenerator = networkTaskGenerator
self.addressCacheStore = addressCacheStore
self.retryStrategy = retryStrategy
- self.completionHandler = completionHandler
- loggerMetadata = ["requestID": .string(UUID().uuidString), "name": .string(name)]
+ loggerMetadata = ["taskIdentifier": .stringConvertible(taskIdentifier), "name": .string(name)]
+
+ super.init(completionQueue: .main, completionHandler: completionHandler)
}
override func cancel() {
@@ -63,15 +62,15 @@ extension REST {
DispatchQueue.main.async {
// Finish immediately if operation was cancelled before execution
guard !self.isCancelled else {
- self.finish(with: .failure(Self.cancellationError))
+ self.finish(completion: .cancelled)
return
}
// Get current endpoint
self.addressCacheStore.getCurrentEndpoint { endpoint in
DispatchQueue.main.async {
- self.sendRequest(endpoint: endpoint) { [weak self] result in
- self?.finish(with: result)
+ self.sendRequest(endpoint: endpoint) { [weak self] completion in
+ self?.finish(completion: completion)
}
}
}
@@ -81,7 +80,7 @@ extension REST {
private func sendRequest(endpoint: AnyIPEndpoint, completionHandler: @escaping CompletionHandler) {
// Handle operation cancellation
guard !isCancelled else {
- completionHandler(.failure(Self.cancellationError))
+ completionHandler(.cancelled)
return
}
@@ -108,7 +107,7 @@ extension REST {
private func handleResponse(endpoint: AnyIPEndpoint, result: Result<Success, REST.Error>, completionHandler: @escaping CompletionHandler) {
guard case .failure(let error) = result else {
- completionHandler(result)
+ completionHandler(OperationCompletion(result: result))
return
}
@@ -129,14 +128,14 @@ extension REST {
case .failImmediately:
// Fail immediately in case of other errors, like server errors
- completionHandler(result)
+ completionHandler(OperationCompletion(result: result))
}
}
private func retryRequest(endpoint: AnyIPEndpoint, previousResult: Result<Success, REST.Error>, completionHandler: @escaping CompletionHandler) {
// Handle operation cancellation
guard !isCancelled else {
- completionHandler(.failure(Self.cancellationError))
+ completionHandler(.cancelled)
return
}
@@ -147,7 +146,7 @@ extension REST {
guard retryCount < retryStrategy.maxRetryCount else {
logger.debug("Ran out of retry attempts (\(retryStrategy.maxRetryCount))", metadata: loggerMetadata)
- completionHandler(previousResult)
+ completionHandler(OperationCompletion(result: previousResult))
return
}
@@ -165,20 +164,13 @@ extension REST {
}
retryTimer?.setCancelHandler {
- completionHandler(.failure(Self.cancellationError))
+ completionHandler(.cancelled)
}
retryTimer?.schedule(wallDeadline: .now() + retryStrategy.retryDelay)
retryTimer?.activate()
}
- private func finish(with result: Result<Success, REST.Error>) {
- completionHandler?(result)
- completionHandler = nil
-
- finish()
- }
-
private static func evaluateError(_ error: REST.Error) -> RetryAction {
guard case .network(let networkError) = error else {
return .failImmediately
@@ -195,10 +187,6 @@ extension REST {
return .useNextEndpoint
}
}
-
- private static var cancellationError: REST.Error {
- return .network(URLError(.cancelled))
- }
}
}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
index b5e9d87a44..89ebe2bda2 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -303,9 +303,8 @@ extension RelayCache.Tracker {
}
}
-fileprivate class UpdateRelaysOperation: AsyncOperation {
+fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult, RelayCache.Error> {
typealias UpdateHandler = (RelayCache.CachedRelays) -> Void
- typealias CompletionHandler = (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void
private let dispatchQueue: DispatchQueue
private let restClient: REST.Client
@@ -315,7 +314,6 @@ fileprivate class UpdateRelaysOperation: AsyncOperation {
private let logger = Logger(label: "RelayCacheTracker.UpdateRelaysOperation")
private let updateHandler: UpdateHandler
- private var completionHandler: CompletionHandler?
private var downloadCancellable: Cancellable?
init(dispatchQueue: DispatchQueue,
@@ -329,7 +327,8 @@ fileprivate class UpdateRelaysOperation: AsyncOperation {
self.cacheFileURL = cacheFileURL
self.relayUpdateInterval = relayUpdateInterval
self.updateHandler = updateHandler
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: dispatchQueue, completionHandler: completionHandler)
}
override func main() {
@@ -370,15 +369,6 @@ fileprivate class UpdateRelaysOperation: AsyncOperation {
}
}
- private func finish(completion: OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) {
- let block = completionHandler
- completionHandler = nil
-
- block?(completion)
-
- finish()
- }
-
private func didReceiveNewRelays(etag: String?, relays: REST.ServerRelaysResponse) {
let numRelays = relays.wireguard.relays.count
@@ -439,6 +429,10 @@ fileprivate class UpdateRelaysOperation: AsyncOperation {
case .failure(let error):
self.didFailToDownloadRelays(error: error)
+
+ case .cancelled:
+ self.logger.debug("Cancelled relays download.")
+ self.finish(completion: .cancelled)
}
}
}
diff --git a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
index 0727d961cd..40aad31c70 100644
--- a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
+++ b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
@@ -8,17 +8,15 @@
import UIKit
-extension AddressCache.CacheUpdateResult {
+extension OperationCompletion where Success == AddressCache.CacheUpdateResult {
var backgroundFetchResult: UIBackgroundFetchResult {
switch self {
- case .failure:
- return .failed
- case .throttled:
- return .noData
- case .success:
+ case .success(.finished):
return .newData
- case .cancelled:
+ case .success(.throttled), .cancelled:
return .noData
+ case .failure:
+ return .failed
}
}
}
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
index c634b679e6..9a81bf3045 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
@@ -20,9 +20,8 @@ extension TunnelIPC {
var timeout: TimeInterval = 5
}
- final class RequestOperation<Output>: AsyncOperation {
+ final class RequestOperation<Output>: ResultOperation<Output, TunnelIPC.Error> {
typealias DecoderHandler = (Data?) -> Result<Output, TunnelIPC.Error>
- typealias CompletionHandler = (OperationCompletion<Output, TunnelIPC.Error>) -> Void
private let queue: DispatchQueue
@@ -31,7 +30,6 @@ extension TunnelIPC {
private let options: RequestOptions
private let decoderHandler: DecoderHandler
- private var completionHandler: CompletionHandler?
private var statusObserver: Tunnel.StatusBlockObserver?
private var timeoutWork: DispatchWorkItem?
@@ -53,12 +51,24 @@ extension TunnelIPC {
self.options = options
self.decoderHandler = decoderHandler
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
- self.execute()
+ guard !self.isCancelled else {
+ self.finish(completion: .cancelled)
+ return
+ }
+
+ self.setTimeoutTimer(connectingStateWaitDelay: 0)
+
+ self.statusObserver = self.tunnel.addBlockObserver(queue: self.queue) { [weak self] tunnel, status in
+ self?.handleVPNStatus(status)
+ }
+
+ self.handleVPNStatus(self.tunnel.status)
}
}
@@ -67,24 +77,21 @@ extension TunnelIPC {
queue.async {
if self.isExecuting {
- self.completeOperation(completion: .cancelled)
+ self.finish(completion: .cancelled)
}
}
}
- private func execute() {
- guard !isCancelled else {
- completeOperation(completion: .cancelled)
- return
- }
-
- setTimeoutTimer(connectingStateWaitDelay: 0)
+ override func finish(completion: Completion) {
+ // Release status observer.
+ removeVPNStatusObserver()
- statusObserver = tunnel.addBlockObserver(queue: queue) { [weak self] tunnel, status in
- self?.handleVPNStatus(status)
- }
+ // Cancel pending work.
+ timeoutWork?.cancel()
+ waitForConnectingStateWork?.cancel()
- handleVPNStatus(tunnel.status)
+ // Finish operation.
+ super.finish(completion: completion)
}
private func removeVPNStatusObserver() {
@@ -94,7 +101,7 @@ extension TunnelIPC {
private func setTimeoutTimer(connectingStateWaitDelay: TimeInterval) {
let workItem = DispatchWorkItem { [weak self] in
- self?.completeOperation(completion: .failure(.send(.timeout)))
+ self?.finish(completion: .failure(.send(.timeout)))
}
// Cancel pending timeout work.
@@ -127,7 +134,7 @@ extension TunnelIPC {
sendRequest()
case .invalid, .disconnecting, .disconnected:
- completeOperation(completion: .failure(.send(.tunnelDown(status))))
+ finish(completion: .failure(.send(.tunnelDown(status))))
@unknown default:
break
@@ -183,7 +190,7 @@ extension TunnelIPC {
do {
messageData = try TunnelIPC.Coding.encodeRequest(request)
} catch {
- completeOperation(completion: .failure(.encoding(error)))
+ finish(completion: .failure(.encoding(error)))
return
}
@@ -195,29 +202,13 @@ extension TunnelIPC {
self.queue.async {
let decodingResult = self.decoderHandler(responseData)
- self.completeOperation(completion: OperationCompletion(result: decodingResult))
+ self.finish(completion: OperationCompletion(result: decodingResult))
}
}
} catch {
- completeOperation(completion: .failure(.send(.system(error))))
+ finish(completion: .failure(.send(.system(error))))
}
}
-
- private func completeOperation(completion: OperationCompletion<Output, TunnelIPC.Error>) {
- // Release status observer.
- removeVPNStatusObserver()
-
- // Cancel pending work.
- timeoutWork?.cancel()
- waitForConnectingStateWork?.cancel()
-
- // Call completion handler.
- completionHandler?(completion)
- completionHandler = nil
-
- // Finish operation.
- finish()
- }
}
}
diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
index fc393b7494..83946f02bf 100644
--- a/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
@@ -9,13 +9,10 @@
import Foundation
import Logging
-class LoadTunnelOperation: AsyncOperation {
- typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
-
+class LoadTunnelOperation: ResultOperation<(), TunnelManager.Error> {
private let queue: DispatchQueue
private let accountToken: String?
private let state: TunnelManager.State
- private var completionHandler: CompletionHandler?
private let logger = Logger(label: "TunnelManager.LoadTunnelOperation")
@@ -23,16 +20,14 @@ class LoadTunnelOperation: AsyncOperation {
self.queue = queue
self.state = state
self.accountToken = accountToken
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
self.execute { completion in
- self.completionHandler?(completion)
- self.completionHandler = nil
-
- self.finish()
+ self.finish(completion: completion)
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
index 090a29f59f..48308b7e77 100644
--- a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
@@ -8,39 +8,37 @@
import Foundation
-class ReloadTunnelOperation: AsyncOperation {
- typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
-
+class ReloadTunnelOperation: ResultOperation<(), TunnelManager.Error> {
private let queue: DispatchQueue
private let state: TunnelManager.State
- private var request: Cancellable?
- private var completionHandler: CompletionHandler?
+ private var cancellableTask: Cancellable?
init(queue: DispatchQueue, state: TunnelManager.State, completionHandler: @escaping CompletionHandler) {
self.queue = queue
self.state = state
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
guard !self.isCancelled else {
- self.completeOperation(completion: .cancelled)
+ self.finish(completion: .cancelled)
return
}
guard let tunnel = self.state.tunnel else {
- self.completeOperation(completion: .failure(.unsetAccount))
+ self.finish(completion: .failure(.unsetAccount))
return
}
let session = TunnelIPC.Session(tunnel: tunnel)
- self.request = session.reloadTunnelSettings { [weak self] completion in
+ self.cancellableTask = session.reloadTunnelSettings { [weak self] completion in
guard let self = self else { return }
self.queue.async {
- self.completeOperation(completion: completion.mapError { .reloadTunnel($0) })
+ self.finish(completion: completion.mapError { .reloadTunnel($0) })
}
}
}
@@ -50,15 +48,8 @@ class ReloadTunnelOperation: AsyncOperation {
super.cancel()
queue.async {
- self.request?.cancel()
+ self.cancellableTask?.cancel()
+ self.cancellableTask = nil
}
}
-
- private func completeOperation(completion: OperationCompletion<(), TunnelManager.Error>) {
- completionHandler?(completion)
- completionHandler = nil
-
- finish()
- }
-
}
diff --git a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
index 7d610c5e6f..ceca68b708 100644
--- a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
@@ -9,9 +9,7 @@
import Foundation
import Logging
-class ReplaceKeyOperation: AsyncOperation {
- typealias CompletionHandler = (OperationCompletion<TunnelManager.KeyRotationResult, TunnelManager.Error>) -> Void
-
+class ReplaceKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, TunnelManager.Error> {
private let queue: DispatchQueue
private let state: TunnelManager.State
@@ -77,16 +75,13 @@ class ReplaceKeyOperation: AsyncOperation {
self.restClient = restClient
self.rotationInterval = rotationInterval
- self.completionHandler = completionHandler
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
self.execute { completion in
- self.completionHandler?(completion)
- self.completionHandler = nil
-
- self.finish()
+ self.finish(completion: completion)
}
}
}
@@ -163,10 +158,10 @@ class ReplaceKeyOperation: AsyncOperation {
oldPublicKey: oldPublicKey,
newPublicKey: newPrivateKey.publicKey,
retryStrategy: .default
- ) { result in
+ ) { completion in
self.queue.async {
self.didReceiveResponse(
- result: result,
+ completion: completion,
accountToken: tunnelInfo.token,
newPrivateKey: newPrivateKey,
completionHandler: completionHandler
@@ -175,8 +170,8 @@ class ReplaceKeyOperation: AsyncOperation {
}
}
- private func didReceiveResponse(result: Result<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) {
- switch result {
+ private func didReceiveResponse(completion: OperationCompletion<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) {
+ switch completion {
case .success(let associatedAddresses):
logger.debug("Replaced old key with new key on server.")
@@ -208,6 +203,11 @@ class ReplaceKeyOperation: AsyncOperation {
logger.error(chainedError: restError, message: "Failed to replace old key with new key on server.")
completionHandler(.failure(.replaceWireguardKey(restError)))
+
+ case .cancelled:
+ logger.debug("Cancelled replace key request.")
+
+ completionHandler(.cancelled)
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 90b7cd3779..88f7fde065 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -10,9 +10,8 @@ import Foundation
import class WireGuardKitTypes.PublicKey
import Logging
-class SetAccountOperation: AsyncOperation {
+class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
typealias WillDeleteVPNConfigurationHandler = () -> Void
- typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
private let queue: DispatchQueue
private let state: TunnelManager.State
@@ -20,8 +19,6 @@ class SetAccountOperation: AsyncOperation {
private let accountToken: String?
private var willDeleteVPNConfigurationHandler: WillDeleteVPNConfigurationHandler?
- private var completionHandler: CompletionHandler?
-
private let logger = Logger(label: "TunnelManager.SetAccountOperation")
init(queue: DispatchQueue, state: TunnelManager.State, restClient: REST.Client, accountToken: String?, willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler, completionHandler: @escaping CompletionHandler) {
@@ -30,16 +27,14 @@ class SetAccountOperation: AsyncOperation {
self.restClient = restClient
self.accountToken = accountToken
self.willDeleteVPNConfigurationHandler = willDeleteVPNConfigurationHandler
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
self.execute { completion in
- self.completionHandler?(completion)
- self.completionHandler = nil
-
- self.finish()
+ self.finish(completion: completion)
}
}
}
@@ -160,6 +155,9 @@ class SetAccountOperation: AsyncOperation {
case .failure(let error):
self.logger.error(chainedError: error, message: "Failed to delete key (\(index)) on server.")
+
+ case .cancelled:
+ self.logger.debug("Cancelled public key deletion.")
}
dispatchGroup.leave()
@@ -231,6 +229,11 @@ class SetAccountOperation: AsyncOperation {
self.logger.error(chainedError: error, message: "Failed to push new key to server.")
completionHandler(.failure(.pushWireguardKey(error)))
+
+ case .cancelled:
+ self.logger.debug("Cancelled new key push to server.")
+
+ completionHandler(.cancelled)
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift
index 86e6c1a4ff..e818474f60 100644
--- a/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift
@@ -8,57 +8,45 @@
import Foundation
-class SetTunnelSettingsOperation: AsyncOperation {
+class SetTunnelSettingsOperation: ResultOperation<(), TunnelManager.Error> {
typealias ModificationHandler = (inout TunnelSettings) -> Void
- typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
private let queue: DispatchQueue
private let state: TunnelManager.State
private let modificationBlock: ModificationHandler
- private var completionHandler: CompletionHandler?
init(queue: DispatchQueue, state: TunnelManager.State, modificationBlock: @escaping ModificationHandler, completionHandler: @escaping CompletionHandler) {
self.queue = queue
self.state = state
self.modificationBlock = modificationBlock
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
- self.execute { completion in
- self.completionHandler?(completion)
- self.completionHandler = nil
-
- self.finish()
+ guard !self.isCancelled else {
+ self.finish(completion: .cancelled)
+ return
}
- }
- }
- private func execute(completionHandler: CompletionHandler) {
- guard !isCancelled else {
- completionHandler(.cancelled)
- return
- }
-
- guard let accountToken = state.tunnelInfo?.token else {
- completionHandler(.failure(.unsetAccount))
- return
- }
-
- let result = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in
- self.modificationBlock(&tunnelSettings)
- }
-
- switch result {
- case .success(let newTunnelSettings):
- state.tunnelInfo?.tunnelSettings = newTunnelSettings
+ guard let accountToken = self.state.tunnelInfo?.token else {
+ self.finish(completion: .failure(.unsetAccount))
+ return
+ }
- completionHandler(.success(()))
+ let result = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in
+ self.modificationBlock(&tunnelSettings)
+ }
- case .failure(let error):
- completionHandler(.failure(.updateTunnelSettings(error)))
+ switch result {
+ case .success(let newTunnelSettings):
+ self.state.tunnelInfo?.tunnelSettings = newTunnelSettings
+ self.finish(completion: .success(()))
+ case .failure(let error):
+ self.finish(completion: .failure(.updateTunnelSettings(error)))
+ }
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index ac8223f963..43bcb9241b 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -9,82 +9,71 @@
import Foundation
import NetworkExtension
-class StartTunnelOperation: AsyncOperation {
+class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
typealias EncodeErrorHandler = (Error) -> Void
- typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
private let queue: DispatchQueue
private let state: TunnelManager.State
private var encodeErrorHandler: EncodeErrorHandler?
- private var completionHandler: CompletionHandler?
init(queue: DispatchQueue, state: TunnelManager.State, encodeErrorHandler: @escaping EncodeErrorHandler, completionHandler: @escaping CompletionHandler) {
self.queue = queue
self.state = state
self.encodeErrorHandler = encodeErrorHandler
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
- self.execute { completion in
- self.completionHandler?(completion)
- self.completionHandler = nil
-
- self.finish()
+ guard !self.isCancelled else {
+ self.finish(completion: .cancelled)
+ return
}
- }
- }
-
- private func execute(completionHandler: @escaping CompletionHandler) {
- guard !self.isCancelled else {
- completionHandler(.cancelled)
- return
- }
- guard let tunnelInfo = self.state.tunnelInfo else {
- completionHandler(.failure(.unsetAccount))
- return
- }
+ guard let tunnelInfo = self.state.tunnelInfo else {
+ self.finish(completion: .failure(.unsetAccount))
+ return
+ }
- switch self.state.tunnelStatus.state {
- case .disconnecting(.nothing):
- self.state.tunnelStatus.state = .disconnecting(.reconnect)
+ switch self.state.tunnelStatus.state {
+ case .disconnecting(.nothing):
+ self.state.tunnelStatus.state = .disconnecting(.reconnect)
- completionHandler(.success(()))
+ self.finish(completion: .success(()))
- case .disconnected, .pendingReconnect:
- RelayCache.Tracker.shared.read { readResult in
- self.queue.async {
- switch readResult {
- case .success(let cachedRelays):
- self.didReceiveRelays(
- tunnelInfo: tunnelInfo,
- cachedRelays: cachedRelays,
- completionHandler: completionHandler
- )
+ case .disconnected, .pendingReconnect:
+ RelayCache.Tracker.shared.read { readResult in
+ self.queue.async {
+ switch readResult {
+ case .success(let cachedRelays):
+ self.didReceiveRelays(
+ tunnelInfo: tunnelInfo,
+ cachedRelays: cachedRelays
+ )
- case .failure(let error):
- completionHandler(.failure(.readRelays(error)))
+ case .failure(let error):
+ self.finish(completion: .failure(.readRelays(error)))
+ }
}
}
- }
- default:
- // Do not attempt to start the tunnel in all other cases.
- completionHandler(.success(()))
+ default:
+ // Do not attempt to start the tunnel in all other cases.
+ self.finish(completion: .success(()))
+ }
}
}
- private func didReceiveRelays(tunnelInfo: TunnelInfo, cachedRelays: RelayCache.CachedRelays, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) {
+ private func didReceiveRelays(tunnelInfo: TunnelInfo, cachedRelays: RelayCache.CachedRelays) {
let selectorResult = RelaySelector.evaluate(
relays: cachedRelays.relays,
constraints: tunnelInfo.tunnelSettings.relayConstraints
)
guard let selectorResult = selectorResult else {
- completionHandler(.failure(.cannotSatisfyRelayConstraints))
+ finish(completion: .failure(.cannotSatisfyRelayConstraints))
return
}
@@ -94,10 +83,10 @@ class StartTunnelOperation: AsyncOperation {
case .success(let tunnelProvider):
let startTunnelResult = Result { try self.startTunnel(tunnelProvider: tunnelProvider, selectorResult: selectorResult) }
- completionHandler(OperationCompletion(result: startTunnelResult.mapError { .startVPNTunnel($0) }))
+ self.finish(completion: OperationCompletion(result: startTunnelResult.mapError { .startVPNTunnel($0) }))
case .failure(let error):
- completionHandler(.failure(error))
+ self.finish(completion: .failure(error))
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
index 377d946399..39b6833716 100644
--- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
@@ -8,64 +8,53 @@
import Foundation
-class StopTunnelOperation: AsyncOperation {
- typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
-
+class StopTunnelOperation: ResultOperation<(), TunnelManager.Error> {
private let queue: DispatchQueue
private let state: TunnelManager.State
- private var completionHandler: CompletionHandler?
init(queue: DispatchQueue, state: TunnelManager.State, completionHandler: @escaping CompletionHandler) {
self.queue = queue
self.state = state
- self.completionHandler = completionHandler
+
+ super.init(completionQueue: queue, completionHandler: completionHandler)
}
override func main() {
queue.async {
- self.execute { completion in
- self.completionHandler?(completion)
- self.completionHandler = nil
-
- self.finish()
+ guard !self.isCancelled else {
+ self.finish(completion: .cancelled)
+ return
}
- }
- }
- private func execute(completionHandler: @escaping CompletionHandler) {
- guard !isCancelled else {
- completionHandler(.cancelled)
- return
- }
-
- guard let tunnel = state.tunnel else {
- completionHandler(.failure(.unsetAccount))
- return
- }
+ guard let tunnel = self.state.tunnel else {
+ self.finish(completion: .failure(.unsetAccount))
+ return
+ }
- switch self.state.tunnelStatus.state {
- case .disconnecting(.reconnect):
- state.tunnelStatus.state = .disconnecting(.nothing)
+ switch self.state.tunnelStatus.state {
+ case .disconnecting(.reconnect):
+ self.state.tunnelStatus.state = .disconnecting(.nothing)
- completionHandler(.success(()))
+ self.finish(completion: .success(()))
- case .connected, .connecting, .reconnecting:
- // Disable on-demand when stopping the tunnel to prevent it from coming back up
- tunnel.isOnDemandEnabled = false
+ case .connected, .connecting, .reconnecting:
+ // Disable on-demand when stopping the tunnel to prevent it from coming back up
+ tunnel.isOnDemandEnabled = false
- tunnel.saveToPreferences { error in
- self.queue.async {
- if let error = error {
- completionHandler(.failure(.saveVPNConfiguration(error)))
- } else {
- tunnel.stop()
- completionHandler(.success(()))
+ tunnel.saveToPreferences { error in
+ self.queue.async {
+ if let error = error {
+ self.finish(completion: .failure(.saveVPNConfiguration(error)))
+ } else {
+ tunnel.stop()
+ self.finish(completion: .success(()))
+ }
}
}
- }
- default:
- completionHandler(.success(()))
+ default:
+ self.finish(completion: .success(()))
+ }
}
}
}
diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift
index d777c8c8d3..1c839a0c54 100644
--- a/ios/MullvadVPN/WireguardKeysViewController.swift
+++ b/ios/MullvadVPN/WireguardKeysViewController.swift
@@ -209,7 +209,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
private func verifyKey() {
guard let tunnelInfo = TunnelManager.shared.tunnelInfo else { return }
- self.updateViewState(.verifyingKey)
+ updateViewState(.verifyingKey)
verifyKeyCancellable?.cancel()
@@ -231,6 +231,9 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
self.showKeyVerificationFailureAlert(error)
self.updateViewState(.default)
}
+
+ case .cancelled:
+ break
}
}
}