Skip to content

Commit 3470cf0

Browse files
committed
feat: pass agent updates to UI
1 parent 15f2bcc commit 3470cf0

20 files changed

+390
-294
lines changed

Coder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@ import SwiftUI
44
@MainActor
55
final class PreviewVPN: Coder_Desktop.VPNService {
66
@Published var state: Coder_Desktop.VPNServiceState = .disabled
7-
@Published var agents: [Coder_Desktop.Agent] = [
8-
Agent(id: UUID(), name: "dogfood2", status: .error, copyableDNS: "asdf.coder", workspaceName: "dogfood2"),
9-
Agent(id: UUID(), name: "testing-a-very-long-name", status: .okay, copyableDNS: "asdf.coder",
10-
workspaceName: "testing-a-very-long-name"),
11-
Agent(id: UUID(), name: "opensrc", status: .warn, copyableDNS: "asdf.coder", workspaceName: "opensrc"),
12-
Agent(id: UUID(), name: "gvisor", status: .off, copyableDNS: "asdf.coder", workspaceName: "gvisor"),
13-
Agent(id: UUID(), name: "example", status: .off, copyableDNS: "asdf.coder", workspaceName: "example"),
14-
Agent(id: UUID(), name: "dogfood2", status: .error, copyableDNS: "asdf.coder", workspaceName: "dogfood2"),
15-
Agent(id: UUID(), name: "testing-a-very-long-name", status: .okay, copyableDNS: "asdf.coder",
16-
workspaceName: "testing-a-very-long-name"),
17-
Agent(id: UUID(), name: "opensrc", status: .warn, copyableDNS: "asdf.coder", workspaceName: "opensrc"),
18-
Agent(id: UUID(), name: "gvisor", status: .off, copyableDNS: "asdf.coder", workspaceName: "gvisor"),
19-
Agent(id: UUID(), name: "example", status: .off, copyableDNS: "asdf.coder", workspaceName: "example"),
7+
@Published var agents: [UUID: Coder_Desktop.Agent] = [
8+
UUID(): Agent(id: UUID(), status: .error, copyableDNS: "asdf.coder", wsName: "dogfood2", wsID: UUID()),
9+
UUID(): Agent(id: UUID(), status: .okay, copyableDNS: "asdf.coder", wsName: "testing-a-very-long-name",
10+
wsID: UUID()),
11+
UUID(): Agent(id: UUID(), status: .warn, copyableDNS: "asdf.coder", wsName: "opensrc", wsID: UUID()),
12+
UUID(): Agent(id: UUID(), status: .off, copyableDNS: "asdf.coder", wsName: "gvisor", wsID: UUID()),
13+
UUID(): Agent(id: UUID(), status: .off, copyableDNS: "asdf.coder", wsName: "example", wsID: UUID()),
14+
UUID(): Agent(id: UUID(), status: .error, copyableDNS: "asdf.coder", wsName: "dogfood2", wsID: UUID()),
15+
UUID(): Agent(id: UUID(), status: .okay, copyableDNS: "asdf.coder", wsName: "testing-a-very-long-name",
16+
wsID: UUID()),
17+
UUID(): Agent(id: UUID(), status: .warn, copyableDNS: "asdf.coder", wsName: "opensrc", wsID: UUID()),
18+
UUID(): Agent(id: UUID(), status: .off, copyableDNS: "asdf.coder", wsName: "gvisor", wsID: UUID()),
19+
UUID(): Agent(id: UUID(), status: .off, copyableDNS: "asdf.coder", wsName: "example", wsID: UUID()),
2020
]
2121
let shouldFail: Bool
22+
let longError = "This is a long error to test the UI with long error messages"
2223

2324
init(shouldFail: Bool = false) {
2425
self.shouldFail = shouldFail
@@ -35,10 +36,10 @@ final class PreviewVPN: Coder_Desktop.VPNService {
3536
do {
3637
try await Task.sleep(for: .seconds(5))
3738
} catch {
38-
state = .failed(.longTestError)
39+
state = .failed(.internalError(longError))
3940
return
4041
}
41-
state = shouldFail ? .failed(.longTestError) : .connected
42+
state = shouldFail ? .failed(.internalError(longError)) : .connected
4243
}
4344
defer { startTask = nil }
4445
await startTask?.value
@@ -57,7 +58,7 @@ final class PreviewVPN: Coder_Desktop.VPNService {
5758
do {
5859
try await Task.sleep(for: .seconds(5))
5960
} catch {
60-
state = .failed(.longTestError)
61+
state = .failed(.internalError(longError))
6162
return
6263
}
6364
state = .disabled

Coder Desktop/Coder Desktop/VPNService.swift

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import NetworkExtension
22
import os
33
import SwiftUI
44
import VPNLib
5-
import VPNXPC
65

76
@MainActor
87
protocol VPNService: ObservableObject {
98
var state: VPNServiceState { get }
10-
var agents: [Agent] { get }
9+
var agents: [UUID: Agent] { get }
1110
func start() async
12-
// Stop must be idempotent
1311
func stop() async
1412
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?)
1513
}
@@ -26,12 +24,9 @@ enum VPNServiceError: Error, Equatable {
2624
case internalError(String)
2725
case systemExtensionError(SystemExtensionState)
2826
case networkExtensionError(NetworkExtensionState)
29-
case longTestError
3027

3128
var description: String {
3229
switch self {
33-
case .longTestError:
34-
"This is a long error to test the UI with long errors"
3530
case let .internalError(description):
3631
"Internal Error: \(description)"
3732
case let .systemExtensionError(state):
@@ -47,6 +42,7 @@ final class CoderVPNService: NSObject, VPNService {
4742
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn")
4843
lazy var xpc: VPNXPCInterface = .init(vpn: self)
4944
var terminating = false
45+
var workspaces: [UUID: String] = [:]
5046

5147
@Published var tunnelState: VPNServiceState = .disabled
5248
@Published var sysExtnState: SystemExtensionState = .uninstalled
@@ -61,7 +57,7 @@ final class CoderVPNService: NSObject, VPNService {
6157
return tunnelState
6258
}
6359

64-
@Published var agents: [Agent] = []
60+
@Published var agents: [UUID: Agent] = [:]
6561

6662
// systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get
6763
// garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework
@@ -74,6 +70,16 @@ final class CoderVPNService: NSObject, VPNService {
7470
Task {
7571
await loadNetworkExtension()
7672
}
73+
NotificationCenter.default.addObserver(
74+
self,
75+
selector: #selector(vpnDidUpdate(_:)),
76+
name: .NEVPNStatusDidChange,
77+
object: nil
78+
)
79+
}
80+
81+
deinit {
82+
NotificationCenter.default.removeObserver(self)
7783
}
7884

7985
func start() async {
@@ -84,16 +90,14 @@ final class CoderVPNService: NSObject, VPNService {
8490
return
8591
}
8692

93+
await enableNetworkExtension()
8794
// this ping is somewhat load bearing since it causes xpc to init
8895
xpc.ping()
89-
tunnelState = .connecting
90-
await enableNetworkExtension()
9196
logger.debug("network extension enabled")
9297
}
9398

9499
func stop() async {
95100
guard tunnelState == .connected else { return }
96-
tunnelState = .disconnecting
97101
await disableNetworkExtension()
98102
logger.info("network extension stopped")
99103
}
@@ -131,31 +135,88 @@ final class CoderVPNService: NSObject, VPNService {
131135
}
132136

133137
func onExtensionPeerUpdate(_ data: Data) {
134-
// TODO: handle peer update
135138
logger.info("network extension peer update")
136139
do {
137-
let msg = try Vpn_TunnelMessage(serializedBytes: data)
140+
let msg = try Vpn_PeerUpdate(serializedBytes: data)
138141
debugPrint(msg)
142+
applyPeerUpdate(with: msg)
139143
} catch {
140144
logger.error("failed to decode peer update \(error)")
141145
}
142146
}
143147

144-
func onExtensionStart() {
145-
logger.info("network extension reported started")
146-
tunnelState = .connected
147-
}
148+
func applyPeerUpdate(with update: Vpn_PeerUpdate) {
149+
// Delete agents
150+
let deletedWorkspaceIDs = Set(update.deletedWorkspaces.compactMap { UUID(uuidData: $0.id) })
151+
let deletedAgentIDs = Set(update.deletedAgents.compactMap { UUID(uuidData: $0.id) })
152+
for agentID in deletedAgentIDs {
153+
agents[agentID] = nil
154+
}
155+
for workspaceID in deletedWorkspaceIDs {
156+
workspaces[workspaceID] = nil
157+
for (id, agent) in agents where agent.wsID == workspaceID {
158+
agents[id] = nil
159+
}
160+
}
148161

149-
func onExtensionStop() {
150-
logger.info("network extension reported stopped")
151-
tunnelState = .disabled
152-
if terminating {
153-
NSApp.reply(toApplicationShouldTerminate: true)
162+
// Update workspaces
163+
for workspaceProto in update.upsertedWorkspaces {
164+
if let workspaceID = UUID(uuidData: workspaceProto.id) {
165+
workspaces[workspaceID] = workspaceProto.name
166+
}
167+
}
168+
169+
for agentProto in update.upsertedAgents {
170+
guard let agentID = UUID(uuidData: agentProto.id) else {
171+
continue
172+
}
173+
guard let workspaceID = UUID(uuidData: agentProto.workspaceID) else {
174+
continue
175+
}
176+
let workspaceName = workspaces[workspaceID] ?? "Unknown Workspace"
177+
let newAgent = Agent(
178+
id: agentID,
179+
// If last handshake was not within last five minutes, the agent is unhealthy
180+
status: agentProto.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .off,
181+
copyableDNS: agentProto.fqdn.first ?? "UNKNOWN",
182+
wsName: workspaceName,
183+
wsID: workspaceID
184+
)
185+
186+
agents[agentID] = newAgent
154187
}
155188
}
189+
}
156190

157-
func onExtensionError(_ error: NSError) {
158-
logger.error("network extension reported error: \(error)")
159-
tunnelState = .failed(.internalError(error.localizedDescription))
191+
extension CoderVPNService {
192+
@objc private func vpnDidUpdate(_ notification: Notification) {
193+
guard let connection = notification.object as? NETunnelProviderSession else {
194+
return
195+
}
196+
switch connection.status {
197+
case .disconnected:
198+
if terminating {
199+
NSApp.reply(toApplicationShouldTerminate: true)
200+
}
201+
connection.fetchLastDisconnectError { err in
202+
self.tunnelState = if let err {
203+
.failed(.internalError(err.localizedDescription))
204+
} else {
205+
.disabled
206+
}
207+
}
208+
case .connecting:
209+
tunnelState = .connecting
210+
case .connected:
211+
tunnelState = .connected
212+
case .reasserting:
213+
tunnelState = .connecting
214+
case .disconnecting:
215+
tunnelState = .disconnecting
216+
case .invalid:
217+
tunnelState = .failed(.networkExtensionError(.unconfigured))
218+
@unknown default:
219+
tunnelState = .disabled
220+
}
160221
}
161222
}

Coder Desktop/Coder Desktop/Views/Agent.swift

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
import SwiftUI
22

3-
struct Agent: Identifiable, Equatable {
3+
struct Agent: Identifiable, Equatable, Comparable {
44
let id: UUID
5-
let name: String
65
let status: AgentStatus
76
let copyableDNS: String
8-
let workspaceName: String
7+
let wsName: String
8+
let wsID: UUID
9+
10+
// Agents are sorted by status, and then by name
11+
static func < (lhs: Agent, rhs: Agent) -> Bool {
12+
if lhs.status != rhs.status {
13+
return lhs.status < rhs.status
14+
}
15+
return lhs.wsName.localizedCompare(rhs.wsName) == .orderedAscending
16+
}
917
}
1018

11-
enum AgentStatus: Equatable {
12-
case okay
13-
case warn
14-
case error
15-
case off
19+
enum AgentStatus: Int, Equatable, Comparable {
20+
case okay = 0
21+
case warn = 1
22+
case error = 2
23+
case off = 3
1624

1725
public var color: Color {
1826
switch self {
@@ -22,16 +30,20 @@ enum AgentStatus: Equatable {
2230
case .off: .gray
2331
}
2432
}
33+
34+
static func < (lhs: AgentStatus, rhs: AgentStatus) -> Bool {
35+
lhs.rawValue < rhs.rawValue
36+
}
2537
}
2638

2739
struct AgentRowView: View {
28-
let workspace: Agent
40+
let agent: Agent
2941
let baseAccessURL: URL
3042
@State private var nameIsSelected: Bool = false
3143
@State private var copyIsSelected: Bool = false
3244

3345
private var fmtWsName: AttributedString {
34-
var formattedName = AttributedString(workspace.name)
46+
var formattedName = AttributedString(agent.wsName)
3547
formattedName.foregroundColor = .primary
3648
var coderPart = AttributedString(".coder")
3749
coderPart.foregroundColor = .gray
@@ -41,7 +53,7 @@ struct AgentRowView: View {
4153

4254
private var wsURL: URL {
4355
// TODO: CoderVPN currently only supports owned workspaces
44-
baseAccessURL.appending(path: "@me").appending(path: workspace.workspaceName)
56+
baseAccessURL.appending(path: "@me").appending(path: agent.wsName)
4557
}
4658

4759
var body: some View {
@@ -50,10 +62,10 @@ struct AgentRowView: View {
5062
HStack(spacing: Theme.Size.trayPadding) {
5163
ZStack {
5264
Circle()
53-
.fill(workspace.status.color.opacity(0.4))
65+
.fill(agent.status.color.opacity(0.4))
5466
.frame(width: 12, height: 12)
5567
Circle()
56-
.fill(workspace.status.color.opacity(1.0))
68+
.fill(agent.status.color.opacity(1.0))
5769
.frame(width: 7, height: 7)
5870
}
5971
Text(fmtWsName).lineLimit(1).truncationMode(.tail)
@@ -69,7 +81,7 @@ struct AgentRowView: View {
6981
}.buttonStyle(.plain)
7082
Button {
7183
// TODO: Proper clipboard abstraction
72-
NSPasteboard.general.setString(workspace.copyableDNS, forType: .string)
84+
NSPasteboard.general.setString(agent.copyableDNS, forType: .string)
7385
} label: {
7486
Image(systemName: "doc.on.doc")
7587
.symbolVariant(.fill)

Coder Desktop/Coder Desktop/Views/Agents.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ struct Agents<VPN: VPNService, S: Session>: View {
1010

1111
var body: some View {
1212
Group {
13-
// Workspaces List
13+
// Agents List
1414
if vpn.state == .connected {
15-
let visibleData = viewAll ? vpn.agents : Array(vpn.agents.prefix(defaultVisibleRows))
16-
ForEach(visibleData, id: \.id) { workspace in
17-
AgentRowView(workspace: workspace, baseAccessURL: session.baseAccessURL!)
15+
let sortedAgents = vpn.agents.values.sorted()
16+
let visibleData = viewAll ? sortedAgents[...] : sortedAgents.prefix(defaultVisibleRows)
17+
ForEach(visibleData, id: \.id) { agent in
18+
AgentRowView(agent: agent, baseAccessURL: session.baseAccessURL!)
1819
.padding(.horizontal, Theme.Size.trayMargin)
1920
}
2021
if vpn.agents.count > defaultVisibleRows {

Coder Desktop/Coder Desktop/Views/Util.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,16 @@ final class Inspection<V> {
1212
}
1313
}
1414
}
15+
16+
extension UUID {
17+
init?(uuidData: Data) {
18+
guard uuidData.count == 16 else {
19+
return nil
20+
}
21+
var uuid: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
22+
withUnsafeMutableBytes(of: &uuid) {
23+
$0.copyBytes(from: uuidData)
24+
}
25+
self.init(uuid: uuid)
26+
}
27+
}

Coder Desktop/Coder Desktop/XPCInterface.swift

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
2+
import NetworkExtension
23
import os
3-
import VPNXPC
4+
import VPNLib
45

56
@objc final class VPNXPCInterface: NSObject, VPNXPCClientCallbackProtocol, @unchecked Sendable {
67
private var svc: CoderVPNService
@@ -49,22 +50,4 @@ import VPNXPC
4950
svc.onExtensionPeerUpdate(data)
5051
}
5152
}
52-
53-
func onStart() {
54-
Task { @MainActor in
55-
svc.onExtensionStart()
56-
}
57-
}
58-
59-
func onStop() {
60-
Task { @MainActor in
61-
svc.onExtensionStop()
62-
}
63-
}
64-
65-
func onError(_ err: NSError) {
66-
Task { @MainActor in
67-
svc.onExtensionError(err)
68-
}
69-
}
7053
}

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