diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-07-06 21:02:23 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-07-06 21:02:23 +0200 |
| commit | 4097b39627b618eb95cfad290e20aea6ab32fd29 (patch) | |
| tree | d67505f53d264a3846c1caee987828bf96add90e | |
| parent | d30a40c59918736dafb2631bd332c5ef81b5cecc (diff) | |
| parent | f4a58dc484eb1464857644e79d6e46b9b36d2f0f (diff) | |
| download | mullvadvpn-4097b39627b618eb95cfad290e20aea6ab32fd29.tar.xz mullvadvpn-4097b39627b618eb95cfad290e20aea6ab32fd29.zip | |
Merge branch 'add-operations'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 98 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/AnyOperationObserver.swift | 15 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/AssociatedValue.swift | 31 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/AsyncBlockOperation.swift | 26 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/AsyncOperation.swift | 142 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/DelayOperation.swift | 48 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/ExclusivityController.swift | 69 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/InputOperation.swift | 103 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/OperationBlockObserver.swift | 33 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/OperationObserver.swift | 17 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/OperationProtocol.swift | 20 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/OutputOperation.swift | 50 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/ResultOperation.swift | 63 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/RetryOperation.swift | 120 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/TransformOperation.swift | 50 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/TransformOperationObserver.swift | 39 |
16 files changed, 924 insertions, 0 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index fed0afca2b..307a474a1b 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -10,6 +10,34 @@ 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; 5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; }; 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; + 580EE20124B321D500F9D8A1 /* OperationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */; }; + 580EE20224B321DB00F9D8A1 /* OperationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */; }; + 580EE20424B321EC00F9D8A1 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */; }; + 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; }; + 580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; }; + 580EE20924B3224200F9D8A1 /* RetryOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20824B3224200F9D8A1 /* RetryOperation.swift */; }; + 580EE20A24B3224200F9D8A1 /* RetryOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20824B3224200F9D8A1 /* RetryOperation.swift */; }; + 580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */; }; + 580EE20D24B3225F00F9D8A1 /* DelayOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */; }; + 580EE20F24B322E700F9D8A1 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */; }; + 580EE21024B322E700F9D8A1 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */; }; + 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */; }; + 580EE21324B322FC00F9D8A1 /* ResultOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */; }; + 580EE21524B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */; }; + 580EE21624B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */; }; + 580EE21824B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */; }; + 580EE21924B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */; }; + 580EE21B24B3236900F9D8A1 /* InputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21A24B3236900F9D8A1 /* InputOperation.swift */; }; + 580EE21C24B3236900F9D8A1 /* InputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21A24B3236900F9D8A1 /* InputOperation.swift */; }; + 580EE21E24B3237F00F9D8A1 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */; }; + 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */; }; + 580EE22124B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */; }; + 580EE22224B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */; }; + 580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; }; + 580EE22524B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; }; + 580EE22624B3245600F9D8A1 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */; }; + 580EE22824B3289300F9D8A1 /* AssociatedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */; }; + 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */; }; 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; }; 581CBCE62296B97300727D7F /* ViewControllerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCE52296B97300727D7F /* ViewControllerIdentifier.swift */; }; 581CBCEC2298041B00727D7F /* SettingsAppVersionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581CBCEB2298041B00727D7F /* SettingsAppVersionCell.swift */; }; @@ -71,6 +99,7 @@ 5888AD89227B18C40051EB06 /* RelayList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD88227B18C40051EB06 /* RelayList.swift */; }; 588AE72F2362001F009F9F2E /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */; }; 588AE730236200E2009F9F2E /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */; }; + 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */; }; 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; 5896AE7F246ACE76005B36CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; @@ -138,6 +167,7 @@ 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; 58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */; }; 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; }; + 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; 58F840AF2464382C0044E708 /* KeychainItemRevision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840AE2464382C0044E708 /* KeychainItemRevision.swift */; }; 58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840AE2464382C0044E708 /* KeychainItemRevision.swift */; }; 58F840B22464491D0044E708 /* ChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F840B12464491D0044E708 /* ChainedError.swift */; }; @@ -203,6 +233,20 @@ /* Begin PBXFileReference section */ 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>"; }; + 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationProtocol.swift; sourceTree = "<group>"; }; + 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserver.swift; sourceTree = "<group>"; }; + 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclusivityController.swift; sourceTree = "<group>"; }; + 580EE20824B3224200F9D8A1 /* RetryOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryOperation.swift; sourceTree = "<group>"; }; + 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayOperation.swift; sourceTree = "<group>"; }; + 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; }; + 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultOperation.swift; sourceTree = "<group>"; }; + 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserver.swift; sourceTree = "<group>"; }; + 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOperationObserver.swift; sourceTree = "<group>"; }; + 580EE21A24B3236900F9D8A1 /* InputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputOperation.swift; sourceTree = "<group>"; }; + 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; }; + 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransformOperationObserver.swift; sourceTree = "<group>"; }; + 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; }; + 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedValue.swift; sourceTree = "<group>"; }; 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+Debug.swift"; sourceTree = "<group>"; }; 581CBCE52296B97300727D7F /* ViewControllerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerIdentifier.swift; sourceTree = "<group>"; }; 581CBCEB2298041B00727D7F /* SettingsAppVersionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionCell.swift; sourceTree = "<group>"; }; @@ -300,6 +344,7 @@ 58D0C79F23F1CECF00FE9BA7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; }; 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManager.swift; sourceTree = "<group>"; }; + 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; }; 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+MullvadVersion.swift"; sourceTree = "<group>"; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; }; 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = "<group>"; }; @@ -365,6 +410,28 @@ name = Frameworks; sourceTree = "<group>"; }; + 580EE1FF24B3218800F9D8A1 /* Operations */ = { + isa = PBXGroup; + children = ( + 580EE21724B3235100F9D8A1 /* AnyOperationObserver.swift */, + 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */, + 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */, + 58E973DD24850EB600096F90 /* AsyncOperation.swift */, + 580EE20B24B3225F00F9D8A1 /* DelayOperation.swift */, + 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */, + 580EE21A24B3236900F9D8A1 /* InputOperation.swift */, + 580EE21424B3231200F9D8A1 /* OperationBlockObserver.swift */, + 580EE20324B321EC00F9D8A1 /* OperationObserver.swift */, + 580EE20024B321D500F9D8A1 /* OperationProtocol.swift */, + 580EE21D24B3237F00F9D8A1 /* OutputOperation.swift */, + 580EE21124B322FC00F9D8A1 /* ResultOperation.swift */, + 580EE20824B3224200F9D8A1 /* RetryOperation.swift */, + 580EE20E24B322E700F9D8A1 /* TransformOperation.swift */, + 580EE22024B3240100F9D8A1 /* TransformOperationObserver.swift */, + ); + path = Operations; + sourceTree = "<group>"; + }; 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = { isa = PBXGroup; children = ( @@ -457,6 +524,7 @@ 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */, 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */, 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */, + 580EE1FF24B3218800F9D8A1 /* Operations */, 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */, 58BFA5C522A7C97F00A6173D /* RelayCache.swift */, 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */, @@ -803,8 +871,10 @@ files = ( 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */, + 580EE21E24B3237F00F9D8A1 /* OutputOperation.swift in Sources */, 5840250122B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, 58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */, + 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */, 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 58FD5BE92419406000112C88 /* SKRequestPublisher.swift in Sources */, @@ -812,14 +882,18 @@ 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */, 58CCA010224249A1004F3011 /* ConnectViewController.swift in Sources */, 581CBCE62296B97300727D7F /* ViewControllerIdentifier.swift in Sources */, + 580EE21524B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */, 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */, 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, + 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */, 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */, 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */, 58C6B35422BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */, + 580EE20924B3224200F9D8A1 /* RetryOperation.swift in Sources */, 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, 58FAEDF7245088E100CB0F5B /* Keychain.swift in Sources */, 5888AD87227B17950051EB06 /* SelectLocationController.swift in Sources */, + 580EE20424B321EC00F9D8A1 /* OperationObserver.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, 584E96BA240D791E00D3334F /* CancellableDelayPublisher.swift in Sources */, 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */, @@ -839,18 +913,22 @@ 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */, 584E96BC240FD4DA00D3334F /* Location.swift in Sources */, 58ADDB3E227B1CD900FAFEA7 /* MullvadRpc.swift in Sources */, + 580EE20F24B322E700F9D8A1 /* TransformOperation.swift in Sources */, 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */, 587B08E0229433EB000E6F17 /* LoginState.swift in Sources */, 58C6B34F22BB7AC0003C19AD /* IPAddressRange.swift in Sources */, 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */, + 580EE22124B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */, 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */, 5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */, 582650862384116F00FA7A86 /* ReplaceNilWithError.swift in Sources */, 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */, + 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */, 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, + 580EE21B24B3236900F9D8A1 /* InputOperation.swift in Sources */, 5877152E23981C5B001F8237 /* SettingsBasicCell.swift in Sources */, 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, @@ -859,8 +937,10 @@ 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, 58C6B35E22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */, 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */, + 580EE22824B3289300F9D8A1 /* AssociatedValue.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, 58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */, + 580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */, 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */, 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */, 5888AD7F2279B6BF0051EB06 /* RelayStatusIndicatorView.swift in Sources */, @@ -871,10 +951,12 @@ 58A8BE8323A0F362006B74AC /* UIAlertController+Error.swift in Sources */, 58F840AF2464382C0044E708 /* KeychainItemRevision.swift in Sources */, 587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */, + 580EE20124B321D500F9D8A1 /* OperationProtocol.swift in Sources */, 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, 588AE72F2362001F009F9F2E /* MutuallyExclusive.swift in Sources */, 5888AD89227B18C40051EB06 /* RelayList.swift in Sources */, 587AD7C623421D7000E93A53 /* TunnelConfiguration.swift in Sources */, + 580EE21824B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */, 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */, @@ -882,6 +964,7 @@ 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, + 580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -891,19 +974,26 @@ files = ( 5845F83C236C72E300B2D93C /* AutoDisposableSink.swift in Sources */, 5860F1C423A8D25F00CEA666 /* WireguardConfiguration.swift in Sources */, + 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */, + 580EE20224B321DB00F9D8A1 /* OperationProtocol.swift in Sources */, 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */, 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */, 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 58BFA5C222A7C92900A6173D /* JsonRpc.swift in Sources */, 588AE730236200E2009F9F2E /* MutuallyExclusive.swift in Sources */, + 580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */, 58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */, 58C6B35122BB7CFD003C19AD /* IPAddressRange.swift in Sources */, 587AD7C723421D8600E93A53 /* TunnelConfiguration.swift in Sources */, + 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */, + 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */, 58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */, 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 587AD7C82342237300E93A53 /* TunnelManager.swift in Sources */, 5840250222B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */, + 580EE21924B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */, + 580EE21324B322FC00F9D8A1 /* ResultOperation.swift in Sources */, 58C6B36522C10596003C19AD /* AnyIPEndpoint+Wireguard.swift in Sources */, 58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */, 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, @@ -912,6 +1002,7 @@ 58B8743B22B788D200015324 /* PacketTunnelSettingsGenerator.swift in Sources */, 5860F1EB23AA4CF300CEA666 /* Logging.swift in Sources */, 5860F1C223A785C600CEA666 /* WireguardDevice.swift in Sources */, + 580EE21624B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */, 58C6B35522BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */, 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */, 58AEEF6C2344A49D00C9BBD5 /* TunnelConfigurationManager.swift in Sources */, @@ -920,18 +1011,25 @@ 582650872384117900FA7A86 /* ReplaceNilWithError.swift in Sources */, 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */, 58BFA5C022A7C8A900A6173D /* MullvadRpc.swift in Sources */, + 580EE21024B322E700F9D8A1 /* TransformOperation.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, 58FAEDF8245088E100CB0F5B /* Keychain.swift in Sources */, 58C6B36122C0EC82003C19AD /* AnyIPEndpoint+DNS64.swift in Sources */, 58F840B32464491D0044E708 /* ChainedError.swift in Sources */, + 580EE20A24B3224200F9D8A1 /* RetryOperation.swift in Sources */, 58C6B36722C106FC003C19AD /* WireguardCommand.swift in Sources */, 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, + 580EE22524B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */, + 580EE20D24B3225F00F9D8A1 /* DelayOperation.swift in Sources */, 588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */, 584B26FF237435A90073B10E /* RelaySelector+RelayCache.swift in Sources */, 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */, 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */, + 580EE21C24B3236900F9D8A1 /* InputOperation.swift in Sources */, + 580EE22224B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */, 5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */, + 580EE22624B3245600F9D8A1 /* OperationObserver.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/MullvadVPN/Operations/AnyOperationObserver.swift b/ios/MullvadVPN/Operations/AnyOperationObserver.swift new file mode 100644 index 0000000000..72f98a92f9 --- /dev/null +++ b/ios/MullvadVPN/Operations/AnyOperationObserver.swift @@ -0,0 +1,15 @@ +// +// AnyOperationObserver.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class AnyOperationObserver<OperationType: OperationProtocol>: OperationBlockObserver<OperationType> { + init<T: OperationObserver>(_ observer: T) where T.OperationType == OperationType { + super.init(willFinish: observer.operationWillFinish, didFinish: observer.operationDidFinish) + } +} diff --git a/ios/MullvadVPN/Operations/AssociatedValue.swift b/ios/MullvadVPN/Operations/AssociatedValue.swift new file mode 100644 index 0000000000..0f197086de --- /dev/null +++ b/ios/MullvadVPN/Operations/AssociatedValue.swift @@ -0,0 +1,31 @@ +// +// AssociatedValue.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// A container type for storing associated values +final class AssociatedValue<T>: NSObject { + let value: T + init(_ value: T) { + self.value = value + } + + class func get(object: Any, key: UnsafeRawPointer) -> T? { + let container = objc_getAssociatedObject(object, key) as? Self + return container?.value + } + + class func set(object: Any, key: UnsafeRawPointer, value: T?) { + objc_setAssociatedObject( + object, + key, + value.flatMap { AssociatedValue($0) }, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } +} diff --git a/ios/MullvadVPN/Operations/AsyncBlockOperation.swift b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift new file mode 100644 index 0000000000..c8f4287b32 --- /dev/null +++ b/ios/MullvadVPN/Operations/AsyncBlockOperation.swift @@ -0,0 +1,26 @@ +// +// AsyncBlockOperation.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Asynchronous block operation +class AsyncBlockOperation: AsyncOperation { + private let block: (@escaping () -> Void) -> Void + + init(_ block: @escaping (@escaping () -> Void) -> Void) { + self.block = block + super.init() + } + + override func main() { + self.block { [weak self] in + self?.finish() + } + } +} + diff --git a/ios/MullvadVPN/Operations/AsyncOperation.swift b/ios/MullvadVPN/Operations/AsyncOperation.swift new file mode 100644 index 0000000000..98a5f8aa39 --- /dev/null +++ b/ios/MullvadVPN/Operations/AsyncOperation.swift @@ -0,0 +1,142 @@ +// +// AsyncOperation.swift +// MullvadVPN +// +// Created by pronebird on 01/06/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// A base implementation of an asynchronous operation +class AsyncOperation: Operation, OperationProtocol { + + /// A state lock used for manipulating the operation state flags in a thread safe fashion. + fileprivate let stateLock = NSRecursiveLock() + + /// Operation state flags. + private var _isExecuting = false + private var _isFinished = false + private var _isCancelled = false + + final override var isExecuting: Bool { + return stateLock.withCriticalBlock { _isExecuting } + } + + final override var isFinished: Bool { + return stateLock.withCriticalBlock { _isFinished } + } + + final override var isCancelled: Bool { + return stateLock.withCriticalBlock { _isCancelled } + } + + final override var isAsynchronous: Bool { + return true + } + + final override func start() { + stateLock.withCriticalBlock { + if self._isCancelled { + self.finish() + } else { + self.setExecuting(true) + self.main() + } + } + } + + override func main() { + // Override in subclasses + } + + /// Cancel operation + /// Subclasses should override `operationDidCancel` instead + final override func cancel() { + stateLock.withCriticalBlock { + if !self._isCancelled { + self.setCancelled(true) + + if self._isExecuting { + self.operationDidCancel() + } + } + } + } + + /// Override in subclasses to support task cancellation. + /// Subclasses should call `finish()` to complete the operation + func operationDidCancel() { + // no-op + } + + final func finish() { + stateLock.withCriticalBlock { + if !self._isFinished { + self.observers.forEach { $0.operationWillFinish(self) } + } + + if self._isExecuting { + self.setExecuting(false) + } + + if !self._isFinished { + self.setFinished(true) + self.observers.forEach { $0.operationDidFinish(self) } + } + } + } + + private func setExecuting(_ value: Bool) { + willChangeValue(for: \.isExecuting) + _isExecuting = value + didChangeValue(for: \.isExecuting) + } + + private func setFinished(_ value: Bool) { + willChangeValue(for: \.isFinished) + _isFinished = value + didChangeValue(for: \.isFinished) + } + + private func setCancelled(_ value: Bool) { + willChangeValue(for: \.isCancelled) + _isCancelled = value + didChangeValue(for: \.isCancelled) + } + + // MARK: - Observation + + /// The operation observers. + fileprivate var observers: [AnyOperationObserver<AsyncOperation>] = [] + + /// Add type-erased operation observer + fileprivate func addAnyObserver(_ observer: AnyOperationObserver<AsyncOperation>) { + stateLock.withCriticalBlock { + self.observers.append(observer) + } + } +} + +/// This extension exists because Swift has some issues to infer the +extension OperationProtocol where Self: AsyncOperation { + func addObserver<T: OperationObserver>(_ observer: T) where T.OperationType == Self { + let transform = TransformOperationObserver<AsyncOperation>(observer) + let wrapped = AnyOperationObserver(transform) + addAnyObserver(wrapped) + } +} + + +protocol OperationSubclassing { + /// Use this method in subclasses or extensions where you would like to synchronize + /// the class members access using the same lock used for guarding from race conditions + /// when managing operation state. + func synchronized<T>(_ body: () -> T) -> T +} + +extension AsyncOperation: OperationSubclassing { + func synchronized<T>(_ body: () -> T) -> T { + return stateLock.withCriticalBlock(body) + } +} diff --git a/ios/MullvadVPN/Operations/DelayOperation.swift b/ios/MullvadVPN/Operations/DelayOperation.swift new file mode 100644 index 0000000000..80719b573d --- /dev/null +++ b/ios/MullvadVPN/Operations/DelayOperation.swift @@ -0,0 +1,48 @@ +// +// DelayOperation.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +enum DelayTimerType { + case deadline + case walltime +} + +class DelayOperation: AsyncOperation { + private let delay: TimeInterval + private let timerType: DelayTimerType + private var timer: DispatchSourceTimer? + + init(delay: TimeInterval, timerType: DelayTimerType) { + self.delay = delay + self.timerType = timerType + } + + override func main() { + let timer = DispatchSource.makeTimerSource() + timer.setEventHandler { [weak self] in + self?.finish() + } + + switch timerType { + case .deadline: + timer.schedule(deadline: DispatchTime.now() + delay) + case .walltime: + timer.schedule(wallDeadline: DispatchWallTime.now() + delay) + } + + self.timer = timer + timer.activate() + } + + override func operationDidCancel() { + timer?.cancel() + timer = nil + finish() + } +} diff --git a/ios/MullvadVPN/Operations/ExclusivityController.swift b/ios/MullvadVPN/Operations/ExclusivityController.swift new file mode 100644 index 0000000000..9e18516869 --- /dev/null +++ b/ios/MullvadVPN/Operations/ExclusivityController.swift @@ -0,0 +1,69 @@ +// +// ExclusivityController.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class ExclusivityController<Category> where Category: Hashable { + private let operationQueue: OperationQueue + private let lock = NSRecursiveLock() + + private var operations: [Category: [Operation]] = [:] + private var observers: [Operation: NSObjectProtocol] = [:] + + init(operationQueue: OperationQueue) { + self.operationQueue = operationQueue + } + + func addOperation(_ operation: Operation, categories: [Category]) { + addOperations([operation], categories: categories) + } + + func addOperations(_ operations: [Operation], categories: [Category]) { + lock.withCriticalBlock { + for operation in operations { + for category in categories { + addDependencies(operation: operation, category: category) + } + + observers[operation] = operation.observe(\.isFinished, options: [.initial, .new]) { [weak self] (op, change) in + if let isFinished = change.newValue, isFinished { + self?.operationDidFinish(op, categories: categories) + } + } + } + + operationQueue.addOperations(operations, waitUntilFinished: false) + } + } + + private func addDependencies(operation: Operation, category: Category) { + var exclusiveOperations = self.operations[category] ?? [] + + if let dependency = exclusiveOperations.last, !operation.dependencies.contains(dependency) { + operation.addDependency(dependency) + } + + exclusiveOperations.append(operation) + self.operations[category] = exclusiveOperations + } + + private func operationDidFinish(_ operation: Operation, categories: [Category]) { + lock.withCriticalBlock { + for category in categories { + var exclusiveOperations = self.operations[category] ?? [] + + exclusiveOperations.removeAll { (storedOperation) -> Bool in + return operation == storedOperation + } + + self.operations[category] = exclusiveOperations + } + self.observers.removeValue(forKey: operation) + } + } +} diff --git a/ios/MullvadVPN/Operations/InputOperation.swift b/ios/MullvadVPN/Operations/InputOperation.swift new file mode 100644 index 0000000000..a1c9d6931d --- /dev/null +++ b/ios/MullvadVPN/Operations/InputOperation.swift @@ -0,0 +1,103 @@ +// +// InputOperation.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol InputOperation: OperationProtocol { + associatedtype Input + + /// When overriding `input` in Subclasses, make sure to call `operationDidSetInput` + var input: Input? { get set } + + func operationDidSetInput(_ input: Input?) +} + +private var kInputOperationAssociatedValue = 0 +extension InputOperation where Self: OperationSubclassing { + var input: Input? { + get { + return synchronized { + return AssociatedValue.get(object: self, key: &kInputOperationAssociatedValue) + } + } + set { + synchronized { + AssociatedValue.set(object: self, key: &kInputOperationAssociatedValue, value: newValue) + + operationDidSetInput(newValue) + } + } + } + + func operationDidSetInput(_ input: Input?) { + // Override in subclasses + } +} + +extension InputOperation { + + @discardableResult func inject<Dependency>(from dependency: Dependency, via block: @escaping (Dependency.Output) -> Input?) -> Self + where Dependency: OutputOperation + { + let observer = OperationBlockObserver<Dependency>(willFinish: { [weak self] (operation) in + guard let self = self else { return } + + if let output = operation.output { + self.input = block(output) + } + }) + dependency.addObserver(observer) + addDependency(dependency) + + return self + } + + @discardableResult func injectResult<Dependency>(from dependency: Dependency) -> Self + where Dependency: OutputOperation, Dependency.Output == Input? + { + return self.inject(from: dependency, via: { $0 }) + } + + /// Inject input from operation that outputs `Result<Input, Failure>` + @discardableResult func injectResult<Dependency, Failure>(from dependency: Dependency) -> Self + where Dependency: OutputOperation, Failure: Error, Dependency.Output == Result<Input, Failure> + { + return self.inject(from: dependency) { (output) -> Input? in + switch output { + case .success(let value): + return value + case .failure: + return nil + } + } + } + + /// Inject input from operation that outputs `Result<Input, Never>` + @discardableResult func injectResult<Dependency>(from dependency: Dependency) -> Self + where Dependency: OutputOperation, Dependency.Output == Result<Input, Never> + { + return self.inject(from: dependency) { (output) -> Input? in + switch output { + case .success(let value): + return value + } + } + } + + /// Inject input from operation that outputs `Result<Input?, Never>` + @discardableResult func injectResult<Dependency>(from dependency: Dependency) -> Self + where Dependency: OutputOperation, Dependency.Output == Result<Input?, Never> + { + return self.inject(from: dependency) { (output) -> Input? in + switch output { + case .success(let value): + return value + } + } + } +} diff --git a/ios/MullvadVPN/Operations/OperationBlockObserver.swift b/ios/MullvadVPN/Operations/OperationBlockObserver.swift new file mode 100644 index 0000000000..728e9a5a02 --- /dev/null +++ b/ios/MullvadVPN/Operations/OperationBlockObserver.swift @@ -0,0 +1,33 @@ +// +// OperationBlockObserver.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class OperationBlockObserver<OperationType: OperationProtocol>: OperationObserver { + private var willFinish: ((OperationType) -> Void)? + private var didFinish: ((OperationType) -> Void)? + + init(willFinish: ((OperationType) -> Void)? = nil, didFinish: ((OperationType) -> Void)? = nil) { + self.willFinish = willFinish + self.didFinish = didFinish + } + + func operationWillFinish(_ operation: OperationType) { + self.willFinish?(operation) + } + + func operationDidFinish(_ operation: OperationType) { + self.didFinish?(operation) + } +} + +extension OperationProtocol { + func addDidFinishBlockObserver(_ block: @escaping (Self) -> Void) { + addObserver(OperationBlockObserver(didFinish: block)) + } +} diff --git a/ios/MullvadVPN/Operations/OperationObserver.swift b/ios/MullvadVPN/Operations/OperationObserver.swift new file mode 100644 index 0000000000..295b993a10 --- /dev/null +++ b/ios/MullvadVPN/Operations/OperationObserver.swift @@ -0,0 +1,17 @@ +// +// OperationObserver.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol OperationObserver { + associatedtype OperationType: OperationProtocol + + func operationWillFinish(_ operation: OperationType) + func operationDidFinish(_ operation: OperationType) +} + diff --git a/ios/MullvadVPN/Operations/OperationProtocol.swift b/ios/MullvadVPN/Operations/OperationProtocol.swift new file mode 100644 index 0000000000..a41102c265 --- /dev/null +++ b/ios/MullvadVPN/Operations/OperationProtocol.swift @@ -0,0 +1,20 @@ +// +// OperationProtocol.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol OperationProtocol: Operation { + /// Add operation observer + func addObserver<T: OperationObserver>(_ observer: T) where T.OperationType == Self + + /// Finish operation + func finish() + + /// Cancel operation + func cancel() +} diff --git a/ios/MullvadVPN/Operations/OutputOperation.swift b/ios/MullvadVPN/Operations/OutputOperation.swift new file mode 100644 index 0000000000..533d5e5151 --- /dev/null +++ b/ios/MullvadVPN/Operations/OutputOperation.swift @@ -0,0 +1,50 @@ +// +// OutputOperation.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol OutputOperation: OperationProtocol { + associatedtype Output + + var output: Output? { get set } + + func finish(with output: Output) +} + +extension OutputOperation { + func finish(with output: Output) { + self.output = output + self.finish() + } +} + +private var kOutputOperationAssociatedValue = 0 +extension OutputOperation where Self: OperationSubclassing { + var output: Output? { + get { + return synchronized { + return AssociatedValue.get(object: self, key: &kOutputOperationAssociatedValue) + } + } + set { + synchronized { + AssociatedValue.set(object: self, key: &kOutputOperationAssociatedValue, value: newValue) + } + } + } +} + +extension OperationProtocol where Self: OutputOperation { + func addDidFinishBlockObserver(_ block: @escaping (Self, Output) -> Void) { + addDidFinishBlockObserver { (operation) in + if let output = operation.output { + block(operation, output) + } + } + } +} diff --git a/ios/MullvadVPN/Operations/ResultOperation.swift b/ios/MullvadVPN/Operations/ResultOperation.swift new file mode 100644 index 0000000000..7ce9c20358 --- /dev/null +++ b/ios/MullvadVPN/Operations/ResultOperation.swift @@ -0,0 +1,63 @@ +// +// ResultOperation.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class ResultOperation<Success, Failure: Error>: AsyncOperation, OutputOperation { + typealias Output = Result<Success, Failure> + + private enum Executor { + case callback((@escaping (Result<Success, Failure>) -> Void) -> Void) + case transform(() -> Result<Success, Failure>) + } + + private let executor: Executor + + private init(_ executor: Executor) { + self.executor = executor + } + + convenience init(_ block: @escaping (@escaping (Output) -> Void) -> Void) { + self.init(.callback(block)) + } + + convenience init(_ block: @escaping () -> Output) { + self.init(.transform(block)) + } + + override func main() { + switch executor { + case .callback(let block): + block { [weak self] (result) in + self?.finish(with: result) + } + + case .transform(let block): + self.finish(with: block()) + } + } + +} + +extension ResultOperation where Failure == Never { + /// A convenience initializer for infallible `ResultOperation` that automatically wraps the + /// return value of the given closure into `Result<Success, Never>` + convenience init(_ block: @escaping () -> Success) { + self.init(.transform({ .success(block()) })) + } + + /// A convenience initializer for infallible `ResultOperation` that automatically wraps the + /// value, passed to the given closure, into `Result<Success, Never>` + convenience init(_ block: @escaping (@escaping (Success) -> Void) -> Void) { + self.init(.callback({ (finish) in + block { + finish(.success($0)) + } + })) + } +} diff --git a/ios/MullvadVPN/Operations/RetryOperation.swift b/ios/MullvadVPN/Operations/RetryOperation.swift new file mode 100644 index 0000000000..1e73e81d79 --- /dev/null +++ b/ios/MullvadVPN/Operations/RetryOperation.swift @@ -0,0 +1,120 @@ +// +// RetryOperation.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +enum WaitStrategy { + case immediate + case constant(TimeInterval) + + var iterator: AnyIterator<TimeInterval> { + switch self { + case .immediate: + return AnyIterator { .zero } + case .constant(let constant): + return AnyIterator { constant } + } + } +} + +struct RetryStrategy { + var maxRetries: Int + var waitStrategy: WaitStrategy + var waitTimerType: DelayTimerType +} + +class RetryOperation<OperationType, Success, Failure: Error>: AsyncOperation, OutputOperation + where OperationType: OutputOperation, OperationType.Output == Result<Success, Failure> +{ + typealias Output = OperationType.Output + + private let operationQueue = OperationQueue() + + private let producer: () -> OperationType + private let delayIterator: AnyIterator<TimeInterval> + + private var retryCount: Int = 0 + private let retryStrategy: RetryStrategy + + private var childConfigurator: ((OperationType) -> Void)? + + init(underlyingQueue: DispatchQueue? = nil, strategy: RetryStrategy, producer: @escaping () -> OperationType) { + operationQueue.underlyingQueue = underlyingQueue + delayIterator = strategy.waitStrategy.iterator + retryStrategy = strategy + self.producer = producer + } + + override func main() { + retry() + } + + override func operationDidCancel() { + operationQueue.cancelAllOperations() + } + + private func retry(delay: TimeInterval? = nil) { + let child = producer() + + child.addDidFinishBlockObserver { [weak self] (operation) in + guard let self = self else { return } + + // Operation finished without output set? + guard let result = operation.output else { + self.finish() + return + } + + self.synchronized { + guard case .failure(let error) = result, + let delay = self.delayIterator.next(), + self.shouldRetry(error: error) else { + self.finish(with: result) + return + } + + self.retryCount += 1 + self.retry(delay: delay) + } + } + + synchronized { + childConfigurator?(child) + } + + if let delay = delay { + let delayOperation = DelayOperation(delay: delay, timerType: retryStrategy.waitTimerType) + + child.addDependency(delayOperation) + operationQueue.addOperation(delayOperation) + } + + operationQueue.addOperation(child) + } + + private func setChildConfigurator(_ body: @escaping (OperationType) -> Void) { + synchronized { + self.childConfigurator = body + } + } + + private func shouldRetry(error: Failure) -> Bool { + return retryCount < retryStrategy.maxRetries && !self.isCancelled + } + +} + +extension RetryOperation: InputOperation where OperationType: InputOperation { + typealias Input = OperationType.Input + + func operationDidSetInput(_ input: OperationType.Input?) { + setChildConfigurator { (child) in + child.input = input + } + } +} diff --git a/ios/MullvadVPN/Operations/TransformOperation.swift b/ios/MullvadVPN/Operations/TransformOperation.swift new file mode 100644 index 0000000000..ce2a1617ea --- /dev/null +++ b/ios/MullvadVPN/Operations/TransformOperation.swift @@ -0,0 +1,50 @@ +// +// TransformOperation.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +class TransformOperation<Input, Output>: AsyncOperation, InputOperation, OutputOperation { + private enum Executor { + case callback((Input, @escaping (Output) -> Void) -> Void) + case transform((Input) -> Output) + } + + private let executor: Executor + + private init(input: Input? = nil, executor: Executor) { + self.executor = executor + + super.init() + self.input = input + } + + convenience init(input: Input? = nil, _ block: @escaping (Input, @escaping (Output) -> Void) -> Void) { + self.init(input: input, executor: .callback(block)) + } + + convenience init(input: Input? = nil, _ block: @escaping (Input) -> Output) { + self.init(input: input, executor: .transform(block)) + } + + override func main() { + guard let input = input else { + self.finish() + return + } + + switch executor { + case .callback(let block): + block(input) { [weak self] (result) in + self?.finish(with: result) + } + + case .transform(let block): + self.finish(with: block(input)) + } + } +} diff --git a/ios/MullvadVPN/Operations/TransformOperationObserver.swift b/ios/MullvadVPN/Operations/TransformOperationObserver.swift new file mode 100644 index 0000000000..48fbe02dbb --- /dev/null +++ b/ios/MullvadVPN/Operations/TransformOperationObserver.swift @@ -0,0 +1,39 @@ +// +// TransformOperationObserver.swift +// MullvadVPN +// +// Created by pronebird on 06/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// A private type erasing observer that type casts the input operation type to the expected +/// operation type before calling the wrapped observer +class TransformOperationObserver<S: OperationProtocol>: OperationObserver { + private let willFinish: (S) -> Void + private let didFinish: (S) -> Void + + init<T: OperationObserver>(_ observer: T) { + willFinish = Self.wrap(observer.operationWillFinish) + didFinish = Self.wrap(observer.operationDidFinish) + } + + func operationWillFinish(_ operation: S) { + willFinish(operation) + } + + func operationDidFinish(_ operation: S) { + didFinish(operation) + } + + private class func wrap<U>(_ body: @escaping (U) -> Void) -> (S) -> Void { + return { (operation: S) in + if let transformed = operation as? U { + body(transformed) + } else { + fatalError("\(Self.self) failed to cast \(S.self) to \(U.self)") + } + } + } +} |
