Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit10c2109

Browse files
feat: pass agent updates to UI (#35)
1 parent15f2bcc commit10c2109

21 files changed

+530
-293
lines changed

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

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@ import SwiftUI
44
@MainActor
55
finalclass PreviewVPN:Coder_Desktop.VPNService{
66
@Publishedvarstate:Coder_Desktop.VPNServiceState=.disabled
7-
@Publishedvaragents:[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+
@Publishedvaragents:[UUID:Coder_Desktop.Agent]=[
8+
UUID():Agent(id:UUID(), name:"dev", status:.error, copyableDNS:"asdf.coder", wsName:"dogfood2",
9+
wsID:UUID()),
10+
UUID():Agent(id:UUID(), name:"dev", status:.okay, copyableDNS:"asdf.coder",
11+
wsName:"testing-a-very-long-name", wsID:UUID()),
12+
UUID():Agent(id:UUID(), name:"dev", status:.warn, copyableDNS:"asdf.coder", wsName:"opensrc",
13+
wsID:UUID()),
14+
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"gvisor",
15+
wsID:UUID()),
16+
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"example",
17+
wsID:UUID()),
18+
UUID():Agent(id:UUID(), name:"dev", status:.error, copyableDNS:"asdf.coder", wsName:"dogfood2",
19+
wsID:UUID()),
20+
UUID():Agent(id:UUID(), name:"dev", status:.okay, copyableDNS:"asdf.coder",
21+
wsName:"testing-a-very-long-name", wsID:UUID()),
22+
UUID():Agent(id:UUID(), name:"dev", status:.warn, copyableDNS:"asdf.coder", wsName:"opensrc",
23+
wsID:UUID()),
24+
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"gvisor",
25+
wsID:UUID()),
26+
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"example",
27+
wsID:UUID()),
2028
]
2129
letshouldFail:Bool
30+
letlongError="This is a long error to test the UI with long error messages"
2231

2332
init(shouldFail:Bool=false){
2433
self.shouldFail= shouldFail
@@ -35,10 +44,10 @@ final class PreviewVPN: Coder_Desktop.VPNService {
3544
do{
3645
tryawaitTask.sleep(for:.seconds(5))
3746
}catch{
38-
state=.failed(.longTestError)
47+
state=.failed(.internalError(longError))
3948
return
4049
}
41-
state= shouldFail?.failed(.longTestError):.connected
50+
state= shouldFail?.failed(.internalError(longError)):.connected
4251
}
4352
defer{ startTask=nil}
4453
await startTask?.value
@@ -57,7 +66,7 @@ final class PreviewVPN: Coder_Desktop.VPNService {
5766
do{
5867
tryawaitTask.sleep(for:.seconds(5))
5968
}catch{
60-
state=.failed(.longTestError)
69+
state=.failed(.internalError(longError))
6170
return
6271
}
6372
state=.disabled

‎Coder Desktop/Coder Desktop/VPNService.swift

Lines changed: 94 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
protocolVPNService:ObservableObject{
98
varstate:VPNServiceState{get}
10-
varagents:[Agent]{get}
9+
varagents:[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
vardescription:String{
3229
switchself{
33-
case.longTestError:
34-
"This is a long error to test the UI with long errors"
3530
caselet.internalError(description):
3631
"Internal Error:\(description)"
3732
caselet.systemExtensionError(state):
@@ -47,6 +42,7 @@ final class CoderVPNService: NSObject, VPNService {
4742
varlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"vpn")
4843
lazyvarxpc:VPNXPCInterface=.init(vpn:self)
4944
varterminating=false
45+
varworkspaces:[UUID:String]=[:]
5046

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

64-
@Publishedvaragents:[Agent]=[]
60+
@Publishedvaragents:[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
awaitloadNetworkExtension()
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+
awaitenableNetworkExtension()
8794
// this ping is somewhat load bearing since it causes xpc to init
8895
xpc.ping()
89-
tunnelState=.connecting
90-
awaitenableNetworkExtension()
9196
logger.debug("network extension enabled")
9297
}
9398

9499
func stop()async{
95100
guard tunnelState==.connectedelse{return}
96-
tunnelState=.disconnecting
97101
awaitdisableNetworkExtension()
98102
logger.info("network extension stopped")
99103
}
@@ -131,31 +135,97 @@ 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-
letmsg=tryVpn_TunnelMessage(serializedBytes: data)
140+
letmsg=tryVpn_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+
update.deletedAgents
151+
.compactMap{UUID(uuidData: $0.id)}
152+
.forEach{ agentIDin
153+
agents[agentID]=nil
154+
}
155+
update.deletedWorkspaces
156+
.compactMap{UUID(uuidData: $0.id)}
157+
.forEach{ workspaceIDin
158+
workspaces[workspaceID]=nil
159+
for(id, agent)in agentswhere agent.wsID== workspaceID{
160+
agents[id]=nil
161+
}
162+
}
148163

149-
func onExtensionStop(){
150-
logger.info("network extension reported stopped")
151-
tunnelState=.disabled
152-
if terminating{
153-
NSApp.reply(toApplicationShouldTerminate:true)
164+
// Update workspaces
165+
forworkspaceProtoin update.upsertedWorkspaces{
166+
iflet workspaceID=UUID(uuidData: workspaceProto.id){
167+
workspaces[workspaceID]= workspaceProto.name
168+
}
169+
}
170+
171+
foragentProtoin update.upsertedAgents{
172+
guardlet agentID=UUID(uuidData: agentProto.id)else{
173+
continue
174+
}
175+
guardlet workspaceID=UUID(uuidData: agentProto.workspaceID)else{
176+
continue
177+
}
178+
letworkspaceName=workspaces[workspaceID]??"Unknown Workspace"
179+
letnewAgent=Agent(
180+
id: agentID,
181+
name: agentProto.name,
182+
// If last handshake was not within last five minutes, the agent is unhealthy
183+
status: agentProto.lastHandshake.date>Date.now.addingTimeInterval(-300)?.okay:.off,
184+
copyableDNS: agentProto.fqdn.first??"UNKNOWN",
185+
wsName: workspaceName,
186+
wsID: workspaceID
187+
)
188+
189+
// An existing agent with the same name, belonging to the same workspace
190+
// is from a previous workspace build, and should be removed.
191+
agents
192+
.filter{ $0.value.name== agentProto.name && $0.value.wsID== workspaceID}
193+
.forEach{agents[$0.key]=nil}
194+
195+
agents[agentID]= newAgent
154196
}
155197
}
198+
}
156199

157-
func onExtensionError(_ error:NSError){
158-
logger.error("network extension reported error:\(error)")
159-
tunnelState=.failed(.internalError(error.localizedDescription))
200+
extensionCoderVPNService{
201+
@objcprivatefunc vpnDidUpdate(_ notification:Notification){
202+
guardlet connection= notification.objectas?NETunnelProviderSessionelse{
203+
return
204+
}
205+
switch connection.status{
206+
case.disconnected:
207+
if terminating{
208+
NSApp.reply(toApplicationShouldTerminate:true)
209+
}
210+
connection.fetchLastDisconnectError{ errin
211+
self.tunnelState=iflet err{
212+
.failed(.internalError(err.localizedDescription))
213+
}else{
214+
.disabled
215+
}
216+
}
217+
case.connecting:
218+
tunnelState=.connecting
219+
case.connected:
220+
tunnelState=.connected
221+
case.reasserting:
222+
tunnelState=.connecting
223+
case.disconnecting:
224+
tunnelState=.disconnecting
225+
case.invalid:
226+
tunnelState=.failed(.networkExtensionError(.unconfigured))
227+
@unknowndefault:
228+
tunnelState=.disabled
229+
}
160230
}
161231
}

‎Coder Desktop/Coder Desktop/Views/Agent.swift

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

3-
structAgent:Identifiable,Equatable{
3+
structAgent:Identifiable,Equatable,Comparable{
44
letid:UUID
55
letname:String
66
letstatus:AgentStatus
77
letcopyableDNS:String
8-
letworkspaceName:String
8+
letwsName:String
9+
letwsID:UUID
10+
11+
// Agents are sorted by status, and then by name
12+
staticfunc<(lhs:Agent, rhs:Agent)->Bool{
13+
if lhs.status!= rhs.status{
14+
return lhs.status< rhs.status
15+
}
16+
return lhs.wsName.localizedCompare(rhs.wsName)==.orderedAscending
17+
}
918
}
1019

11-
enumAgentStatus:Equatable{
12-
case okay
13-
case warn
14-
case error
15-
case off
20+
enumAgentStatus:Int,Equatable,Comparable{
21+
case okay=0
22+
case warn=1
23+
case error=2
24+
case off=3
1625

1726
publicvarcolor:Color{
1827
switchself{
@@ -22,16 +31,20 @@ enum AgentStatus: Equatable {
2231
case.off:.gray
2332
}
2433
}
34+
35+
staticfunc<(lhs:AgentStatus, rhs:AgentStatus)->Bool{
36+
lhs.rawValue< rhs.rawValue
37+
}
2538
}
2639

2740
structAgentRowView:View{
28-
letworkspace:Agent
41+
letagent:Agent
2942
letbaseAccessURL:URL
3043
@StateprivatevarnameIsSelected:Bool=false
3144
@StateprivatevarcopyIsSelected:Bool=false
3245

3346
privatevarfmtWsName:AttributedString{
34-
varformattedName=AttributedString(workspace.name)
47+
varformattedName=AttributedString(agent.wsName)
3548
formattedName.foregroundColor=.primary
3649
varcoderPart=AttributedString(".coder")
3750
coderPart.foregroundColor=.gray
@@ -41,7 +54,7 @@ struct AgentRowView: View {
4154

4255
privatevarwsURL:URL{
4356
// TODO: CoderVPN currently only supports owned workspaces
44-
baseAccessURL.appending(path:"@me").appending(path:workspace.workspaceName)
57+
baseAccessURL.appending(path:"@me").appending(path:agent.wsName)
4558
}
4659

4760
varbody:someView{
@@ -50,10 +63,10 @@ struct AgentRowView: View {
5063
HStack(spacing:Theme.Size.trayPadding){
5164
ZStack{
5265
Circle()
53-
.fill(workspace.status.color.opacity(0.4))
66+
.fill(agent.status.color.opacity(0.4))
5467
.frame(width:12, height:12)
5568
Circle()
56-
.fill(workspace.status.color.opacity(1.0))
69+
.fill(agent.status.color.opacity(1.0))
5770
.frame(width:7, height:7)
5871
}
5972
Text(fmtWsName).lineLimit(1).truncationMode(.tail)
@@ -69,7 +82,7 @@ struct AgentRowView: View {
6982
}.buttonStyle(.plain)
7083
Button{
7184
// TODO: Proper clipboard abstraction
72-
NSPasteboard.general.setString(workspace.copyableDNS, forType:.string)
85+
NSPasteboard.general.setString(agent.copyableDNS, forType:.string)
7386
} label:{
7487
Image(systemName:"doc.on.doc")
7588
.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
varbody:someView{
1212
Group{
13-
//Workspaces List
13+
//Agents List
1414
if vpn.state==.connected{
15-
letvisibleData= viewAll? vpn.agents:Array(vpn.agents.prefix(defaultVisibleRows))
16-
ForEach(visibleData, id: \.id){ workspacein
17-
AgentRowView(workspace: workspace, baseAccessURL: session.baseAccessURL!)
15+
letsortedAgents= vpn.agents.values.sorted()
16+
letvisibleData= viewAll?sortedAgents[...]: sortedAgents.prefix(defaultVisibleRows)
17+
ForEach(visibleData, id: \.id){ agentin
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,22 @@ final class Inspection<V> {
1212
}
1313
}
1414
}
15+
16+
extensionUUID{
17+
varuuidData:Data{
18+
withUnsafePointer(to: uuid){
19+
Data(bytes: $0, count:MemoryLayout.size(ofValue: uuid))
20+
}
21+
}
22+
23+
init?(uuidData:Data){
24+
guard uuidData.count==16else{
25+
returnnil
26+
}
27+
varuuid:uuid_t=(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
28+
withUnsafeMutableBytes(of:&uuid){
29+
$0.copyBytes(from: uuidData)
30+
}
31+
self.init(uuid: uuid)
32+
}
33+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp