summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-06-08 13:58:30 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-06-08 13:58:30 +0200
commit3227e1999eb540fd8645cd0f4c8e370851714662 (patch)
tree6a1879bdfe92f7fbe16a69a35911e3bf813e56d8
parent4486a98b4e974273f89c1de7d2654773bf091956 (diff)
parent24d1318b92113ef92703d504e90587b052959b31 (diff)
downloadmullvadvpn-3227e1999eb540fd8645cd0f4c8e370851714662.tar.xz
mullvadvpn-3227e1999eb540fd8645cd0f4c8e370851714662.zip
Merge branch 'conditions-observers'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj72
-rw-r--r--ios/MullvadVPN/AddressCache/AddressCacheTracker.swift14
-rw-r--r--ios/MullvadVPN/AlertPresenter.swift2
-rw-r--r--ios/MullvadVPN/AppDelegate.swift2
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift26
-rw-r--r--ios/MullvadVPN/AppStoreReceipt.swift12
-rw-r--r--ios/MullvadVPN/Operations/AsyncBlockOperation.swift34
-rw-r--r--ios/MullvadVPN/Operations/AsyncOperation.swift266
-rw-r--r--ios/MullvadVPN/Operations/AsyncOperationQueue.swift101
-rw-r--r--ios/MullvadVPN/Operations/BackgroundObserver.swift54
-rw-r--r--ios/MullvadVPN/Operations/ExclusivityController.swift117
-rw-r--r--ios/MullvadVPN/Operations/GroupOperation.swift35
-rw-r--r--ios/MullvadVPN/Operations/OperationCondition.swift107
-rw-r--r--ios/MullvadVPN/Operations/OperationObserver.swift63
-rw-r--r--ios/MullvadVPN/Operations/ResultBlockOperation.swift30
-rw-r--r--ios/MullvadVPN/Operations/ResultOperation+Fallible.swift15
-rw-r--r--ios/MullvadVPN/REST/RESTAccessTokenManager.swift2
-rw-r--r--ios/MullvadVPN/REST/RESTProxy.swift2
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift12
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift168
-rw-r--r--ios/MullvadVPNTests/OperationConditionTests.swift144
-rw-r--r--ios/MullvadVPNTests/OperationObserverTests.swift68
23 files changed, 1040 insertions, 308 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index fad611075f..6d6c7ff27b 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 */; };
@@ -20,7 +21,7 @@
58095C552760F02500890776 /* UpdateAddressCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C542760F02500890776 /* UpdateAddressCacheOperation.swift */; };
58095C572760F47900890776 /* api-ip-address.json in Resources */ = {isa = PBXBuildFile; fileRef = 58095C562760F47900890776 /* api-ip-address.json */; };
58095C592762155700890776 /* RESTRetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C582762155700890776 /* RESTRetryStrategy.swift */; };
- 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; };
+ 580CBFB82848D503007878F0 /* OperationConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580CBFB72848D503007878F0 /* OperationConditionTests.swift */; };
580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; };
580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
@@ -70,6 +71,17 @@
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 */; };
+ 583E1E1C2848DE1C004838B3 /* AsyncOperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */; };
+ 583E1E1E2848DE1C004838B3 /* OperationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28772846250500F9A7B3 /* OperationCondition.swift */; };
+ 583E1E202848DE1C004838B3 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28792846250500F9A7B3 /* OperationObserver.swift */; };
+ 583E1E222848DE1C004838B3 /* GroupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28812846306C00F9A7B3 /* GroupOperation.swift */; };
+ 583E1E232848DE1C004838B3 /* OperationCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840BE34279EDB16002836BA /* OperationCompletion.swift */; };
+ 583E1E252848DE1C004838B3 /* ResultOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F7D26427EB50A300E4D821 /* ResultOperation.swift */; };
+ 583E1E262848DE1C004838B3 /* ResultBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */; };
+ 583E1E282848DE1C004838B3 /* BackgroundObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */; };
+ 583E1E2A2848DF67004838B3 /* OperationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583E1E292848DF67004838B3 /* OperationObserverTests.swift */; };
+ 583E1E2C2848E1A1004838B3 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 583E1E2B2848E1A1004838B3 /* WireGuardKitTypes */; };
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 */; };
@@ -84,7 +96,6 @@
5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* AppStoreSubscription.swift */; };
5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* AppStorePaymentObserver.swift */; };
5846227726E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift */; };
- 5846227A26E24F1F0035F7C2 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; };
584789BE264D4A2A000E45FB /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* le_root_cert.cer */; };
584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
584789EC2652A1A2000E45FB /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 584789EB2652A1A2000E45FB /* Logging */; };
@@ -177,7 +188,6 @@
5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 587B7543266922BF00DEF7E9 /* Localizable.strings */; };
588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */; };
588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */; };
- 58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58871D1D25D535A3002297FA /* WireGuardKit */; };
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; };
5888AD87227B17950051EB06 /* SelectLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD86227B17950051EB06 /* SelectLocationViewController.swift */; };
@@ -194,6 +204,11 @@
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 */; };
+ 589D288028462CB000F9A7B3 /* BackgroundObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */; };
+ 589D28822846306C00F9A7B3 /* GroupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589D28812846306C00F9A7B3 /* GroupOperation.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 +342,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>"; };
@@ -339,7 +355,7 @@
58095C542760F02500890776 /* UpdateAddressCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAddressCacheOperation.swift; sourceTree = "<group>"; };
58095C562760F47900890776 /* api-ip-address.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "api-ip-address.json"; sourceTree = "<group>"; };
58095C582762155700890776 /* RESTRetryStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRetryStrategy.swift; sourceTree = "<group>"; };
- 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclusivityController.swift; sourceTree = "<group>"; };
+ 580CBFB72848D503007878F0 /* OperationConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationConditionTests.swift; sourceTree = "<group>"; };
580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; };
580F8B8228197881002E0998 /* TunnelSettingsV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV2.swift; sourceTree = "<group>"; };
580F8B8528197958002E0998 /* DNSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSSettings.swift; sourceTree = "<group>"; };
@@ -373,6 +389,7 @@
5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; };
5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = "<group>"; };
583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; };
+ 583E1E292848DF67004838B3 /* OperationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserverTests.swift; sourceTree = "<group>"; };
5840250022B1124600E4CFEC /* IPAddress+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddress+Codable.swift"; sourceTree = "<group>"; };
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
5840BE34279EDB16002836BA /* OperationCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationCompletion.swift; sourceTree = "<group>"; };
@@ -463,6 +480,11 @@
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>"; };
+ 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundObserver.swift; sourceTree = "<group>"; };
+ 589D28812846306C00F9A7B3 /* GroupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupOperation.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>"; };
@@ -550,8 +572,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 583E1E2C2848E1A1004838B3 /* WireGuardKitTypes in Frameworks */,
584789EC2652A1A2000E45FB /* Logging in Frameworks */,
- 58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -607,12 +629,17 @@
children = (
580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */,
58E973DD24850EB600096F90 /* AsyncOperation.swift */,
- 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */,
+ 589D28782846250500F9A7B3 /* AsyncOperationQueue.swift */,
+ 589D287F28462CB000F9A7B3 /* BackgroundObserver.swift */,
+ 589D28812846306C00F9A7B3 /* GroupOperation.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>";
@@ -763,6 +790,8 @@
58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = {
isa = PBXGroup;
children = (
+ 583E1E292848DF67004838B3 /* OperationObserverTests.swift */,
+ 580CBFB72848D503007878F0 /* OperationConditionTests.swift */,
582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */,
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */,
58B0A2A4238EE67E00BC001D /* Info.plist */,
@@ -1002,8 +1031,8 @@
);
name = MullvadVPNTests;
packageProductDependencies = (
- 58871D1D25D535A3002297FA /* WireGuardKit */,
584789EB2652A1A2000E45FB /* Logging */,
+ 583E1E2B2848E1A1004838B3 /* WireGuardKitTypes */,
);
productName = MullvadVPNTests;
productReference = 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */;
@@ -1187,6 +1216,8 @@
files = (
582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */,
58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */,
+ 583E1E232848DE1C004838B3 /* OperationCompletion.swift in Sources */,
+ 583E1E222848DE1C004838B3 /* GroupOperation.swift in Sources */,
58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */,
5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */,
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
@@ -1198,20 +1229,28 @@
58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */,
58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */,
58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */,
+ 583E1E252848DE1C004838B3 /* ResultOperation.swift in Sources */,
+ 583E1E262848DE1C004838B3 /* ResultBlockOperation.swift in Sources */,
+ 583E1E2A2848DF67004838B3 /* OperationObserverTests.swift in Sources */,
5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */,
582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */,
585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */,
58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */,
+ 583E1E282848DE1C004838B3 /* BackgroundObserver.swift in Sources */,
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */,
5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */,
585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */,
+ 583E1E1E2848DE1C004838B3 /* OperationCondition.swift in Sources */,
+ 583E1E1C2848DE1C004838B3 /* AsyncOperationQueue.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 */,
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */,
+ 580CBFB82848D503007878F0 /* OperationConditionTests.swift in Sources */,
+ 583E1E202848DE1C004838B3 /* OperationObserver.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1242,10 +1281,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 +1313,8 @@
58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */,
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */,
58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */,
+ 589D287A2846250500F9A7B3 /* OperationCondition.swift in Sources */,
+ 589D288028462CB000F9A7B3 /* BackgroundObserver.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */,
@@ -1283,6 +1326,7 @@
58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */,
582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */,
58554F7D280D6FE000013055 /* RESTURLSession.swift in Sources */,
+ 589D28822846306C00F9A7B3 /* GroupOperation.swift in Sources */,
5846227726E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift in Sources */,
5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */,
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */,
@@ -1332,7 +1376,6 @@
584D26BF270C550B004EA533 /* AnyIPAddress.swift in Sources */,
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */,
587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */,
- 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */,
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */,
585DA89326B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */,
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */,
@@ -1397,6 +1440,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 */,
@@ -1887,6 +1931,11 @@
package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
productName = WireGuardKitTypes;
};
+ 583E1E2B2848E1A1004838B3 /* WireGuardKitTypes */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
+ productName = WireGuardKitTypes;
+ };
584789EB2652A1A2000E45FB /* Logging */ = {
isa = XCSwiftPackageProductDependency;
package = 585834F624D2BC1F00A8AF56 /* XCRemoteSwiftPackageReference "swift-log" */;
@@ -1902,11 +1951,6 @@
package = 585834F624D2BC1F00A8AF56 /* XCRemoteSwiftPackageReference "swift-log" */;
productName = Logging;
};
- 58871D1D25D535A3002297FA /* WireGuardKit */ = {
- isa = XCSwiftPackageProductDependency;
- package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
- productName = WireGuardKit;
- };
58BA791A2578F092006FAEA0 /* WireGuardKit */ = {
isa = XCSwiftPackageProductDependency;
package = 58BA79192578F092006FAEA0 /* XCRemoteSwiftPackageReference "wireguard-apple" */;
diff --git a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
index d0b7babb93..bbfaeb08c8 100644
--- a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
+++ b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
@@ -37,8 +37,8 @@ extension AddressCache {
private var timer: DispatchSourceTimer?
/// Operation queue.
- private let operationQueue: OperationQueue = {
- let operationQueue = OperationQueue()
+ private let operationQueue: AsyncOperationQueue = {
+ let operationQueue = AsyncOperationQueue()
operationQueue.maxConcurrentOperationCount = 1
return operationQueue
}()
@@ -96,13 +96,9 @@ extension AddressCache {
}
)
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "AddressCache.Tracker.updateEndpoints") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Update endpoints", cancelUponExpiration: true)
+ )
operationQueue.addOperation(operation)
diff --git a/ios/MullvadVPN/AlertPresenter.swift b/ios/MullvadVPN/AlertPresenter.swift
index 954a641e28..f67b07a3e8 100644
--- a/ios/MullvadVPN/AlertPresenter.swift
+++ b/ios/MullvadVPN/AlertPresenter.swift
@@ -13,7 +13,7 @@ class AlertPresenter {
static let alertControllerDidDismissNotification = Notification.Name("UIAlertControllerDidDismiss")
private let operationQueue: OperationQueue = {
- let operationQueue = OperationQueue()
+ let operationQueue = AsyncOperationQueue()
operationQueue.name = "AlertPresenterQueue"
operationQueue.maxConcurrentOperationCount = 1
return operationQueue
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index c5d4d9e9e4..9c853b018b 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -165,7 +165,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var relaysFetchResult: UIBackgroundFetchResult?
var rotatePrivateKeyFetchResult: UIBackgroundFetchResult?
- let operationQueue = OperationQueue()
+ let operationQueue = AsyncOperationQueue()
let updateAddressCacheOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in
let handle = self.addressCacheTracker.updateEndpoints { completion in
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
index e94a6e906b..6fb85bd1dd 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
@@ -23,7 +23,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
private let logger = Logger(label: "AppStorePaymentManager")
private let operationQueue: OperationQueue = {
- let queue = OperationQueue()
+ let queue = AsyncOperationQueue()
queue.name = "AppStorePaymentManagerQueue"
return queue
}()
@@ -31,8 +31,6 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
private let accountsProxy = REST.ProxyFactory.shared.createAccountsProxy()
- private let exclusivityController = ExclusivityController()
-
private let paymentQueue: SKPaymentQueue
private var observerList = ObserverList<AppStorePaymentObserver>()
@@ -102,9 +100,11 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
func requestProducts(with productIdentifiers: Set<AppStoreSubscription>, completionHandler: @escaping (OperationCompletion<SKProductsResponse, Swift.Error>) -> Void) -> Cancellable {
let productIdentifiers = productIdentifiers.productIdentifiersSet
- let operation = ProductsRequestOperation(productIdentifiers: productIdentifiers, completionHandler: completionHandler)
-
- exclusivityController.addOperation(operation, categories: [OperationCategory.productsRequest])
+ let operation = ProductsRequestOperation(
+ productIdentifiers: productIdentifiers,
+ completionHandler: completionHandler
+ )
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.productsRequest))
operationQueue.addOperation(operation)
@@ -189,15 +189,13 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
completionHandler: completionHandler
)
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Send AppStore receipt") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Send AppStore receipt", cancelUponExpiration: true)
+ )
- exclusivityController.addOperation(operation, categories: [OperationCategory.sendAppStoreReceipt])
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.sendAppStoreReceipt)
+ )
operationQueue.addOperation(operation)
diff --git a/ios/MullvadVPN/AppStoreReceipt.swift b/ios/MullvadVPN/AppStoreReceipt.swift
index baa797d592..a565bd4012 100644
--- a/ios/MullvadVPN/AppStoreReceipt.swift
+++ b/ios/MullvadVPN/AppStoreReceipt.swift
@@ -34,7 +34,7 @@ enum AppStoreReceipt {
/// Internal operation queue.
private static let operationQueue: OperationQueue = {
- let queue = OperationQueue()
+ let queue = AsyncOperationQueue()
queue.name = "AppStoreReceiptQueue"
queue.maxConcurrentOperationCount = 1
return queue
@@ -49,13 +49,9 @@ enum AppStoreReceipt {
completionHandler: completionHandler
)
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Fetch AppStore receipt") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Fetch AppStore receipt", cancelUponExpiration: true)
+ )
operationQueue.addOperation(operation)
diff --git a/ios/MullvadVPN/Operations/AsyncBlockOperation.swift b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
index a0096f77d6..9c4101fdff 100644
--- a/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
+++ b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift
@@ -13,16 +13,32 @@ class AsyncBlockOperation: AsyncOperation {
private var executionBlock: ((AsyncBlockOperation) -> Void)?
private var cancellationBlocks: [() -> Void] = []
- init(dispatchQueue: DispatchQueue?, block: @escaping (AsyncBlockOperation) -> Void) {
+ override init(dispatchQueue: DispatchQueue? = nil) {
+ super.init(dispatchQueue: dispatchQueue)
+ }
+
+ init(dispatchQueue: DispatchQueue? = nil, block: @escaping (AsyncBlockOperation) -> Void) {
executionBlock = block
super.init(dispatchQueue: dispatchQueue)
}
+ init(dispatchQueue: DispatchQueue? = nil, block: @escaping () -> Void) {
+ executionBlock = { operation in
+ block()
+ operation.finish()
+ }
+ super.init(dispatchQueue: dispatchQueue)
+ }
+
override func main() {
let block = executionBlock
executionBlock = nil
- block?(self)
+ if let block = block {
+ block(self)
+ } else {
+ finish()
+ }
}
override func operationDidCancel() {
@@ -39,6 +55,20 @@ class AsyncBlockOperation: AsyncOperation {
executionBlock = nil
}
+ func setExecutionBlock(_ block: @escaping (AsyncBlockOperation) -> Void) {
+ dispatchQueue.async {
+ assert(!self.isExecuting && !self.isFinished)
+ self.executionBlock = block
+ }
+ }
+
+ func setExecutionBlock(_ block: @escaping () -> Void) {
+ setExecutionBlock { operation in
+ block()
+ operation.finish()
+ }
+ }
+
func addCancellationBlock(_ block: @escaping () -> Void) {
dispatchQueue.async {
if self.isCancelled {
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/BackgroundObserver.swift b/ios/MullvadVPN/Operations/BackgroundObserver.swift
new file mode 100644
index 0000000000..e5d83c6235
--- /dev/null
+++ b/ios/MullvadVPN/Operations/BackgroundObserver.swift
@@ -0,0 +1,54 @@
+//
+// BackgroundObserver.swift
+// MullvadVPN
+//
+// Created by pronebird on 31/05/2022.
+//
+
+#if canImport(UIKit)
+
+import UIKit
+
+class BackgroundObserver: OperationObserver {
+ let name: String
+ let application: UIApplication
+ let cancelUponExpiration: Bool
+
+ private var taskIdentifier: UIBackgroundTaskIdentifier?
+
+ init(
+ application: UIApplication = .shared,
+ name: String,
+ cancelUponExpiration: Bool
+ )
+ {
+ self.application = application
+ self.name = name
+ self.cancelUponExpiration = cancelUponExpiration
+ }
+
+ func didAttach(to operation: Operation) {
+ let expirationHandler = cancelUponExpiration ? { operation.cancel() } : nil
+
+ taskIdentifier = application.beginBackgroundTask(
+ withName: name,
+ expirationHandler: expirationHandler
+ )
+ }
+
+ func operationDidStart(_ operation: Operation) {
+ // no-op
+ }
+
+ func operationDidCancel(_ operation: Operation) {
+ // no-op
+ }
+
+ func operationDidFinish(_ operation: Operation) {
+ if let taskIdentifier = taskIdentifier {
+ application.endBackgroundTask(taskIdentifier)
+ }
+ }
+}
+
+#endif
diff --git a/ios/MullvadVPN/Operations/ExclusivityController.swift b/ios/MullvadVPN/Operations/ExclusivityController.swift
deleted file mode 100644
index 5e8fdaf97f..0000000000
--- a/ios/MullvadVPN/Operations/ExclusivityController.swift
+++ /dev/null
@@ -1,117 +0,0 @@
-//
-// ExclusivityController.swift
-// MullvadVPN
-//
-// Created by pronebird on 06/07/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-class ExclusivityController: NSObject {
- private let lock = NSLock()
- private var operations: [String: [Operation]] = [:]
- private var categoriesByOperation: [Operation: [String]] = [:]
-
- func addOperation(_ operation: Operation, categories: [String]) {
- lock.lock()
-
- categories.forEach { category in
- addOperation(operation, category: category)
- }
-
- addObserverIfNeeded(operation: operation, categories: categories)
-
- lock.unlock()
- }
-
- func removeOperation(_ operation: Operation, categories: [String]) {
- lock.lock()
-
- categories.forEach { category in
- removeOperation(operation, category: category)
- }
-
- removeObserverIfNeeded(operation: operation, categories: categories)
-
- lock.unlock()
- }
-
- 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)
- }
- }
-
- // MARK: - Private
-
- private func addOperation(_ operation: Operation, category: String) {
- var operationsWithThisCategory = operations[category] ?? []
-
- if let last = operationsWithThisCategory.last {
- operation.addDependency(last)
- }
-
- 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) {
- lock.lock()
-
- let operationCategories = categoriesByOperation[operation] ?? []
-
- removeObserverIfNeeded(operation: operation, categories: operationCategories)
-
- operationCategories.forEach { category in
- removeOperation(operation, category: category)
- }
-
- lock.unlock()
- }
-}
diff --git a/ios/MullvadVPN/Operations/GroupOperation.swift b/ios/MullvadVPN/Operations/GroupOperation.swift
new file mode 100644
index 0000000000..5a6ca9902d
--- /dev/null
+++ b/ios/MullvadVPN/Operations/GroupOperation.swift
@@ -0,0 +1,35 @@
+//
+// GroupOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 31/05/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class GroupOperation: AsyncOperation {
+ private let operationQueue = AsyncOperationQueue()
+ private let children: [Operation]
+
+ init(operations: [Operation]) {
+ children = operations
+
+ super.init(dispatchQueue: nil)
+ }
+
+ override func main() {
+ let finishingOperation = BlockOperation()
+ finishingOperation.completionBlock = { [weak self] in
+ self?.finish()
+ }
+ finishingOperation.addDependencies(children)
+
+ operationQueue.addOperations(children, waitUntilFinished: false)
+ operationQueue.addOperation(finishingOperation)
+ }
+
+ override func operationDidCancel() {
+ operationQueue.cancelAllOperations()
+ }
+}
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/ResultBlockOperation.swift b/ios/MullvadVPN/Operations/ResultBlockOperation.swift
index 4ac88695b5..41e1e8ee77 100644
--- a/ios/MullvadVPN/Operations/ResultBlockOperation.swift
+++ b/ios/MullvadVPN/Operations/ResultBlockOperation.swift
@@ -10,11 +10,16 @@ import Foundation
class ResultBlockOperation<Success, Failure: Error>: ResultOperation<Success, Failure> {
typealias ExecutionBlock = (ResultBlockOperation<Success, Failure>) -> Void
+ typealias ThrowingExecutionBlock = () throws -> Success
private var executionBlock: ExecutionBlock?
private var cancellationBlocks: [() -> Void] = []
- convenience init(dispatchQueue: DispatchQueue?, executionBlock: @escaping ExecutionBlock) {
+ convenience init(
+ dispatchQueue: DispatchQueue? = nil,
+ executionBlock: @escaping ExecutionBlock
+ )
+ {
self.init(
dispatchQueue: dispatchQueue,
executionBlock: executionBlock,
@@ -23,6 +28,29 @@ class ResultBlockOperation<Success, Failure: Error>: ResultOperation<Success, Fa
)
}
+ convenience init(
+ dispatchQueue: DispatchQueue? = nil,
+ executionBlock: @escaping ThrowingExecutionBlock
+ )
+ {
+ self.init(
+ dispatchQueue: dispatchQueue,
+ executionBlock: { operation in
+ do {
+ let value = try executionBlock()
+
+ operation.finish(completion: .success(value))
+ } catch {
+ let castedError = error as! Failure
+
+ operation.finish(completion: .failure(castedError))
+ }
+ },
+ completionQueue: nil,
+ completionHandler: nil
+ )
+ }
+
init(
dispatchQueue: DispatchQueue?,
executionBlock: @escaping ExecutionBlock,
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
+ }
+}
diff --git a/ios/MullvadVPN/REST/RESTAccessTokenManager.swift b/ios/MullvadVPN/REST/RESTAccessTokenManager.swift
index 6b1986b585..eb124ae829 100644
--- a/ios/MullvadVPN/REST/RESTAccessTokenManager.swift
+++ b/ios/MullvadVPN/REST/RESTAccessTokenManager.swift
@@ -13,7 +13,7 @@ extension REST {
final class AccessTokenManager {
private let logger = Logger(label: "REST.AccessTokenManager")
- private let operationQueue = OperationQueue()
+ private let operationQueue = AsyncOperationQueue()
private let dispatchQueue = DispatchQueue(label: "REST.AccessTokenManager.dispatchQueue")
private let proxy: AuthenticationProxy
private var tokens = [String: AccessTokenData]()
diff --git a/ios/MullvadVPN/REST/RESTProxy.swift b/ios/MullvadVPN/REST/RESTProxy.swift
index f166338976..5ef15bff86 100644
--- a/ios/MullvadVPN/REST/RESTProxy.swift
+++ b/ios/MullvadVPN/REST/RESTProxy.swift
@@ -16,7 +16,7 @@ extension REST {
let dispatchQueue: DispatchQueue
/// Operation queue used for running network operations.
- let operationQueue = OperationQueue()
+ let operationQueue = AsyncOperationQueue()
/// Proxy configuration.
let configuration: ConfigurationType
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
index 025731fe23..218c55f69c 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -31,7 +31,7 @@ extension RelayCache {
/// Internal operation queue.
private let operationQueue: OperationQueue = {
- let operationQueue = OperationQueue()
+ let operationQueue = AsyncOperationQueue()
operationQueue.name = "RelayCacheTrackerQueue"
operationQueue.maxConcurrentOperationCount = 1
return operationQueue
@@ -125,13 +125,9 @@ extension RelayCache {
completionHandler: completionHandler
)
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update relays") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Update relays", cancelUponExpiration: true)
+ )
operationQueue.addOperation(operation)
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
index 3a05d996cd..698dd3c3fc 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
@@ -15,7 +15,7 @@ extension TunnelIPC {
final class Session {
private let tunnel: Tunnel
private let queue = DispatchQueue(label: "TunnelIPC.SessionQueue")
- private let operationQueue = OperationQueue()
+ private let operationQueue = AsyncOperationQueue()
init(tunnel: Tunnel) {
self.tunnel = tunnel
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 71a7c3a4a9..19438ad893 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -61,8 +61,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
private let logger = Logger(label: "TunnelManager")
private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue")
- private let operationQueue = OperationQueue()
- private let exclusivityController = ExclusivityController()
+ private let operationQueue = AsyncOperationQueue()
private var statusObserver: Tunnel.StatusBlockObserver?
private var lastMapConnectionStatusOperation: Operation?
@@ -223,32 +222,24 @@ final class TunnelManager: TunnelManagerStateDelegate {
completionHandler(completion.error)
}
}
+ loadTunnelOperation.addDependency(migrateSettingsOperation)
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(
- withName: "Load tunnel configuration"
- ) {
- // no-op
- }
-
- loadTunnelOperation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
-
- exclusivityController.addOperation(migrateSettingsOperation, categories: [
- OperationCategory.changeTunnelSettings
+ let groupOperation = GroupOperation(operations: [
+ migrateSettingsOperation, loadTunnelOperation
])
- exclusivityController.addOperation(loadTunnelOperation, categories: [
- OperationCategory.manageTunnelProvider,
- OperationCategory.changeTunnelSettings
- ])
+ groupOperation.addObserver(
+ BackgroundObserver(name: "Load tunnel configuration", cancelUponExpiration: false)
+ )
- loadTunnelOperation.addDependency(migrateSettingsOperation)
+ groupOperation.addCondition(
+ MutuallyExclusive(category: OperationCategory.manageTunnelProvider)
+ )
+ groupOperation.addCondition(
+ MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ )
- operationQueue.addOperations([
- migrateSettingsOperation,
- loadTunnelOperation
- ], waitUntilFinished: false)
+ operationQueue.addOperation(groupOperation)
}
func startTunnel() {
@@ -272,16 +263,8 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
})
-
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Start tunnel") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
-
- exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider])
+ operation.addObserver(BackgroundObserver(name: "Start tunnel", cancelUponExpiration: true))
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnelProvider))
operationQueue.addOperation(operation)
}
@@ -305,15 +288,8 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Stop tunnel") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
-
- exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider])
+ operation.addObserver(BackgroundObserver(name: "Stop tunnel", cancelUponExpiration: true))
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnelProvider))
operationQueue.addOperation(operation)
}
@@ -345,15 +321,12 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Reconnect tunnel") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
-
- exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider])
+ operation.addObserver(
+ BackgroundObserver(name: "Reconnect tunnel", cancelUponExpiration: true)
+ )
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.manageTunnelProvider)
+ )
operationQueue.addOperation(operation)
}
@@ -389,18 +362,14 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
})
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: action.taskName) {
- operation.cancel()
- }
+ operation.addObserver(BackgroundObserver(name: action.taskName, cancelUponExpiration: true))
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
-
- exclusivityController.addOperation(operation, categories: [
- OperationCategory.manageTunnelProvider,
- OperationCategory.changeTunnelSettings
- ])
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.manageTunnelProvider)
+ )
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ )
operationQueue.addOperation(operation)
}
@@ -423,17 +392,13 @@ final class TunnelManager: TunnelManagerStateDelegate {
completionHandler?(completion.error)
}
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update account data") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Update account data", cancelUponExpiration: true)
+ )
- exclusivityController.addOperation(operation, categories: [
- OperationCategory.changeTunnelSettings
- ])
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ )
operationQueue.addOperation(operation)
}
@@ -448,17 +413,13 @@ final class TunnelManager: TunnelManagerStateDelegate {
operation.completionQueue = .main
operation.completionHandler = completionHandler
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update device data") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Update device data", cancelUponExpiration: true)
+ )
- exclusivityController.addOperation(operation, categories: [
- OperationCategory.changeTunnelSettings
- ])
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ )
operationQueue.addOperation(operation)
@@ -493,15 +454,13 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Regenerate private key") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Regenerate private key", cancelUponExpiration: true)
+ )
- exclusivityController.addOperation(operation, categories: [OperationCategory.changeTunnelSettings])
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ )
operationQueue.addOperation(operation)
}
@@ -537,15 +496,13 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Rotate private key") {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
+ operation.addObserver(
+ BackgroundObserver(name: "Rotate private key", cancelUponExpiration: true)
+ )
- exclusivityController.addOperation(operation, categories: [OperationCategory.changeTunnelSettings])
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ )
operationQueue.addOperation(operation)
@@ -678,7 +635,9 @@ final class TunnelManager: TunnelManagerStateDelegate {
self.startTunnel()
}
- exclusivityController.addOperation(operation, categories: [OperationCategory.tunnelStateUpdate])
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.tunnelStateUpdate)
+ )
// Cancel last VPN status mapping operation
lastMapConnectionStatusOperation?.cancel()
@@ -727,15 +686,8 @@ final class TunnelManager: TunnelManagerStateDelegate {
completionHandler(completion.error)
}
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: taskName) {
- operation.cancel()
- }
-
- operation.completionBlock = {
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- }
-
- exclusivityController.addOperation(operation, categories: [OperationCategory.changeTunnelSettings])
+ operation.addObserver(BackgroundObserver(name: taskName, cancelUponExpiration: true))
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.changeTunnelSettings))
operationQueue.addOperation(operation)
}
diff --git a/ios/MullvadVPNTests/OperationConditionTests.swift b/ios/MullvadVPNTests/OperationConditionTests.swift
new file mode 100644
index 0000000000..297b8128ee
--- /dev/null
+++ b/ios/MullvadVPNTests/OperationConditionTests.swift
@@ -0,0 +1,144 @@
+//
+// OperationConditionTests.swift
+// MullvadVPNTests
+//
+// Created by pronebird on 02/06/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import XCTest
+
+class OperationConditionTests: XCTestCase {
+ func testTrueCondition() {
+ let expectConditionEvaluation = expectation(description: "Expect condition evaluation")
+ let expectOperationToExecute = expectation(description: "Expect operation to execute")
+
+ let operation = AsyncBlockOperation {
+ expectOperationToExecute.fulfill()
+ }
+
+ let blockCondition = BlockCondition { op, completion in
+ expectConditionEvaluation.fulfill()
+ completion(true)
+ }
+
+ operation.addCondition(blockCondition)
+
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperation(operation)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testFalseCondition() {
+ let expectConditionEvaluation = expectation(description: "Expect condition evaluation")
+ let expectOperationToNeverExecute = expectation(
+ description: "Expect operation to never execute"
+ )
+ expectOperationToNeverExecute.isInverted = true
+
+ let operation = AsyncBlockOperation {
+ expectOperationToNeverExecute.fulfill()
+ }
+
+ let blockCondition = BlockCondition { op, completion in
+ expectConditionEvaluation.fulfill()
+ completion(false)
+ }
+
+ operation.addCondition(blockCondition)
+
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperation(operation)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testNoCancelledDependenciesCondition() {
+ let expectToNeverExecute = expectation(description: "Expect child to never execute.")
+ expectToNeverExecute.isInverted = true
+
+ let parent = BlockOperation()
+ parent.cancel()
+
+ let child = AsyncBlockOperation {
+ expectToNeverExecute.fulfill()
+ }
+ child.addDependency(parent)
+ child.addCondition(NoCancelledDependenciesCondition())
+
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperations([parent, child], waitUntilFinished: false)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testNoFailedDependenciesCondition() {
+ let expectToNeverExecute = expectation(description: "Expect child to never execute.")
+ expectToNeverExecute.isInverted = true
+
+ let parent = ResultBlockOperation<(), URLError> {
+ throw URLError(.badURL)
+ }
+
+ let child = AsyncBlockOperation {
+ expectToNeverExecute.fulfill()
+ }
+ child.addDependency(parent)
+ child.addCondition(NoFailedDependenciesCondition(ignoreCancellations: false))
+
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperations([parent, child], waitUntilFinished: false)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testNoFailedDependenciesIgnoringCancellationsCondition() {
+ let expectToExecute = expectation(description: "Expect child to execute.")
+
+ let parent = BlockOperation()
+ parent.cancel()
+
+ let child = AsyncBlockOperation {
+ expectToExecute.fulfill()
+ }
+ child.addDependency(parent)
+ child.addCondition(NoFailedDependenciesCondition(ignoreCancellations: true))
+
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperations([parent, child], waitUntilFinished: false)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testMutuallyExclusiveCondition() {
+ let expectFirstOperationExecution = expectation(
+ description: "Expect first operation to execute first"
+ )
+ let expectSecondOperationExecution = expectation(
+ description: "Expect second operation to execute last"
+ )
+
+ let exclusiveCategory = "exclusiveOperations"
+ let operationQueue = AsyncOperationQueue()
+
+ let firstOperation = AsyncBlockOperation { op in
+ DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
+ expectFirstOperationExecution.fulfill()
+ op.finish()
+ }
+ }
+ firstOperation.addCondition(MutuallyExclusive(category: exclusiveCategory))
+
+ let secondOperation = AsyncBlockOperation {
+ expectSecondOperationExecution.fulfill()
+ }
+ secondOperation.addCondition(MutuallyExclusive(category: exclusiveCategory))
+
+ operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
+
+ let expectations = [expectFirstOperationExecution, expectSecondOperationExecution]
+ wait(for: expectations, timeout: 2, enforceOrder: true)
+ }
+
+}
diff --git a/ios/MullvadVPNTests/OperationObserverTests.swift b/ios/MullvadVPNTests/OperationObserverTests.swift
new file mode 100644
index 0000000000..ab4c29f6e3
--- /dev/null
+++ b/ios/MullvadVPNTests/OperationObserverTests.swift
@@ -0,0 +1,68 @@
+//
+// OperationObserverTests.swift
+// MullvadVPNTests
+//
+// Created by pronebird on 02/06/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import XCTest
+
+class OperationObserverTests: XCTestCase {
+
+ func testBlockObserver() throws {
+ let expectDidAttach = expectation(description: "didAttach handler")
+ let expectDidStart = expectation(description: "didStart handler")
+ let expectDidCancel = expectation(description: "didCancel handler")
+ expectDidCancel.isInverted = true
+ let expectDidFinish = expectation(description: "didAttach handler")
+
+ let operation = AsyncBlockOperation()
+ operation.addBlockObserver(OperationBlockObserver(
+ didAttach: { op in
+ expectDidAttach.fulfill()
+ }, didStart: { op in
+ expectDidStart.fulfill()
+ }, didCancel: { op in
+ expectDidCancel.fulfill()
+ }, didFinish: { op in
+ expectDidFinish.fulfill()
+ }
+ ))
+
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperation(operation)
+
+ let expectations = [expectDidCancel, expectDidAttach, expectDidStart, expectDidFinish]
+ wait(for: expectations, timeout: 1, enforceOrder: true)
+ }
+
+ func testBlockObserverWithCancelledOperation() {
+ let expectDidAttach = expectation(description: "didAttach handler")
+ let expectDidStart = expectation(description: "didStart handler")
+ expectDidStart.isInverted = true
+ let expectDidCancel = expectation(description: "didCancel handler")
+ let expectDidFinish = expectation(description: "didAttach handler")
+
+ let operation = AsyncBlockOperation()
+ operation.addBlockObserver(OperationBlockObserver(
+ didAttach: { op in
+ expectDidAttach.fulfill()
+ }, didStart: { op in
+ expectDidStart.fulfill()
+ }, didCancel: { op in
+ expectDidCancel.fulfill()
+ }, didFinish: { op in
+ expectDidFinish.fulfill()
+ }
+ ))
+ operation.cancel()
+
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperation(operation)
+
+ let expectations = [expectDidAttach, expectDidCancel, expectDidStart, expectDidFinish]
+ wait(for: expectations, timeout: 1, enforceOrder: true)
+ }
+
+}