diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-05-31 14:05:45 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-06-08 12:31:16 +0200 |
| commit | 2c8cb8005d1ff97bccdc08f9e3be7657629b205c (patch) | |
| tree | e37ae78b1ceb62746e65532e7a2eaeda8bd252ba | |
| parent | 4486a98b4e974273f89c1de7d2654773bf091956 (diff) | |
| download | mullvadvpn-2c8cb8005d1ff97bccdc08f9e3be7657629b205c.tar.xz mullvadvpn-2c8cb8005d1ff97bccdc08f9e3be7657629b205c.zip | |
Add custom operation queue, conditions and observers
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 18 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/AsyncOperation.swift | 266 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/AsyncOperationQueue.swift | 101 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/OperationCondition.swift | 107 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/OperationObserver.swift | 63 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/ResultOperation+Fallible.swift | 15 |
6 files changed, 548 insertions, 22 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index fad611075f..61d4e38245 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */; }; + 58059DE228468255002B1049 /* ResultOperation+Fallible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DE128468255002B1049 /* ResultOperation+Fallible.swift */; }; 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; }; 5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 5807483A27DB8A980020ECBF /* WireGuardKitTypes */; }; 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; @@ -70,6 +71,7 @@ 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; 5838318B27C40A3900000571 /* Pinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838318A27C40A3900000571 /* Pinger.swift */; }; 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; }; + 583E1E1B2848DE1C004838B3 /* ResultOperation+Fallible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DE128468255002B1049 /* ResultOperation+Fallible.swift */; }; 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; 5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; @@ -194,6 +196,9 @@ 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; }; 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */; }; + 589D287A2846250500F9A7B3 /* OperationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28772846250500F9A7B3 /* OperationCondition.swift */; }; + 589D287B2846250500F9A7B3 /* AsyncOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */; }; + 589D287C2846250500F9A7B3 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28792846250500F9A7B3 /* OperationObserver.swift */; }; 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A8055E2716EA6700681642 /* AnyIPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26BE270C550B004EA533 /* AnyIPAddress.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; @@ -327,6 +332,7 @@ /* Begin PBXFileReference section */ 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerState.swift; sourceTree = "<group>"; }; + 58059DE128468255002B1049 /* ResultOperation+Fallible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultOperation+Fallible.swift"; sourceTree = "<group>"; }; 5807E2BF2432038B00F5FF30 /* String+Split.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = "<group>"; }; 5807E2C1243203D000F5FF30 /* StringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; }; 5808273928487E3E006B77A4 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = "<group>"; }; @@ -463,6 +469,9 @@ 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormatting.swift; sourceTree = "<group>"; }; 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormattingTests.swift; sourceTree = "<group>"; }; 5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContentView.swift; sourceTree = "<group>"; }; + 589D28772846250500F9A7B3 /* OperationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationCondition.swift; sourceTree = "<group>"; }; + 589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncOperationQueue.swift; sourceTree = "<group>"; }; + 589D28792846250500F9A7B3 /* OperationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationObserver.swift; sourceTree = "<group>"; }; 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; 58A94AE326CFD945001CB97C /* TunnelErrorNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrorNotificationProvider.swift; sourceTree = "<group>"; }; @@ -608,11 +617,15 @@ 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */, 58E973DD24850EB600096F90 /* AsyncOperation.swift */, 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */, + 589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */, 5840BE34279EDB16002836BA /* OperationCompletion.swift */, + 589D28772846250500F9A7B3 /* OperationCondition.swift */, + 589D28792846250500F9A7B3 /* OperationObserver.swift */, 5820675D26E6839900655B05 /* PresentAlertOperation.swift */, 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */, 58F7D26427EB50A300E4D821 /* ResultOperation.swift */, 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */, + 58059DE128468255002B1049 /* ResultOperation+Fallible.swift */, ); path = Operations; sourceTree = "<group>"; @@ -1208,6 +1221,7 @@ 585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */, 5820676226E75D8500655B05 /* REST.swift in Sources */, 58A8055E2716EA6700681642 /* AnyIPAddress.swift in Sources */, + 583E1E1B2848DE1C004838B3 /* ResultOperation+Fallible.swift in Sources */, 5857F23024C843ED00CF6F47 /* ChainedError.swift in Sources */, 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */, 5846227A26E24F1F0035F7C2 /* ExclusivityController.swift in Sources */, @@ -1242,10 +1256,12 @@ 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, 58F1311327E09B00007AC5BC /* Cancellable.swift in Sources */, 587C575326D2615F005EF767 /* PacketTunnelOptions.swift in Sources */, + 589D287C2846250500F9A7B3 /* OperationObserver.swift in Sources */, 587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */, 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */, 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, + 58059DE228468255002B1049 /* ResultOperation+Fallible.swift in Sources */, 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */, 58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */, @@ -1272,6 +1288,7 @@ 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */, + 589D287A2846250500F9A7B3 /* OperationCondition.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */, 5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */, @@ -1397,6 +1414,7 @@ 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */, 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */, 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, + 589D287B2846250500F9A7B3 /* AsyncOperationQueue.swift in Sources */, 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, 588BCF24280FE43D009ADCEC /* RESTDevicesProxy.swift in Sources */, 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, diff --git a/ios/MullvadVPN/Operations/AsyncOperation.swift b/ios/MullvadVPN/Operations/AsyncOperation.swift index 898fc4b297..d3e13d2f81 100644 --- a/ios/MullvadVPN/Operations/AsyncOperation.swift +++ b/ios/MullvadVPN/Operations/AsyncOperation.swift @@ -8,28 +8,82 @@ import Foundation +@objc enum State: Int, Comparable, CustomStringConvertible { + case initialized + case pending + case evaluatingConditions + case ready + case executing + case finished + + static func < (lhs: State, rhs: State) -> Bool { + return lhs.rawValue < rhs.rawValue + } + + var description: String { + switch self { + case .initialized: + return "initialized" + case .pending: + return "pending" + case .evaluatingConditions: + return "evaluatingConditions" + case .ready: + return "ready" + case .executing: + return "executing" + case .finished: + return "finished" + } + } +} + /// A base implementation of an asynchronous operation class AsyncOperation: Operation { + private static var observerContext = 0 + /// A state lock used for manipulating the operation state flags in a thread safe fashion. private let stateLock = NSRecursiveLock() - /// Operation state flags. - private var _isExecuting = false - private var _isFinished = false + /// Operation state. + @objc private var state: State = .initialized private var _isCancelled = false + final override var isReady: Bool { + stateLock.lock() + defer { stateLock.unlock() } + + // super.isReady should turn true when all dependencies are satisfied. + guard super.isReady else { + return false + } + + // Mark operation ready when cancelled, so that operation queue could flush it faster. + guard !_isCancelled else { + return true + } + + switch state { + case .initialized, .pending, .evaluatingConditions: + return false + + case .ready, .executing, .finished: + return true + } + } + final override var isExecuting: Bool { stateLock.lock() defer { stateLock.unlock() } - return _isExecuting + return state == .executing } final override var isFinished: Bool { stateLock.lock() defer { stateLock.unlock() } - return _isFinished + return state == .finished } final override var isCancelled: Bool { @@ -43,14 +97,145 @@ class AsyncOperation: Operation { return true } + // MARK: - Observers + + private var _observers: [OperationObserver] = [] + + final var observers: [OperationObserver] { + stateLock.lock() + defer { stateLock.unlock() } + + return _observers + } + + final func addObserver(_ observer: OperationObserver) { + stateLock.lock() + assert(state < .executing) + _observers.append(observer) + stateLock.unlock() + observer.didAttach(to: self) + } + + // MARK: - Conditions + + private var _conditions: [OperationCondition] = [] + + final var conditions: [OperationCondition] { + stateLock.lock() + defer { stateLock.unlock() } + + return _conditions + } + + func addCondition(_ condition: OperationCondition) { + stateLock.lock() + assert(state < .evaluatingConditions) + _conditions.append(condition) + stateLock.unlock() + } + + private func evaluateConditions() { + guard !_conditions.isEmpty else { + setState(.ready) + return + } + + setState(.evaluatingConditions) + + var results = [Bool](repeating: false, count: _conditions.count) + let group = DispatchGroup() + + for (index, condition) in _conditions.enumerated() { + group.enter() + condition.evaluate(for: self) { [weak self] isSatisfied in + self?.dispatchQueue.async { + results[index] = isSatisfied + group.leave() + } + } + } + + group.notify(queue: dispatchQueue) { [weak self] in + self?.didEvaluateConditions(results) + } + } + + private func didEvaluateConditions(_ results: [Bool]) { + stateLock.lock() + defer { stateLock.unlock() } + + guard state < .ready else { return } + + let conditionsSatisfied = results.allSatisfy { $0 } + if !conditionsSatisfied { + cancel() + } + + setState(.ready) + } + + // MARK: - + let dispatchQueue: DispatchQueue + init(dispatchQueue: DispatchQueue? = nil) { self.dispatchQueue = dispatchQueue ?? DispatchQueue(label: "AsyncOperation.dispatchQueue") super.init() + + addObserver( + self, + forKeyPath: #keyPath(isReady), + options: [], + context: &Self.observerContext + ) + } + + deinit { + removeObserver( + self, + forKeyPath: #keyPath(isReady), + context: &Self.observerContext + ) + } + + // MARK: - KVO + + @objc class func keyPathsForValuesAffectingIsReady() -> Set<String> { + return ["state"] + } + + @objc class func keyPathsForValuesAffectingIsExecuting() -> Set<String> { + return ["state"] } + @objc class func keyPathsForValuesAffectingIsFinished() -> Set<String> { + return ["state"] + } + + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer? + ) + { + if context == &Self.observerContext { + checkReadiness() + } else { + super.observeValue( + forKeyPath: keyPath, + of: object, + change: change, + context: context + ) + } + } + + // MARK: - Lifecycle + final override func start() { - let underlyingQueue = OperationQueue.current?.underlyingQueue + let currentQueue = OperationQueue.current + let underlyingQueue = currentQueue?.underlyingQueue if underlyingQueue == dispatchQueue { _start() @@ -67,8 +252,13 @@ class AsyncOperation: Operation { stateLock.unlock() finish() } else { - setExecuting(true) + setState(.executing) + + for observer in _observers { + observer.operationDidStart(self) + } stateLock.unlock() + main() } } @@ -95,6 +285,10 @@ class AsyncOperation: Operation { if notifyDidCancel { dispatchQueue.async { self.operationDidCancel() + + for observer in self.observers { + observer.operationDidCancel(self) + } } } } @@ -103,34 +297,46 @@ class AsyncOperation: Operation { var notifyDidFinish = false stateLock.lock() - - if _isExecuting { - setExecuting(false) - } - - if !_isFinished { - willChangeValue(for: \.isFinished) - _isFinished = true - didChangeValue(for: \.isFinished) - + if state < .finished { + setState(.finished) notifyDidFinish = true } - stateLock.unlock() if notifyDidFinish { dispatchQueue.async { self.operationDidFinish() + + for observer in self.observers { + observer.operationDidFinish(self) + } } } } - private func setExecuting(_ value: Bool) { - willChangeValue(for: \.isExecuting) - _isExecuting = value - didChangeValue(for: \.isExecuting) + // MARK: - Private + + private func setState(_ newState: State) { + willChangeValue(for: \.state) + assert(state < newState) + state = newState + didChangeValue(for: \.state) + } + + private func checkReadiness() { + stateLock.lock() + if state == .pending && !_isCancelled && super.isReady { + evaluateConditions() + } + stateLock.unlock() + } + + func didEnqueue() { + setState(.pending) } + // MARK: - Subclass overrides + func operationDidCancel() { // Override in subclasses. } @@ -147,3 +353,19 @@ extension Operation { } } } + +extension Operation { + var operationName: String { + return name ?? "\(self)" + } +} + + +protocol OperationBlockObserverSupport {} +extension AsyncOperation: OperationBlockObserverSupport {} + +extension OperationBlockObserverSupport where Self: AsyncOperation { + func addBlockObserver(_ observer: OperationBlockObserver<Self>) { + addObserver(observer) + } +} diff --git a/ios/MullvadVPN/Operations/AsyncOperationQueue.swift b/ios/MullvadVPN/Operations/AsyncOperationQueue.swift new file mode 100644 index 0000000000..f89fd70841 --- /dev/null +++ b/ios/MullvadVPN/Operations/AsyncOperationQueue.swift @@ -0,0 +1,101 @@ +// +// AsyncOperationQueue.swift +// MullvadVPN +// +// Created by pronebird on 30/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class AsyncOperationQueue: OperationQueue { + override func addOperation(_ operation: Operation) { + if let operation = operation as? AsyncOperation { + let categories = operation.conditions + .filter { condition in + return condition.isMutuallyExclusive + } + .map { condition in + return condition.name + } + + if !categories.isEmpty { + ExclusivityManager.shared.addOperation(operation, categories: Set(categories)) + } + + super.addOperation(operation) + + operation.didEnqueue() + } else { + super.addOperation(operation) + } + } + + override func addOperations(_ operations: [Operation], waitUntilFinished wait: Bool) { + for operation in operations { + addOperation(operation) + } + + if wait { + let blockOperation = BlockOperation() + blockOperation.addDependencies(operations) + + addOperation(blockOperation) + blockOperation.waitUntilFinished() + } + } +} + +private final class ExclusivityManager { + static let shared = ExclusivityManager() + + private var operationsByCategory = [String: [Operation]]() + private let nslock = NSLock() + + private init() {} + + func addOperation(_ operation: AsyncOperation, categories: Set<String>) { + nslock.lock() + defer { nslock.unlock() } + + for category in categories { + var operations = operationsByCategory[category] ?? [] + + if let lastOperation = operations.last { + operation.addDependency(lastOperation) + } + + operations.append(operation) + + operationsByCategory[category] = operations + + let blockObserver = OperationBlockObserver(didFinish: { [weak self] op in + self?.removeOperation(op, categories: categories) + }) + + operation.addObserver(blockObserver) + } + } + + private func removeOperation(_ operation: Operation, categories: Set<String>) { + nslock.lock() + defer { nslock.unlock() } + + for category in categories { + guard var operations = operationsByCategory[category] else { + continue + } + + if let index = operations.firstIndex(of: operation) { + operations.remove(at: index) + } + + if operations.isEmpty { + operationsByCategory.removeValue(forKey: category) + } else { + operationsByCategory[category] = operations + } + } + } + +} diff --git a/ios/MullvadVPN/Operations/OperationCondition.swift b/ios/MullvadVPN/Operations/OperationCondition.swift new file mode 100644 index 0000000000..da3e5f56a1 --- /dev/null +++ b/ios/MullvadVPN/Operations/OperationCondition.swift @@ -0,0 +1,107 @@ +// +// OperationCondition.swift +// MullvadVPN +// +// Created by pronebird on 30/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol FallibleOperation { + var error: Error? { get } +} + +protocol OperationCondition { + var name: String { get } + var isMutuallyExclusive: Bool { get } + + func evaluate(for operation: Operation, completion: @escaping (Bool) -> Void) +} + +final class NoCancelledDependenciesCondition: OperationCondition { + var name: String { + return "NoCancelledDependenciesCondition" + } + + var isMutuallyExclusive: Bool { + return false + } + + func evaluate(for operation: Operation, completion: @escaping (Bool) -> Void) { + let satisfy = operation.dependencies.allSatisfy { operation in + return !operation.isCancelled + } + + completion(satisfy) + } +} + +final class NoFailedDependenciesCondition: OperationCondition { + var name: String { + return "NoFailedDependenciesCondition" + } + + var isMutuallyExclusive: Bool { + return false + } + + let ignoreCancellations: Bool + init(ignoreCancellations: Bool) { + self.ignoreCancellations = ignoreCancellations + } + + func evaluate(for operation: Operation, completion: @escaping (Bool) -> Void) { + let satisfy = operation.dependencies.allSatisfy { operation in + if let fallibleOperation = operation as? FallibleOperation, + fallibleOperation.error != nil { + return false + } + + if operation.isCancelled && !self.ignoreCancellations { + return false + } + + return true + } + + completion(satisfy) + } +} + +final class BlockCondition: OperationCondition { + typealias HandlerBlock = (Operation, @escaping (Bool) -> Void) -> Void + + var name: String { + return "BlockCondition" + } + + var isMutuallyExclusive: Bool { + return false + } + + let block: HandlerBlock + init(block: @escaping HandlerBlock) { + self.block = block + } + + func evaluate(for operation: Operation, completion: @escaping (Bool) -> Void) { + block(operation, completion) + } +} + +final class MutuallyExclusive: OperationCondition { + let name: String + + var isMutuallyExclusive: Bool { + return true + } + + init(category: String) { + name = "MutuallyExclusive<\(category)>" + } + + func evaluate(for operation: Operation, completion: @escaping (Bool) -> Void) { + completion(true) + } +} diff --git a/ios/MullvadVPN/Operations/OperationObserver.swift b/ios/MullvadVPN/Operations/OperationObserver.swift new file mode 100644 index 0000000000..28ce7ee348 --- /dev/null +++ b/ios/MullvadVPN/Operations/OperationObserver.swift @@ -0,0 +1,63 @@ +// +// OperationObserver.swift +// MullvadVPN +// +// Created by pronebird on 30/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol OperationObserver { + func didAttach(to operation: Operation) + func operationDidStart(_ operation: Operation) + func operationDidCancel(_ operation: Operation) + func operationDidFinish(_ operation: Operation) +} + +/// Block based operation observer. +class OperationBlockObserver<OperationType: Operation>: OperationObserver { + typealias VoidBlock = (OperationType) -> Void + + private let _didAttach: VoidBlock? + private let _didStart: VoidBlock? + private let _didCancel: VoidBlock? + private let _didFinish: VoidBlock? + + init( + didAttach: VoidBlock? = nil, + didStart: VoidBlock? = nil, + didCancel: VoidBlock? = nil, + didFinish: VoidBlock? = nil + ) + { + _didAttach = didAttach + _didStart = didStart + _didCancel = didCancel + _didFinish = didFinish + } + + func didAttach(to operation: Operation) { + if let operation = operation as? OperationType { + _didAttach?(operation) + } + } + + func operationDidStart(_ operation: Operation) { + if let operation = operation as? OperationType { + _didStart?(operation) + } + } + + func operationDidCancel(_ operation: Operation) { + if let operation = operation as? OperationType { + _didCancel?(operation) + } + } + + func operationDidFinish(_ operation: Operation) { + if let operation = operation as? OperationType { + _didFinish?(operation) + } + } +} diff --git a/ios/MullvadVPN/Operations/ResultOperation+Fallible.swift b/ios/MullvadVPN/Operations/ResultOperation+Fallible.swift new file mode 100644 index 0000000000..fac45cbf93 --- /dev/null +++ b/ios/MullvadVPN/Operations/ResultOperation+Fallible.swift @@ -0,0 +1,15 @@ +// +// ResultOperation+Fallible.swift +// MullvadVPN +// +// Created by pronebird on 31/05/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension ResultOperation: FallibleOperation { + var error: Error? { + return completion?.error + } +} |
