Skip to content

Commit aedbe8e

Browse files
committed
chore: handle waking from device sleep
1 parent 250017b commit aedbe8e

File tree

5 files changed

+37
-31
lines changed

5 files changed

+37
-31
lines changed

Coder Desktop/Coder Desktop/Views/VPNMenu.swift

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
55
@EnvironmentObject var session: S
66
@Environment(\.openSettings) private var openSettings
77

8-
// There appears to be a race between the VPN service reporting itself as disconnected,
9-
// and the system extension process exiting. When the VPN is toggled off and on quickly,
10-
// an error is shown: "The VPN session failed because an internal error occurred".
11-
// This forces the user to wait a few seconds before they can toggle the VPN back on.
12-
@State private var waitCleanup = false
13-
private var waitCleanupDuration: Duration = .seconds(6)
14-
158
let inspection = Inspection<Self>()
169

1710
var body: some View {
@@ -23,7 +16,7 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
2316
Toggle(isOn: Binding(
2417
get: { vpn.state == .connected || vpn.state == .connecting },
2518
set: { isOn in Task {
26-
if isOn { await vpn.start() } else { await stop() }
19+
if isOn { await vpn.start() } else { await vpn.stop() }
2720
}
2821
}
2922
)) {
@@ -93,21 +86,11 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
9386
}
9487

9588
private var vpnDisabled: Bool {
96-
waitCleanup ||
97-
!session.hasSession ||
89+
!session.hasSession ||
9890
vpn.state == .connecting ||
9991
vpn.state == .disconnecting ||
10092
vpn.state == .failed(.systemExtensionError(.needsUserApproval))
10193
}
102-
103-
private func stop() async {
104-
await vpn.stop()
105-
waitCleanup = true
106-
Task {
107-
try? await Task.sleep(for: waitCleanupDuration)
108-
waitCleanup = false
109-
}
110-
}
11194
}
11295

11396
func openSystemExtensionSettings() {

Coder Desktop/VPN/Manager.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ actor Manager {
2727
fatalError("unknown architecture")
2828
#endif
2929
do {
30-
try await download(src: dylibPath, dest: dest)
30+
let sessionConfig = URLSessionConfiguration.default
31+
// The tunnel might be asked to start before the network interfaces have woken up from sleep
32+
sessionConfig.waitsForConnectivity = true
33+
try await download(src: dylibPath, dest: dest, urlSession: URLSession(configuration: sessionConfig))
3134
} catch {
3235
throw .download(error)
3336
}

Coder Desktop/VPN/PacketTunnelProvider.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4747
options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void
4848
) {
4949
logger.info("startTunnel called")
50+
start(completionHandler)
51+
}
52+
53+
// called by `startTunnel` and on `wake`
54+
func start(_ completionHandler: @escaping (Error?) -> Void) {
5055
guard manager == nil else {
5156
logger.error("startTunnel called with non-nil Manager")
5257
completionHandler(makeNSError(suffix: "PTP", desc: "Already running"))
@@ -95,8 +100,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
95100
with _: NEProviderStopReason, completionHandler: @escaping () -> Void
96101
) {
97102
logger.debug("stopTunnel called")
103+
teardown(completionHandler)
104+
}
105+
106+
// called by `stopTunnel` and `sleep`
107+
func teardown(_ completionHandler: @escaping () -> Void) {
98108
guard let manager else {
99-
logger.error("stopTunnel called with nil Manager")
109+
logger.error("teardown called with nil Manager")
100110
completionHandler()
101111
return
102112
}
@@ -121,15 +131,25 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
121131
}
122132
}
123133

134+
// sleep and wake reference: https://developer.apple.com/forums/thread/95988
124135
override func sleep(completionHandler: @escaping () -> Void) {
125-
// Add code here to get ready to sleep.
126136
logger.debug("sleep called")
127-
completionHandler()
137+
teardown(completionHandler)
128138
}
129139

130140
override func wake() {
131-
// Add code here to wake up.
132141
logger.debug("wake called")
142+
reasserting = true
143+
currentSettings = .init(tunnelRemoteAddress: "127.0.0.1")
144+
setTunnelNetworkSettings(nil)
145+
start { error in
146+
if let error {
147+
self.logger.error("error starting tunnel after wake: \(error.localizedDescription)")
148+
self.cancelTunnelWithError(error)
149+
} else {
150+
self.reasserting = false
151+
}
152+
}
133153
}
134154

135155
// Wrapper around `setTunnelNetworkSettings` that supports merging updates

Coder Desktop/VPNLib/Download.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public class SignatureValidator {
101101
}
102102
}
103103

104-
public func download(src: URL, dest: URL) async throws(DownloadError) {
104+
public func download(src: URL, dest: URL, urlSession: URLSession) async throws(DownloadError) {
105105
var req = URLRequest(url: src)
106106
if FileManager.default.fileExists(atPath: dest.path) {
107107
if let existingFileData = try? Data(contentsOf: dest, options: .mappedIfSafe) {
@@ -112,7 +112,7 @@ public func download(src: URL, dest: URL) async throws(DownloadError) {
112112
let tempURL: URL
113113
let response: URLResponse
114114
do {
115-
(tempURL, response) = try await URLSession.shared.download(for: req)
115+
(tempURL, response) = try await urlSession.download(for: req)
116116
} catch {
117117
throw .networkError(error)
118118
}

Coder Desktop/VPNLibTests/DownloadTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ struct DownloadTests {
1313
let fileURL = URL(string: "http://example.com/test1.txt")!
1414
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: testData]).register()
1515

16-
try await download(src: fileURL, dest: destinationURL)
16+
try await download(src: fileURL, dest: destinationURL, urlSession: URLSession.shared)
1717

1818
try #require(FileManager.default.fileExists(atPath: destinationURL.path))
1919
defer { try? FileManager.default.removeItem(at: destinationURL) }
@@ -32,7 +32,7 @@ struct DownloadTests {
3232

3333
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: testData]).register()
3434

35-
try await download(src: fileURL, dest: destinationURL)
35+
try await download(src: fileURL, dest: destinationURL, urlSession: URLSession.shared)
3636
try #require(FileManager.default.fileExists(atPath: destinationURL.path))
3737
let downloadedData = try Data(contentsOf: destinationURL)
3838
#expect(downloadedData == testData)
@@ -44,7 +44,7 @@ struct DownloadTests {
4444
}
4545
mock.register()
4646

47-
try await download(src: fileURL, dest: destinationURL)
47+
try await download(src: fileURL, dest: destinationURL, urlSession: URLSession.shared)
4848
let unchangedData = try Data(contentsOf: destinationURL)
4949
#expect(unchangedData == testData)
5050
#expect(etagIncluded)
@@ -61,7 +61,7 @@ struct DownloadTests {
6161

6262
Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: ogData]).register()
6363

64-
try await download(src: fileURL, dest: destinationURL)
64+
try await download(src: fileURL, dest: destinationURL, urlSession: URLSession.shared)
6565
try #require(FileManager.default.fileExists(atPath: destinationURL.path))
6666
var downloadedData = try Data(contentsOf: destinationURL)
6767
#expect(downloadedData == ogData)
@@ -73,7 +73,7 @@ struct DownloadTests {
7373
}
7474
mock.register()
7575

76-
try await download(src: fileURL, dest: destinationURL)
76+
try await download(src: fileURL, dest: destinationURL, urlSession: URLSession.shared)
7777
downloadedData = try Data(contentsOf: destinationURL)
7878
#expect(downloadedData == newData)
7979
#expect(etagIncluded)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy