diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-06-08 13:58:30 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-06-08 13:58:30 +0200 |
| commit | 3227e1999eb540fd8645cd0f4c8e370851714662 (patch) | |
| tree | 6a1879bdfe92f7fbe16a69a35911e3bf813e56d8 | |
| parent | 4486a98b4e974273f89c1de7d2654773bf091956 (diff) | |
| parent | 24d1318b92113ef92703d504e90587b052959b31 (diff) | |
| download | mullvadvpn-3227e1999eb540fd8645cd0f4c8e370851714662.tar.xz mullvadvpn-3227e1999eb540fd8645cd0f4c8e370851714662.zip | |
Merge branch 'conditions-observers'
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) + } + +} |
