diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-04-05 09:13:46 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-04-05 09:13:46 +0200 |
| commit | f758f894da5e368e54c83bd59013074192ce12be (patch) | |
| tree | 172961804441441ac6800d0c463b1e99feec5912 | |
| parent | 0f313bf16aae17e43981ad9716d9c73c780ea8db (diff) | |
| parent | 25ed6a8ea14ab9ef2442abb08d4ef2de0a5de8a9 (diff) | |
| download | mullvadvpn-f758f894da5e368e54c83bd59013074192ce12be.tar.xz mullvadvpn-f758f894da5e368e54c83bd59013074192ce12be.zip | |
Merge branch 'result-operation-subclass'
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 } } } |
