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

feat: pass agent updates to UI#35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 2 commits intomainfromethan/agents-to-ui
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletionsCoder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,21 +4,30 @@ import SwiftUI
@MainActor
finalclass PreviewVPN:Coder_Desktop.VPNService{
@Publishedvarstate:Coder_Desktop.VPNServiceState=.disabled
@Publishedvaragents:[Coder_Desktop.Agent]=[
Agent(id:UUID(), name:"dogfood2", status:.error, copyableDNS:"asdf.coder", workspaceName:"dogfood2"),
Agent(id:UUID(), name:"testing-a-very-long-name", status:.okay, copyableDNS:"asdf.coder",
workspaceName:"testing-a-very-long-name"),
Agent(id:UUID(), name:"opensrc", status:.warn, copyableDNS:"asdf.coder", workspaceName:"opensrc"),
Agent(id:UUID(), name:"gvisor", status:.off, copyableDNS:"asdf.coder", workspaceName:"gvisor"),
Agent(id:UUID(), name:"example", status:.off, copyableDNS:"asdf.coder", workspaceName:"example"),
Agent(id:UUID(), name:"dogfood2", status:.error, copyableDNS:"asdf.coder", workspaceName:"dogfood2"),
Agent(id:UUID(), name:"testing-a-very-long-name", status:.okay, copyableDNS:"asdf.coder",
workspaceName:"testing-a-very-long-name"),
Agent(id:UUID(), name:"opensrc", status:.warn, copyableDNS:"asdf.coder", workspaceName:"opensrc"),
Agent(id:UUID(), name:"gvisor", status:.off, copyableDNS:"asdf.coder", workspaceName:"gvisor"),
Agent(id:UUID(), name:"example", status:.off, copyableDNS:"asdf.coder", workspaceName:"example"),
@Publishedvaragents:[UUID:Coder_Desktop.Agent]=[
UUID():Agent(id:UUID(), name:"dev", status:.error, copyableDNS:"asdf.coder", wsName:"dogfood2",
wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.okay, copyableDNS:"asdf.coder",
wsName:"testing-a-very-long-name", wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.warn, copyableDNS:"asdf.coder", wsName:"opensrc",
wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"gvisor",
wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"example",
wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.error, copyableDNS:"asdf.coder", wsName:"dogfood2",
wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.okay, copyableDNS:"asdf.coder",
wsName:"testing-a-very-long-name", wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.warn, copyableDNS:"asdf.coder", wsName:"opensrc",
wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"gvisor",
wsID:UUID()),
UUID():Agent(id:UUID(), name:"dev", status:.off, copyableDNS:"asdf.coder", wsName:"example",
wsID:UUID()),
]
letshouldFail:Bool
letlongError="This is a long error to test the UI with long error messages"

init(shouldFail:Bool=false){
self.shouldFail= shouldFail
Expand All@@ -35,10 +44,10 @@ final class PreviewVPN: Coder_Desktop.VPNService {
do{
tryawaitTask.sleep(for:.seconds(5))
}catch{
state=.failed(.longTestError)
state=.failed(.internalError(longError))
return
}
state= shouldFail?.failed(.longTestError):.connected
state= shouldFail?.failed(.internalError(longError)):.connected
}
defer{ startTask=nil}
await startTask?.value
Expand All@@ -57,7 +66,7 @@ final class PreviewVPN: Coder_Desktop.VPNService {
do{
tryawaitTask.sleep(for:.seconds(5))
}catch{
state=.failed(.longTestError)
state=.failed(.internalError(longError))
return
}
state=.disabled
Expand Down
118 changes: 94 additions & 24 deletionsCoder Desktop/Coder Desktop/VPNService.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,14 +2,12 @@ import NetworkExtension
import os
import SwiftUI
import VPNLib
import VPNXPC

@MainActor
protocolVPNService:ObservableObject{
varstate:VPNServiceState{get}
varagents:[Agent]{get}
varagents:[UUID:Agent]{get}
func start()async
// Stop must be idempotent
func stop()async
func configureTunnelProviderProtocol(proto:NETunnelProviderProtocol?)
}
Expand All@@ -26,12 +24,9 @@ enum VPNServiceError: Error, Equatable {
case internalError(String)
case systemExtensionError(SystemExtensionState)
case networkExtensionError(NetworkExtensionState)
case longTestError

vardescription:String{
switchself{
case.longTestError:
"This is a long error to test the UI with long errors"
caselet.internalError(description):
"Internal Error:\(description)"
caselet.systemExtensionError(state):
Expand All@@ -47,6 +42,7 @@ final class CoderVPNService: NSObject, VPNService {
varlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"vpn")
lazyvarxpc:VPNXPCInterface=.init(vpn:self)
varterminating=false
varworkspaces:[UUID:String]=[:]

@PublishedvartunnelState:VPNServiceState=.disabled
@PublishedvarsysExtnState:SystemExtensionState=.uninstalled
Expand All@@ -61,7 +57,7 @@ final class CoderVPNService: NSObject, VPNService {
return tunnelState
}

@Publishedvaragents:[Agent]=[]
@Publishedvaragents:[UUID:Agent]=[:]

// systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get
// garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework
Expand All@@ -74,6 +70,16 @@ final class CoderVPNService: NSObject, VPNService {
Task{
awaitloadNetworkExtension()
}
NotificationCenter.default.addObserver(
self,
selector: #selector(vpnDidUpdate(_:)),
name:.NEVPNStatusDidChange,
object:nil
)
}

deinit{
NotificationCenter.default.removeObserver(self)
}

func start()async{
Expand All@@ -84,16 +90,14 @@ final class CoderVPNService: NSObject, VPNService {
return
}

awaitenableNetworkExtension()
// this ping is somewhat load bearing since it causes xpc to init
xpc.ping()
tunnelState=.connecting
awaitenableNetworkExtension()
logger.debug("network extension enabled")
}

func stop()async{
guard tunnelState==.connectedelse{return}
tunnelState=.disconnecting
awaitdisableNetworkExtension()
logger.info("network extension stopped")
}
Expand DownExpand Up@@ -131,31 +135,97 @@ final class CoderVPNService: NSObject, VPNService {
}

func onExtensionPeerUpdate(_ data:Data){
// TODO: handle peer update
logger.info("network extension peer update")
do{
letmsg=tryVpn_TunnelMessage(serializedBytes: data)
letmsg=tryVpn_PeerUpdate(serializedBytes: data)
debugPrint(msg)
applyPeerUpdate(with: msg)
}catch{
logger.error("failed to decode peer update\(error)")
}
}

func onExtensionStart(){
logger.info("network extension reported started")
tunnelState=.connected
}
func applyPeerUpdate(with update:Vpn_PeerUpdate){
// Delete agents
update.deletedAgents
.compactMap{UUID(uuidData: $0.id)}
.forEach{ agentIDin
agents[agentID]=nil
}
update.deletedWorkspaces
.compactMap{UUID(uuidData: $0.id)}
.forEach{ workspaceIDin
workspaces[workspaceID]=nil
for(id, agent)in agentswhere agent.wsID== workspaceID{
agents[id]=nil
}
}

func onExtensionStop(){
logger.info("network extension reported stopped")
tunnelState=.disabled
if terminating{
NSApp.reply(toApplicationShouldTerminate:true)
// Update workspaces
forworkspaceProtoin update.upsertedWorkspaces{
iflet workspaceID=UUID(uuidData: workspaceProto.id){
workspaces[workspaceID]= workspaceProto.name
}
}

foragentProtoin update.upsertedAgents{
guardlet agentID=UUID(uuidData: agentProto.id)else{
continue
}
guardlet workspaceID=UUID(uuidData: agentProto.workspaceID)else{
continue
}
letworkspaceName=workspaces[workspaceID]??"Unknown Workspace"
letnewAgent=Agent(
id: agentID,
name: agentProto.name,
// If last handshake was not within last five minutes, the agent is unhealthy
status: agentProto.lastHandshake.date>Date.now.addingTimeInterval(-300)?.okay:.off,
copyableDNS: agentProto.fqdn.first??"UNKNOWN",
wsName: workspaceName,
wsID: workspaceID
)

// An existing agent with the same name, belonging to the same workspace
// is from a previous workspace build, and should be removed.
agents
.filter{ $0.value.name== agentProto.name && $0.value.wsID== workspaceID}
.forEach{agents[$0.key]=nil}

agents[agentID]= newAgent
}
}
}

func onExtensionError(_ error:NSError){
logger.error("network extension reported error:\(error)")
tunnelState=.failed(.internalError(error.localizedDescription))
extensionCoderVPNService{
@objcprivatefunc vpnDidUpdate(_ notification:Notification){
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

macOS can tell us when the Network Extension changes state, including if there was an error that caused it to disconnect, e.g. NE crashes,ptp.cancelTunnelWithError, start/stopcompletionHandler(err)

guardlet connection= notification.objectas?NETunnelProviderSessionelse{
return
}
switch connection.status{
case.disconnected:
if terminating{
NSApp.reply(toApplicationShouldTerminate:true)
}
connection.fetchLastDisconnectError{ errin
self.tunnelState=iflet err{
.failed(.internalError(err.localizedDescription))
}else{
.disabled
}
}
case.connecting:
tunnelState=.connecting
case.connected:
tunnelState=.connected
case.reasserting:
tunnelState=.connecting
case.disconnecting:
tunnelState=.disconnecting
case.invalid:
tunnelState=.failed(.networkExtensionError(.unconfigured))
@unknowndefault:
tunnelState=.disabled
}
}
}
39 changes: 26 additions & 13 deletionsCoder Desktop/Coder Desktop/Views/Agent.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
import SwiftUI

structAgent:Identifiable,Equatable{
structAgent:Identifiable,Equatable,Comparable{
letid:UUID
letname:String
letstatus:AgentStatus
letcopyableDNS:String
letworkspaceName:String
letwsName:String
letwsID:UUID

// Agents are sorted by status, and then by name
staticfunc<(lhs:Agent, rhs:Agent)->Bool{
if lhs.status!= rhs.status{
return lhs.status< rhs.status
}
return lhs.wsName.localizedCompare(rhs.wsName)==.orderedAscending
}
}

enumAgentStatus:Equatable{
case okay
case warn
case error
case off
enumAgentStatus:Int,Equatable,Comparable{
case okay=0
case warn=1
case error=2
case off=3

publicvarcolor:Color{
switchself{
Expand All@@ -22,16 +31,20 @@ enum AgentStatus: Equatable {
case.off:.gray
}
}

staticfunc<(lhs:AgentStatus, rhs:AgentStatus)->Bool{
lhs.rawValue< rhs.rawValue
}
}

structAgentRowView:View{
letworkspace:Agent
letagent:Agent
letbaseAccessURL:URL
@StateprivatevarnameIsSelected:Bool=false
@StateprivatevarcopyIsSelected:Bool=false

privatevarfmtWsName:AttributedString{
varformattedName=AttributedString(workspace.name)
varformattedName=AttributedString(agent.wsName)
formattedName.foregroundColor=.primary
varcoderPart=AttributedString(".coder")
coderPart.foregroundColor=.gray
Expand All@@ -41,7 +54,7 @@ struct AgentRowView: View {

privatevarwsURL:URL{
// TODO: CoderVPN currently only supports owned workspaces
baseAccessURL.appending(path:"@me").appending(path:workspace.workspaceName)
baseAccessURL.appending(path:"@me").appending(path:agent.wsName)
}

varbody:someView{
Expand All@@ -50,10 +63,10 @@ struct AgentRowView: View {
HStack(spacing:Theme.Size.trayPadding){
ZStack{
Circle()
.fill(workspace.status.color.opacity(0.4))
.fill(agent.status.color.opacity(0.4))
.frame(width:12, height:12)
Circle()
.fill(workspace.status.color.opacity(1.0))
.fill(agent.status.color.opacity(1.0))
.frame(width:7, height:7)
}
Text(fmtWsName).lineLimit(1).truncationMode(.tail)
Expand All@@ -69,7 +82,7 @@ struct AgentRowView: View {
}.buttonStyle(.plain)
Button{
// TODO: Proper clipboard abstraction
NSPasteboard.general.setString(workspace.copyableDNS, forType:.string)
NSPasteboard.general.setString(agent.copyableDNS, forType:.string)
} label:{
Image(systemName:"doc.on.doc")
.symbolVariant(.fill)
Expand Down
9 changes: 5 additions & 4 deletionsCoder Desktop/Coder Desktop/Views/Agents.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,12 @@ struct Agents<VPN: VPNService, S: Session>: View {

varbody:someView{
Group{
//Workspaces List
//Agents List
if vpn.state==.connected{
letvisibleData= viewAll? vpn.agents:Array(vpn.agents.prefix(defaultVisibleRows))
ForEach(visibleData, id: \.id){ workspacein
AgentRowView(workspace: workspace, baseAccessURL: session.baseAccessURL!)
letsortedAgents= vpn.agents.values.sorted()
letvisibleData= viewAll?sortedAgents[...]: sortedAgents.prefix(defaultVisibleRows)
ForEach(visibleData, id: \.id){ agentin
AgentRowView(agent: agent, baseAccessURL: session.baseAccessURL!)
.padding(.horizontal,Theme.Size.trayMargin)
}
if vpn.agents.count> defaultVisibleRows{
Expand Down
19 changes: 19 additions & 0 deletionsCoder Desktop/Coder Desktop/Views/Util.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,3 +12,22 @@ final class Inspection<V> {
}
}
}

extension UUID {
var uuidData: Data {
withUnsafePointer(to: uuid) {
Data(bytes: $0, count: MemoryLayout.size(ofValue: uuid))
}
}

init?(uuidData: Data) {
guard uuidData.count == 16 else {
return nil
}
var uuid: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Very funny: there's no fixed size arrays in Swift (probably cause of objc), souuid_t is just a tuple of 16 u8s.
Also, the proposal to add one includes calling that new type a Vectorhttps://forums.swift.org/t/second-review-se-0453-vector-a-fixed-size-array/76412/20

withUnsafeMutableBytes(of: &uuid) {
$0.copyBytes(from: uuidData)
}
self.init(uuid: uuid)
}
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp