Skip to content

Commit f66f2d8

Browse files
committed
chore: handle waking from device sleep
1 parent 84d6ad3 commit f66f2d8

File tree

5 files changed

+37
-31
lines changed

5 files changed

+37
-31
lines changed

Coder Desktop/Coder Desktop/Views/VPNMenu.swift

+2-19
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

+4-1
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

+24-4
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

+2-2
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

+5-5
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)