summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj14
-rw-r--r--ios/MullvadVPN/Operations/ExclusivityController.swift118
-rw-r--r--ios/MullvadVPN/Promise/Promise+OperationQueue.swift49
-rw-r--r--ios/MullvadVPNTests/PromiseTests.swift51
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)
+ }
+
}