diff options
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/ExclusivityController.swift | 118 | ||||
| -rw-r--r-- | ios/MullvadVPN/Promise/Promise+OperationQueue.swift | 49 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/PromiseTests.swift | 51 |
4 files changed, 194 insertions, 38 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 52571baf85..15263a1696 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -26,8 +26,8 @@ 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; }; 581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */; }; 581FC4FA2695ACE100AA97BA /* Account.strings in Resources */ = {isa = PBXBuildFile; fileRef = 581FC4F82695ACE100AA97BA /* Account.strings */; }; - 5823FA5026CA690600283BF8 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 5820674926E63EC900655B05 /* Promise+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */; }; + 5823FA5026CA690600283BF8 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; }; 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; }; @@ -52,6 +52,8 @@ 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; }; 5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; 5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; + 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; }; + 5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; }; 584789B8264D4A2A000E45FB /* old_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */; }; 584789B9264D4A2A000E45FB /* old_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */; }; 584789BE264D4A2A000E45FB /* new_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */; }; @@ -133,6 +135,7 @@ 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589AB4F6227B64450039131E /* BasicTableViewCell.swift */; }; 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; + 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A94AE526D23C3D001CB97C /* PromiseTests.swift */; }; 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; }; 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */; }; 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; }; @@ -161,6 +164,7 @@ 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */; }; 58BA791B2578F092006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA791A2578F092006FAEA0 /* WireGuardKit */; }; 58BA7947257901A5006FAEA0 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58BA7946257901A5006FAEA0 /* WireGuardKit */; }; + 58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; }; 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; }; 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5C522A7C97F00A6173D /* RelayCache.swift */; }; 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; @@ -334,6 +338,7 @@ 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; }; 584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; }; 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelIpc.swift; sourceTree = "<group>"; }; + 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+OperationQueue.swift"; sourceTree = "<group>"; }; 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = old_le_root_cert.cer; sourceTree = "<group>"; }; 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = new_le_root_cert.cer; sourceTree = "<group>"; }; 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLPinningURLSessionDelegate.swift; sourceTree = "<group>"; }; @@ -773,6 +778,7 @@ 58E1337426D2BEC400CC316B /* Promise+Optional.swift */, 58E1337826D2BEDD00CC316B /* Promise+ReceiveOn.swift */, 58E1338026D2BF5C00CC316B /* Promise+Result.swift */, + 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */, 5820674826E63EC800655B05 /* Promise+BackgroundTask.swift */, ); path = Promise; @@ -1065,14 +1071,17 @@ 584E96BE240FD4DB00D3334F /* Location.swift in Sources */, 5857F23F24C844AD00CF6F47 /* Locking.swift in Sources */, 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */, + 58BF345E26F09F3C002A6CAA /* ExclusivityController.swift in Sources */, 58E1338326D2BF5C00CC316B /* Promise+Result.swift in Sources */, 58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, + 5846226826E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */, 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */, 58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */, 5860392726D91B8400554C79 /* PromiseTests.swift in Sources */, 58E1337326D2BE9C00CC316B /* AnyOptional.swift in Sources */, 5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */, + 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */, 58C3478B26C1094F0060838B /* Promise.swift in Sources */, 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, @@ -1155,6 +1164,7 @@ 584E96BC240FD4DA00D3334F /* Location.swift in Sources */, 581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */, 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */, + 5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */, 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */, 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */, 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */, @@ -1237,6 +1247,8 @@ 58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */, 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, 58B93A2526C683B300A55733 /* Promise.swift in Sources */, + 58E1337A26D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, + 58B93A2526C683B300A55733 /* Promise.swift in Sources */, 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */, 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, diff --git a/ios/MullvadVPN/Operations/ExclusivityController.swift b/ios/MullvadVPN/Operations/ExclusivityController.swift index 9e18516869..b87fd93028 100644 --- a/ios/MullvadVPN/Operations/ExclusivityController.swift +++ b/ios/MullvadVPN/Operations/ExclusivityController.swift @@ -8,62 +8,108 @@ import Foundation -class ExclusivityController<Category> where Category: Hashable { - private let operationQueue: OperationQueue - private let lock = NSRecursiveLock() +class ExclusivityController: NSObject { + private let lock = NSLock() + private var operations: [String: [Operation]] = [:] + private var categoriesByOperation: [Operation: [String]] = [:] - private var operations: [Category: [Operation]] = [:] - private var observers: [Operation: NSObjectProtocol] = [:] + static let shared = ExclusivityController() - init(operationQueue: OperationQueue) { - self.operationQueue = operationQueue - } + private override init() {} + + func addOperation(_ operation: Operation, categories: [String]) { + lock.withCriticalBlock { + categories.forEach { category in + addOperation(operation, category: category) + } - func addOperation(_ operation: Operation, categories: [Category]) { - addOperations([operation], categories: categories) + addObserverIfNeeded(operation: operation, categories: categories) + } } - func addOperations(_ operations: [Operation], categories: [Category]) { + func removeOperation(_ operation: Operation, categories: [String]) { lock.withCriticalBlock { - for operation in operations { - for category in categories { - addDependencies(operation: operation, category: category) - } - - observers[operation] = operation.observe(\.isFinished, options: [.initial, .new]) { [weak self] (op, change) in - if let isFinished = change.newValue, isFinished { - self?.operationDidFinish(op, categories: categories) - } - } + categories.forEach { category in + removeOperation(operation, category: category) } - operationQueue.addOperations(operations, waitUntilFinished: false) + removeObserverIfNeeded(operation: operation, categories: categories) } } - private func addDependencies(operation: Operation, category: Category) { - var exclusiveOperations = self.operations[category] ?? [] + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if let operation = object as? Operation, keyPath == "isFinished" { + operationDidFinish(operation) + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } - if let dependency = exclusiveOperations.last, !operation.dependencies.contains(dependency) { - operation.addDependency(dependency) + // MARK: - Private + + private func addOperation(_ operation: Operation, category: String) { + var operationsWithThisCategory = operations[category] ?? [] + + if let last = operationsWithThisCategory.last { + operation.addDependency(last) } - exclusiveOperations.append(operation) - self.operations[category] = exclusiveOperations + operationsWithThisCategory.append(operation) + + operations[category] = operationsWithThisCategory + } + + private func removeOperation(_ operation: Operation, category: String) { + guard var operationsWithThisCategory = operations[category], + let index = operationsWithThisCategory.firstIndex(of: operation) else { return } + + operationsWithThisCategory.remove(at: index) + + if operationsWithThisCategory.isEmpty { + operations.removeValue(forKey: category) + } else { + operations[category] = operationsWithThisCategory + } + } + + private func addObserverIfNeeded(operation: Operation, categories: [String]) { + let existingCategories = categoriesByOperation[operation] ?? [] + let newCategories = existingCategories + categories + + if existingCategories.isEmpty && !newCategories.isEmpty { + operation.addObserver(self, forKeyPath: "isFinished", options: .new, context: nil) + } + + if !newCategories.isEmpty { + categoriesByOperation[operation] = newCategories + } + } + + private func removeObserverIfNeeded(operation: Operation, categories: [String]) { + guard var newCategories = categoriesByOperation[operation] else { return } + + newCategories.removeAll { s in + categories.contains(s) + } + + if newCategories.isEmpty { + operation.removeObserver(self, forKeyPath: "isFinished", context: nil) + + categoriesByOperation.removeValue(forKey: operation) + } else { + categoriesByOperation[operation] = newCategories + } } - private func operationDidFinish(_ operation: Operation, categories: [Category]) { + private func operationDidFinish(_ operation: Operation) { lock.withCriticalBlock { - for category in categories { - var exclusiveOperations = self.operations[category] ?? [] + let operationCategories = categoriesByOperation[operation] ?? [] - exclusiveOperations.removeAll { (storedOperation) -> Bool in - return operation == storedOperation - } + removeObserverIfNeeded(operation: operation, categories: operationCategories) - self.operations[category] = exclusiveOperations + operationCategories.forEach { category in + removeOperation(operation, category: category) } - self.observers.removeValue(forKey: operation) } } } diff --git a/ios/MullvadVPN/Promise/Promise+OperationQueue.swift b/ios/MullvadVPN/Promise/Promise+OperationQueue.swift new file mode 100644 index 0000000000..be8c4e5166 --- /dev/null +++ b/ios/MullvadVPN/Promise/Promise+OperationQueue.swift @@ -0,0 +1,49 @@ +// +// Promise+OperationQueue.swift +// Promise+OperationQueue +// +// Created by pronebird on 02/09/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension Promise { + + /// Returns a promise that adds operation that finishes along with the upstream. + func run(on operationQueue: OperationQueue) -> Promise<Value> { + return Promise { resolver in + let operation = AsyncBlockOperation { finish in + self.observe { completion in + resolver.resolve(completion: completion) + finish() + } + } + + resolver.setCancelHandler { + operation.cancel() + } + + operationQueue.addOperation(operation) + } + } + + /// Returns a promise that adds a mutually exclusive operation that finishes along with the upstream. + func run(on operationQueue: OperationQueue, categories: [String]) -> Promise<Value> { + return Promise { resolver in + let operation = AsyncBlockOperation { finish in + self.observe { completion in + resolver.resolve(completion: completion) + finish() + } + } + + resolver.setCancelHandler { + operation.cancel() + } + + ExclusivityController.shared.addOperation(operation, categories: categories) + operationQueue.addOperation(operation) + } + } +} diff --git a/ios/MullvadVPNTests/PromiseTests.swift b/ios/MullvadVPNTests/PromiseTests.swift index 616d492a3d..2c436d1ede 100644 --- a/ios/MullvadVPNTests/PromiseTests.swift +++ b/ios/MullvadVPNTests/PromiseTests.swift @@ -112,7 +112,9 @@ class PromiseTests: XCTestCase { } DispatchQueue.main.async(execute: work) - }.observe { completion in + } + + promise.observe { completion in XCTAssertEqual(completion, .cancelled) completionExpectation.fulfill() } @@ -144,4 +146,51 @@ class PromiseTests: XCTestCase { } } + func testRunOnOperationQueue() { + let operationQueue = OperationQueue() + operationQueue.name = "SerialOperationQueue" + operationQueue.maxConcurrentOperationCount = 1 + + let expect1 = expectation(description: "Wait for the first promise") + let expect2 = expectation(description: "Wait for the second promise") + + Promise(value: 1) + .receive(on: .main, after: .milliseconds(100), timerType: .deadline) + .run(on: operationQueue) + .observe { completion in + expect1.fulfill() + } + + Promise(value: 2) + .run(on: operationQueue) + .observe { completion in + expect2.fulfill() + } + + wait(for: [expect1, expect2], timeout: 1) + } + + func testRunOnOperationQueueWithExcusiveCategory() { + let operationQueue = OperationQueue() + operationQueue.name = "ConcurrentOperationQueue" + + let expect1 = expectation(description: "Wait for the first promise") + let expect2 = expectation(description: "Wait for the second promise") + + Promise(value: 1) + .receive(on: .main, after: .milliseconds(100), timerType: .deadline) + .run(on: operationQueue, categories: ["MutuallyExclusive"]) + .observe { completion in + expect1.fulfill() + } + + Promise(value: 2) + .run(on: operationQueue, categories: ["MutuallyExclusive"]) + .observe { completion in + expect2.fulfill() + } + + wait(for: [expect1, expect2], timeout: 1) + } + } |
