@@ -31,10 +31,21 @@ enum VPNManagerError: LocalizedError, Equatable {
3131
3232/// Coordinates connection changes while allowing stop to cancel a pending start.
3333actor VPNLifecycleCoordinator {
34+ private struct StopWaiter {
35+ let connectionID : UInt
36+ let continuation : CheckedContinuation < Void , Never >
37+ }
38+
3439 private var nextConnectionID : UInt = 0
3540 private var activeConnectionID : UInt ?
3641 private var stopPending = false
37- private var stopWaiters : [ CheckedContinuation < Void , Never > ] = [ ]
42+ private var stopWaiter : StopWaiter ?
43+ private var stopHandoffTimeoutTask : Task < Void , Never > ?
44+ private let stopHandoffTimeoutNanoseconds : UInt64
45+
46+ init ( stopHandoffTimeoutNanoseconds: UInt64 = 5_000_000_000 ) {
47+ self . stopHandoffTimeoutNanoseconds = stopHandoffTimeoutNanoseconds
48+ }
3849
3950 /// Starts a connection operation unless another lifecycle change owns the manager.
4051 func beginConnectionOperation( ) throws -> UInt {
@@ -55,22 +66,29 @@ actor VPNLifecycleCoordinator {
5566 func endConnectionOperation( _ id: UInt ) {
5667 guard activeConnectionID == id else { return }
5768 activeConnectionID = nil
58- let waiters = stopWaiters
59- stopWaiters. removeAll ( )
60- waiters. forEach { $0. resume ( ) }
69+ finishStopHandoff ( for: id)
6170 }
6271
63- /// Cancels any active connection operation and waits for its profile writes to finish .
72+ /// Cancels any active connection operation and briefly waits for it to hand off the manager .
6473 /// The return value tells the caller to tear down even if the system status has not caught up.
6574 func beginStopOperation( ) async throws -> Bool {
6675 guard !stopPending else {
6776 throw VPNManagerError . operationInProgress
6877 }
6978 stopPending = true
70- let canceledConnectionOperation = activeConnectionID != nil
71- guard canceledConnectionOperation else { return false }
79+ guard let connectionID = activeConnectionID else { return false }
80+ let timeout = stopHandoffTimeoutNanoseconds
81+
7282 await withCheckedContinuation { continuation in
73- stopWaiters. append ( continuation)
83+ stopWaiter = StopWaiter ( connectionID: connectionID, continuation: continuation)
84+ stopHandoffTimeoutTask = Task { [ weak self] in
85+ do {
86+ try await Task . sleep ( nanoseconds: timeout)
87+ } catch {
88+ return
89+ }
90+ await self ? . expireStopHandoff ( for: connectionID)
91+ }
7492 }
7593 return true
7694 }
@@ -79,6 +97,24 @@ actor VPNLifecycleCoordinator {
7997 func endStopOperation( ) {
8098 stopPending = false
8199 }
100+
101+ private func finishStopHandoff( for connectionID: UInt ) {
102+ guard let waiter = stopWaiter, waiter. connectionID == connectionID else { return }
103+ stopWaiter = nil
104+ stopHandoffTimeoutTask? . cancel ( )
105+ stopHandoffTimeoutTask = nil
106+ waiter. continuation. resume ( )
107+ }
108+
109+ private func expireStopHandoff( for connectionID: UInt ) {
110+ guard let waiter = stopWaiter, waiter. connectionID == connectionID else { return }
111+ stopWaiter = nil
112+ stopHandoffTimeoutTask = nil
113+ if activeConnectionID == connectionID {
114+ activeConnectionID = nil
115+ }
116+ waiter. continuation. resume ( )
117+ }
82118}
83119
84120/// Returns whether a new tunnel should start for the current system status.
@@ -102,10 +138,8 @@ func shouldStopTunnel(for status: NEVPNStatus) throws -> Bool {
102138 switch status {
103139 case . connected, . connecting, . reasserting:
104140 return true
105- case . disconnected, . disconnecting:
141+ case . disconnected, . disconnecting, . invalid :
106142 return false
107- case . invalid:
108- throw VPNManagerError . loadingProviderFailed
109143 @unknown default :
110144 throw VPNManagerError . unknown
111145 }
0 commit comments