summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-01-28 10:44:38 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-02-01 11:04:15 +0100
commit9837e20d4dcad10251965e41eca9146683887791 (patch)
tree942cf777d3e568d71e296ba095c85323d67cc317
parentee5d1d93b0ef5b53d2c3d0b8587a3cad6b931820 (diff)
downloadmullvadvpn-9837e20d4dcad10251965e41eca9146683887791.tar.xz
mullvadvpn-9837e20d4dcad10251965e41eca9146683887791.zip
Add IPC timeout
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/Operations/OperationCompletion.swift11
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCError.swift39
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift232
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift88
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift51
-rw-r--r--ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift96
7 files changed, 348 insertions, 173 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index fe92e0b56c..0cf24d4fb1 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -162,6 +162,7 @@
5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; };
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */; };
586ADD4723FC13F400CE9E87 /* countries.geo.json in Resources */ = {isa = PBXBuildFile; fileRef = 586ADD4523FC13F400CE9E87 /* countries.geo.json */; };
+ 586E54FB27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E54FA27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift */; };
5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB8225498CA20051A0A4 /* Swizzle.swift */; };
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; };
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; };
@@ -466,6 +467,7 @@
5868585424054096000B8131 /* AppButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; };
5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = "<group>"; };
586ADD4523FC13F400CE9E87 /* countries.geo.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = countries.geo.json; sourceTree = "<group>"; };
+ 586E54FA27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCRequestOperation.swift; sourceTree = "<group>"; };
5871FB8225498CA20051A0A4 /* Swizzle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swizzle.swift; sourceTree = "<group>"; };
5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLog.swift; sourceTree = "<group>"; };
5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+IPAddress.swift"; sourceTree = "<group>"; };
@@ -805,6 +807,7 @@
585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */,
585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */,
5875960926F371FC00BF6711 /* TunnelIPCSession.swift */,
+ 586E54FA27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift */,
);
path = TunnelIPC;
sourceTree = "<group>";
@@ -1507,6 +1510,7 @@
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
+ 586E54FB27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift in Sources */,
584592612639B4A200EF967F /* ConsentContentView.swift in Sources */,
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */,
5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */,
diff --git a/ios/MullvadVPN/Operations/OperationCompletion.swift b/ios/MullvadVPN/Operations/OperationCompletion.swift
index 8c115540e3..23d206d32d 100644
--- a/ios/MullvadVPN/Operations/OperationCompletion.swift
+++ b/ios/MullvadVPN/Operations/OperationCompletion.swift
@@ -30,4 +30,15 @@ enum OperationCompletion<Success, Failure: Error> {
self = .failure(error)
}
}
+
+ func mapError<NewFailure: Error>(_ block: (Failure) -> NewFailure) -> OperationCompletion<Success, NewFailure> {
+ switch self {
+ case .success(let value):
+ return .success(value)
+ case .failure(let error):
+ return .failure(block(error))
+ case .cancelled:
+ return .cancelled
+ }
+ }
}
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCError.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCError.swift
index 8775ce1e2e..b2af509c80 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCError.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCError.swift
@@ -7,21 +7,22 @@
//
import Foundation
+import NetworkExtension
extension TunnelIPC {
/// An error type emitted by `TunnelIPC.Session`.
enum Error: ChainedError {
- /// A failure to encode the request
+ /// A failure to encode the request.
case encoding(Swift.Error)
- /// A failure to decode the response
+ /// A failure to decode the response.
case decoding(Swift.Error)
- /// A failure to send the IPC request
- case send(Swift.Error)
+ /// A failure to send the IPC request.
+ case send(TunnelIPC.SendError)
- /// A failure that's raised when the IPC response does not contain any data however the decoder
- /// expected to receive data for decoding
+ /// A failure that's raised when the IPC response does not contain any data however the
+ /// decoder expected to receive data for decoding.
case nilResponse
var errorDescription: String? {
@@ -31,10 +32,34 @@ extension TunnelIPC {
case .decoding:
return "Decoding failure"
case .send:
- return "Submission failure"
+ return "Send failure"
case .nilResponse:
return "Unexpected nil response from the tunnel"
}
}
}
+
+ enum SendError: ChainedError {
+ /// Tunnel process is either down or about to go down.
+ case tunnelDown(NEVPNStatus)
+
+ /// Timeout
+ case timeout
+
+ /// System error.
+ case system(Swift.Error)
+
+ var errorDescription: String? {
+ switch self {
+ case .tunnelDown(let status):
+ return "Tunnel is either down or about to go down (status: \(status))"
+
+ case .timeout:
+ return "Request timeout"
+
+ case .system:
+ return "System error"
+ }
+ }
+ }
}
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
new file mode 100644
index 0000000000..0b74f6c737
--- /dev/null
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift
@@ -0,0 +1,232 @@
+//
+// TunnelIPCRequestOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 27/01/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+
+extension TunnelIPC {
+
+ struct RequestOptions {
+ /// Wait until the tunnel transitioned from reasserting to connected state before sending
+ /// the request.
+ var waitIfReasserting: Bool
+
+ /// Timeout interval in seconds.
+ var timeout: TimeInterval = 5
+ }
+
+ final class RequestOperation<Output>: AsyncOperation {
+ typealias DecoderHandler = (Data?) -> Result<Output, TunnelIPC.Error>
+ typealias CompletionHandler = (OperationCompletion<Output, TunnelIPC.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let notificationQueue: OperationQueue
+
+ private let connection: VPNConnectionProtocol
+ private let request: TunnelIPC.Request
+ private let options: RequestOptions
+
+ private let decoderHandler: DecoderHandler
+ private var completionHandler: CompletionHandler?
+
+ private var statusObserver: NSObjectProtocol?
+ private var timeoutTimer: DispatchSourceTimer?
+
+ init(queue: DispatchQueue,
+ connection: VPNConnectionProtocol,
+ request: TunnelIPC.Request,
+ options: TunnelIPC.RequestOptions,
+ decoderHandler: @escaping DecoderHandler,
+ completionHandler: @escaping CompletionHandler)
+ {
+ self.queue = queue
+ self.notificationQueue = OperationQueue()
+ self.notificationQueue.underlyingQueue = queue
+
+ self.connection = connection
+ self.request = request
+ self.options = options
+
+ self.decoderHandler = decoderHandler
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute()
+ }
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ queue.async {
+ if self.isExecuting {
+ self.completeOperation(completion: .cancelled)
+ }
+ }
+ }
+
+ private func execute() {
+ guard !isCancelled else {
+ completeOperation(completion: .cancelled)
+ return
+ }
+
+ startTimeoutTimer()
+
+ statusObserver = NotificationCenter.default.addObserver(
+ forName: .NEVPNStatusDidChange,
+ object: connection,
+ queue: notificationQueue) { [weak self] notification in
+ guard let self = self else { return }
+ guard let connection = notification.object as? VPNConnectionProtocol else { return }
+
+ self.handleVPNStatus(connection.status)
+ }
+
+ handleVPNStatus(connection.status)
+ }
+
+ private func removeVPNStatusObserver() {
+ if let statusObserver = statusObserver {
+ NotificationCenter.default.removeObserver(statusObserver)
+ self.statusObserver = nil
+ }
+ }
+
+ private func startTimeoutTimer() {
+ let timer = DispatchSource.makeTimerSource(queue: queue)
+ timer.setEventHandler { [weak self] in
+ self?.completeOperation(completion: .failure(.send(.timeout)))
+ }
+
+ timer.schedule(wallDeadline: .now() + options.timeout)
+ timer.activate()
+
+ timeoutTimer = timer
+ }
+
+ private func stopTimeoutTimer() {
+ timeoutTimer?.cancel()
+ timeoutTimer = nil
+ }
+
+ private func handleVPNStatus(_ status: NEVPNStatus) {
+ guard !isCancelled else {
+ return
+ }
+
+ switch status {
+ case .connected:
+ sendRequest()
+
+ case .connecting:
+ // Sending IPC message while in connecting state may cause the tunnel process to
+ // freeze for no apparent reason.
+ break
+
+ case .reasserting:
+ if !options.waitIfReasserting {
+ sendRequest()
+ }
+
+ case .invalid, .disconnecting, .disconnected:
+ completeOperation(completion: .failure(.send(.tunnelDown(status))))
+
+ @unknown default:
+ break
+ }
+ }
+
+ private func sendRequest() {
+ let session = connection as! VPNTunnelProviderSessionProtocol
+
+ removeVPNStatusObserver()
+
+ let messageData: Data
+ do {
+ messageData = try TunnelIPC.Coding.encodeRequest(request)
+ } catch {
+ completeOperation(completion: .failure(.encoding(error)))
+ return
+ }
+
+ do {
+ try session.sendProviderMessage(messageData) { [weak self] responseData in
+ guard let self = self else { return }
+
+ self.queue.async {
+ let decodingResult = self.decoderHandler(responseData)
+
+ self.completeOperation(completion: OperationCompletion(result: decodingResult))
+ }
+ }
+ } catch {
+ completeOperation(completion: .failure(.send(.system(error))))
+ }
+ }
+
+ private func completeOperation(completion: OperationCompletion<Output, TunnelIPC.Error>) {
+ removeVPNStatusObserver()
+ stopTimeoutTimer()
+
+ completionHandler?(completion)
+ completionHandler = nil
+
+ finish()
+ }
+ }
+}
+
+extension TunnelIPC.RequestOperation where Output: Codable {
+ convenience init(
+ queue: DispatchQueue,
+ connection: VPNConnectionProtocol,
+ request: TunnelIPC.Request,
+ options: TunnelIPC.RequestOptions,
+ completionHandler: @escaping CompletionHandler
+ )
+ {
+ self.init(
+ queue: queue,
+ connection: connection,
+ request: request,
+ options: options,
+ decoderHandler: { data in
+ guard let data = data else {
+ return .failure(.nilResponse)
+ }
+
+ let result = Result { try TunnelIPC.Coding.decodeResponse(Output.self, from: data) }
+
+ return result.mapError { .decoding($0) }
+ },
+ completionHandler: completionHandler
+ )
+ }
+}
+
+extension TunnelIPC.RequestOperation where Output == Void {
+ convenience init(
+ queue: DispatchQueue,
+ connection: VPNConnectionProtocol,
+ request: TunnelIPC.Request,
+ options: TunnelIPC.RequestOptions,
+ completionHandler: @escaping CompletionHandler
+ ) {
+ self.init(
+ queue: queue,
+ connection: connection,
+ request: request,
+ options: options,
+ decoderHandler: { _ in .success(()) },
+ completionHandler: completionHandler
+ )
+ }
+}
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
index 4589eeb45f..4b12b15db4 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
@@ -7,78 +7,50 @@
//
import Foundation
+import NetworkExtension
extension TunnelIPC {
- /// Wrapper class around `NETunnelProviderSession` that provides convenient interface for interacting with the
- /// Packet Tunnel process.
+ /// Wrapper class around `NETunnelProviderSession` that provides convenient interface for
+ /// interacting with the Packet Tunnel process.
final class Session {
- private let tunnelProviderSession: VPNTunnelProviderSessionProtocol
+ private let connection: VPNConnectionProtocol
+ private let queue = DispatchQueue(label: "TunnelIPC.SessionQueue")
+ private let operationQueue = OperationQueue()
- init<T: VPNTunnelProviderManagerProtocol>(from tunnelProvider: T) {
- tunnelProviderSession = tunnelProvider.connection as! VPNTunnelProviderSessionProtocol
+ init(connection: VPNConnectionProtocol) {
+ self.connection = connection
}
- func reloadTunnelSettings(completionHandler: @escaping (TunnelIPC.Error?) -> Void) {
- send(message: .reloadTunnelSettings) { result in
- completionHandler(result.error)
- }
- }
-
- func getTunnelConnectionInfo(completionHandler: @escaping (Result<TunnelConnectionInfo?, TunnelIPC.Error>) -> Void) {
- send(message: .tunnelConnectionInfo) { result in
- completionHandler(result)
- }
- }
-
- // MARK: - Private
-
- private func send(message: TunnelIPC.Request, completionHandler: @escaping (Result<(), TunnelIPC.Error>) -> Void) {
- sendWithoutDecoding(message: message) { (result) in
- let result = result.map { _ in () }
+ func reloadTunnelSettings(completionHandler: @escaping (OperationCompletion<(), TunnelIPC.Error>) -> Void) -> Cancellable {
+ let operation = RequestOperation(
+ queue: queue,
+ connection: connection,
+ request: .reloadTunnelSettings,
+ options: TunnelIPC.RequestOptions(waitIfReasserting: true),
+ completionHandler: completionHandler
+ )
- completionHandler(result)
- }
- }
-
- private func send<T>(message: TunnelIPC.Request, completionHandler: @escaping (Result<T, TunnelIPC.Error>) -> Void) where T: Codable
- {
- sendWithoutDecoding(message: message) { (result) in
- let result = result.flatMap { (data) -> Result<T, TunnelIPC.Error> in
- guard let data = data else {
- return .failure(.nilResponse)
- }
-
- return Result { try TunnelIPC.Coding.decodeResponse(T.self, from: data) }
- .mapError { error in
- return .decoding(error)
- }
- }
+ operationQueue.addOperation(operation)
- completionHandler(result)
+ return AnyCancellable {
+ operation.cancel()
}
}
- private func sendWithoutDecoding(message: TunnelIPC.Request, completionHandler: @escaping (Result<Data?, TunnelIPC.Error>) -> Void) {
- do {
- let data = try TunnelIPC.Coding.encodeRequest(message)
+ func getTunnelConnectionInfo(completionHandler: @escaping (OperationCompletion<TunnelConnectionInfo?, TunnelIPC.Error>) -> Void) -> Cancellable {
+ let operation = RequestOperation<TunnelConnectionInfo?>(
+ queue: queue,
+ connection: connection,
+ request: .tunnelConnectionInfo,
+ options: TunnelIPC.RequestOptions(waitIfReasserting: false),
+ completionHandler: completionHandler
+ )
- sendProviderMessage(data) { (result) in
- completionHandler(result)
- }
- } catch {
- completionHandler(.failure(.encoding(error)))
- }
- }
+ operationQueue.addOperation(operation)
- private func sendProviderMessage(_ messageData: Data, completionHandler: @escaping (Result<Data?, TunnelIPC.Error>) -> Void) {
- do {
- try tunnelProviderSession.sendProviderMessage(messageData) { response in
- completionHandler(.success(response))
- }
- } catch {
- completionHandler(.failure(.send(error)))
+ return AnyCancellable {
+ operation.cancel()
}
}
-
}
}
diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
index 88002f7212..6292b831b7 100644
--- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
@@ -17,6 +17,7 @@ class MapConnectionStatusOperation: AsyncOperation {
private let state: TunnelManager.State
private let connectionStatus: NEVPNStatus
private var startTunnelHandler: StartTunnelHandler?
+ private var request: Cancellable?
private let logger = Logger(label: "TunnelManager.MapConnectionStatusOperation")
@@ -37,10 +38,7 @@ class MapConnectionStatusOperation: AsyncOperation {
super.cancel()
queue.async {
- // Finish immediately if cancelled during execution.
- if self.isExecuting {
- self.finish()
- }
+ self.request?.cancel()
}
}
@@ -62,27 +60,35 @@ class MapConnectionStatusOperation: AsyncOperation {
}
case .reasserting:
- requestTunnelRelay(from: tunnelProvider) { [weak self] result in
+ let session = TunnelIPC.Session(connection: tunnelProvider.connection)
+
+ request = session.getTunnelConnectionInfo { [weak self] completion in
guard let self = self else { return }
- if case .success(.some(let connectionInfo)) = result, !self.isCancelled {
- self.state.tunnelState = .reconnecting(connectionInfo)
- }
+ self.queue.async {
+ if case .success(.some(let connectionInfo)) = completion, !self.isCancelled {
+ self.state.tunnelState = .reconnecting(connectionInfo)
+ }
- self.finish()
+ self.finish()
+ }
}
return
case .connected:
- requestTunnelRelay(from: tunnelProvider) { [weak self] result in
+ let session = TunnelIPC.Session(connection: tunnelProvider.connection)
+
+ request = session.getTunnelConnectionInfo { [weak self] completion in
guard let self = self else { return }
- if case .success(.some(let connectionInfo)) = result, !self.isCancelled {
- self.state.tunnelState = .connected(connectionInfo)
- }
+ self.queue.async {
+ if case .success(.some(let connectionInfo)) = completion, !self.isCancelled {
+ self.state.tunnelState = .connected(connectionInfo)
+ }
- self.finish()
+ self.finish()
+ }
}
return
@@ -121,21 +127,4 @@ class MapConnectionStatusOperation: AsyncOperation {
finish()
}
-
- private func requestTunnelRelay(from tunnelProvider: TunnelProviderManagerType, completionHandler: @escaping (Result<TunnelConnectionInfo?, TunnelIPC.Error>?) -> Void) {
- guard tunnelProvider.connection.status == .reasserting || tunnelProvider.connection.status == .connected else {
- completionHandler(nil)
- return
- }
-
- let ipcSession = TunnelIPC.Session(from: tunnelProvider)
-
- ipcSession.getTunnelConnectionInfo { [weak self] result in
- guard let self = self else { return }
-
- self.queue.async {
- completionHandler(result)
- }
- }
- }
}
diff --git a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
index c1740c50d3..a520244e0f 100644
--- a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
@@ -7,15 +7,14 @@
//
import Foundation
-import NetworkExtension
class ReloadTunnelOperation: AsyncOperation {
typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
private let queue: DispatchQueue
private let state: TunnelManager.State
+ private var request: Cancellable?
private var completionHandler: CompletionHandler?
- private var statusObserver: NSObjectProtocol?
init(queue: DispatchQueue, state: TunnelManager.State, completionHandler: @escaping CompletionHandler) {
self.queue = queue
@@ -25,98 +24,41 @@ class ReloadTunnelOperation: AsyncOperation {
override func main() {
queue.async {
- self.execute { [weak self] completion in
- self?.completeOperation(completion: completion)
- }
- }
- }
-
- override func cancel() {
- super.cancel()
-
- queue.async {
- self.removeStatusObserver()
-
- if self.isExecuting {
+ guard !self.isCancelled else {
self.completeOperation(completion: .cancelled)
+ return
}
- }
- }
-
- private func completeOperation(completion: OperationCompletion<(), TunnelManager.Error>) {
- completionHandler?(completion)
- completionHandler = nil
-
- finish()
- }
-
- private func execute(completionHandler: @escaping CompletionHandler) {
- guard !isCancelled else {
- completionHandler(.cancelled)
- return
- }
-
- guard let tunnelProvider = self.state.tunnelProvider else {
- completionHandler(.failure(.missingAccount))
- return
- }
-
- let ipcSession = TunnelIPC.Session(from: tunnelProvider)
-
- // Add observer
- statusObserver = NotificationCenter.default.addObserver(
- forName: .NEVPNStatusDidChange,
- object: tunnelProvider.connection,
- queue: nil) { [weak self] notification in
- guard let self = self else { return }
- guard let connection = notification.object as? VPNConnectionProtocol else { return }
- self.queue.async {
- self.handleStatus(connection.status, ipcSession: ipcSession, completionHandler: completionHandler)
- }
+ guard let tunnelProvider = self.state.tunnelProvider else {
+ self.completeOperation(completion: .failure(.missingAccount))
+ return
}
- // Run initial check
- handleStatus(tunnelProvider.connection.status, ipcSession: ipcSession, completionHandler: completionHandler)
- }
-
- private func handleStatus(_ status: NEVPNStatus, ipcSession: TunnelIPC.Session, completionHandler: @escaping CompletionHandler) {
- guard !isCancelled else {
- completionHandler(.cancelled)
- return
- }
+ let session = TunnelIPC.Session(connection: tunnelProvider.connection)
- switch status {
- case .connected:
- removeStatusObserver()
-
- ipcSession.reloadTunnelSettings { [weak self] error in
+ self.request = session.reloadTunnelSettings { [weak self] completion in
guard let self = self else { return }
self.queue.async {
- completionHandler(error.map { .failure(.reloadTunnel($0)) } ?? .success(()))
+ self.completeOperation(completion: completion.mapError { .reloadTunnel($0) })
}
}
+ }
+ }
- case .connecting, .reasserting:
- // wait for transition to complete
- break
-
- case .invalid, .disconnecting, .disconnected:
- removeStatusObserver()
- completionHandler(.success(()))
+ override func cancel() {
+ super.cancel()
- @unknown default:
- break
+ queue.async {
+ self.request?.cancel()
}
}
- private func removeStatusObserver() {
- if let statusObserver = statusObserver {
- NotificationCenter.default.removeObserver(statusObserver)
+ private func completeOperation(completion: OperationCompletion<(), TunnelManager.Error>) {
+ completionHandler?(completion)
+ completionHandler = nil
- self.statusObserver = nil
- }
+ finish()
}
}