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

Commit88ae5f9

Browse files
committed
feat: include ping and network stats on status tooltip
1 parent170b399 commit88ae5f9

File tree

10 files changed

+337
-11
lines changed

10 files changed

+337
-11
lines changed

‎Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
8484
}
8585

8686
func applicationDidFinishLaunching(_:Notification){
87+
// We have important file sync and network info behind tooltips,
88+
// so the default delay is too long.
89+
UserDefaults.standard.setValue(Theme.Animation.tooltipDelay, forKey:"NSInitialToolTipDelay")
8790
// Init SVG loader
8891
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
8992

‎Coder-Desktop/Coder-Desktop/Theme.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum Theme {
1515

1616
enumAnimation{
1717
staticletcollapsibleDuration=0.2
18+
staticlettooltipDelay:Int=250 // milliseconds
1819
}
1920

2021
staticletdefaultVisibleAgents=5

‎Coder-Desktop/Coder-Desktop/VPN/MenuState.swift

Lines changed: 162 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import SwiftProtobuf
23
import SwiftUI
34
import VPNLib
45

@@ -9,6 +10,29 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
910
lethosts:[String]
1011
letwsName:String
1112
letwsID:UUID
13+
letlastPing:LastPing?
14+
letlastHandshake:Date?
15+
16+
init(id:UUID,
17+
name:String,
18+
status:AgentStatus,
19+
hosts:[String],
20+
wsName:String,
21+
wsID:UUID,
22+
lastPing:LastPing?=nil,
23+
lastHandshake:Date?=nil,
24+
primaryHost:String)
25+
{
26+
self.id= id
27+
self.name= name
28+
self.status= status
29+
self.hosts= hosts
30+
self.wsName= wsName
31+
self.wsID= wsID
32+
self.lastPing= lastPing
33+
self.lastHandshake= lastHandshake
34+
self.primaryHost= primaryHost
35+
}
1236

1337
// Agents are sorted by status, and then by name
1438
staticfunc<(lhs:Agent, rhs:Agent)->Bool{
@@ -18,21 +42,90 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
1842
return lhs.wsName.localizedCompare(rhs.wsName)==.orderedAscending
1943
}
2044

45+
varstatusString:String{
46+
if status==.error{
47+
return status.description
48+
}
49+
50+
guardlet lastPingelse{
51+
// either:
52+
// - old coder deployment
53+
// - we haven't received any pings yet
54+
return status.description
55+
}
56+
57+
varstr:String
58+
if lastPing.didP2p{
59+
str="""
60+
You're connected peer-to-peer.
61+
62+
You ↔\(lastPing.latency.prettyPrintMs)\(wsName)
63+
"""
64+
}else{
65+
str="""
66+
You're connected through a DERP relay.
67+
We'll switch over to peer-to-peer when available.
68+
69+
Total latency:\(lastPing.latency.prettyPrintMs)
70+
"""
71+
// We're not guranteed to have the preferred DERP latency
72+
iflet preferredDerpLatency= lastPing.preferredDerpLatency{
73+
str+="\nYou ↔\(lastPing.preferredDerp):\(preferredDerpLatency.prettyPrintMs)"
74+
letderpToWorkspaceEstLatency= lastPing.latency- preferredDerpLatency
75+
// We're not guaranteed the preferred derp latency is less than
76+
// the total, as they might have been recorded at slightly
77+
// different times, and we don't want to show a negative value.
78+
if derpToWorkspaceEstLatency>0{
79+
str+="\n\(lastPing.preferredDerp)\(wsName):\(derpToWorkspaceEstLatency.prettyPrintMs)"
80+
}
81+
}
82+
}
83+
str+="\n\nLast handshake:\(lastHandshake?.relativeTimeString??"Unknown")"
84+
return str
85+
}
86+
2187
letprimaryHost:String
2288
}
2389

90+
extensionTimeInterval{
91+
varprettyPrintMs:String{
92+
Measurement(value:self*1000, unit:UnitDuration.milliseconds)
93+
.formatted(.measurement(width:.abbreviated,
94+
numberFormatStyle:.number.precision(.fractionLength(2))))
95+
}
96+
}
97+
98+
structLastPing:Equatable,Hashable{
99+
letlatency:TimeInterval
100+
letdidP2p:Bool
101+
letpreferredDerp:String
102+
letpreferredDerpLatency:TimeInterval?
103+
}
104+
24105
enumAgentStatus:Int,Equatable,Comparable{
25106
case okay=0
26-
case warn=1
27-
case error=2
28-
case off=3
107+
case connecting=1
108+
case warn=2
109+
case error=3
110+
case off=4
111+
112+
publicvardescription:String{
113+
switchself{
114+
case.okay:"Connected"
115+
case.connecting:"Connecting..."
116+
case.warn:"Connected, but with high latency" // Currently unused
117+
case.error:"Could not establish a connection to the agent. Retrying..."
118+
case.off:"Offline"
119+
}
120+
}
29121

30122
publicvarcolor:Color{
31123
switchself{
32124
case.okay:.green
33125
case.warn:.yellow
34126
case.error:.red
35127
case.off:.secondary
128+
case.connecting:.yellow
36129
}
37130
}
38131

@@ -87,14 +180,27 @@ struct VPNMenuState {
87180
workspace.agents.insert(id)
88181
workspaces[wsID]= workspace
89182

183+
varlastPing:LastPing?
184+
if agent.hasLastPing{
185+
lastPing=LastPing(
186+
latency: agent.lastPing.latency.timeInterval,
187+
didP2p: agent.lastPing.didP2P,
188+
preferredDerp: agent.lastPing.preferredDerp,
189+
preferredDerpLatency:
190+
agent.lastPing.hasPreferredDerpLatency
191+
? agent.lastPing.preferredDerpLatency.timeInterval
192+
:nil
193+
)
194+
}
90195
agents[id]=Agent(
91196
id: id,
92197
name: agent.name,
93-
// If last handshake was not within last five minutes, the agent is unhealthy
94-
status: agent.lastHandshake.date>Date.now.addingTimeInterval(-300)?.okay:.warn,
198+
status: agent.status,
95199
hosts: nonEmptyHosts,
96200
wsName: workspace.name,
97201
wsID: wsID,
202+
lastPing: lastPing,
203+
lastHandshake: agent.lastHandshake.maybeDate,
98204
// Hosts arrive sorted by length, the shortest looks best in the UI.
99205
primaryHost: nonEmptyHosts.first!
100206
)
@@ -154,3 +260,54 @@ struct VPNMenuState {
154260
workspaces.removeAll()
155261
}
156262
}
263+
264+
extensionDate{
265+
varrelativeTimeString:String{
266+
letformatter=RelativeDateTimeFormatter()
267+
formatter.unitsStyle=.full
268+
ifDate.now.timeIntervalSince(self)<1.0{
269+
// Instead of showing "in 0 seconds"
270+
return"Just now"
271+
}
272+
return formatter.localizedString(for:self, relativeTo:Date.now)
273+
}
274+
}
275+
276+
extensionSwiftProtobuf.Google_Protobuf_Timestamp{
277+
varmaybeDate:Date?{
278+
guard seconds>0else{returnnil}
279+
return date
280+
}
281+
}
282+
283+
extensionVpn_Agent{
284+
varhealthyLastHandshakeMin:Date{
285+
Date.now.addingTimeInterval(-500) // 5 minutes ago
286+
}
287+
288+
varhealthyPingMax:TimeInterval{0.15} // 150ms
289+
290+
varstatus:AgentStatus{
291+
guardlet lastHandshake= lastHandshake.maybeDateelse{
292+
// Initially the handshake is missing
293+
return.connecting
294+
}
295+
296+
returnif lastHandshake< healthyLastHandshakeMin{
297+
// If last handshake was not within the last five minutes, the agent
298+
// is potentially unhealthy.
299+
.error
300+
}elseif hasLastPing, lastPing.latency.timeInterval< healthyPingMax{
301+
// If latency is less than 150ms
302+
.okay
303+
}elseif hasLastPing, lastPing.latency.timeInterval>= healthyPingMax{
304+
// if latency is greater than 150ms
305+
.warn
306+
}else{
307+
// No ping data, but we have a recent handshake.
308+
// We show green for backwards compatibility with old Coder
309+
// deployments.
310+
.okay
311+
}
312+
}
313+
}

‎Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
2121
}
2222
}
2323

24+
varstatusString:String{
25+
switchself{
26+
caselet.agent(agent): agent.statusString
27+
case.offlineWorkspace: status.description
28+
}
29+
}
30+
2431
varid:UUID{
2532
switchself{
2633
caselet.agent(agent): agent.id
@@ -224,6 +231,7 @@ struct MenuItemIcons: View {
224231
StatusDot(color: item.status.color)
225232
.padding(.trailing,3)
226233
.padding(.top,1)
234+
.help(item.statusString)
227235
MenuItemIconButton(systemName:"doc.on.doc", action: copyToClipboard)
228236
.font(.system(size:9))
229237
.symbolVariant(.fill)

‎Coder-Desktop/Coder-DesktopTests/AgentsTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ struct AgentsTests {
2828
hosts:["a\($0).coder"],
2929
wsName:"ws\($0)",
3030
wsID:UUID(),
31+
lastPing:nil,
3132
primaryHost:"a\($0).coder"
3233
)
3334
return(agent.id, agent)

‎Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ struct VPNMenuStateTests {
1818
$0.workspaceID= workspaceID.uuidData
1919
$0.name="dev"
2020
$0.lastHandshake=.init(date:Date.now)
21+
$0.lastPing=.with{
22+
$0.latency=.init(floatLiteral:0.05)
23+
}
2124
$0.fqdn=["foo.coder"]
2225
}
2326

@@ -72,6 +75,29 @@ struct VPNMenuStateTests {
7275
#expect(state.workspaces[workspaceID]==nil)
7376
}
7477

78+
@Test
79+
mutatingfunc testUpsertAgent_poorConnection()asyncthrows{
80+
letagentID=UUID()
81+
letworkspaceID=UUID()
82+
state.upsertWorkspace(Vpn_Workspace.with{ $0.id= workspaceID.uuidData; $0.name="foo"})
83+
84+
letagent=Vpn_Agent.with{
85+
$0.id= agentID.uuidData
86+
$0.workspaceID= workspaceID.uuidData
87+
$0.name="agent1"
88+
$0.lastHandshake=.init(date:Date.now)
89+
$0.lastPing=.with{
90+
$0.latency=.init(seconds:1)
91+
}
92+
$0.fqdn=["foo.coder"]
93+
}
94+
95+
state.upsertAgent(agent)
96+
97+
letstoredAgent=try #require(state.agents[agentID])
98+
#expect(storedAgent.status==.warn)
99+
}
100+
75101
@Test
76102
mutatingfunc testUpsertAgent_unhealthyAgent()asyncthrows{
77103
letagentID=UUID()
@@ -89,7 +115,7 @@ struct VPNMenuStateTests {
89115
state.upsertAgent(agent)
90116

91117
letstoredAgent=try #require(state.agents[agentID])
92-
#expect(storedAgent.status==.warn)
118+
#expect(storedAgent.status==.error)
93119
}
94120

95121
@Test
@@ -114,6 +140,9 @@ struct VPNMenuStateTests {
114140
$0.workspaceID= workspaceID.uuidData
115141
$0.name="agent1" // Same name as old agent
116142
$0.lastHandshake=.init(date:Date.now)
143+
$0.lastPing=.with{
144+
$0.latency=.init(floatLiteral:0.05)
145+
}
117146
$0.fqdn=["foo.coder"]
118147
}
119148

@@ -146,6 +175,9 @@ struct VPNMenuStateTests {
146175
$0.workspaceID= workspaceID.uuidData
147176
$0.name="agent1"
148177
$0.lastHandshake=.init(date:Date.now.addingTimeInterval(-200))
178+
$0.lastPing=.with{
179+
$0.latency=.init(floatLiteral:0.05)
180+
}
149181
$0.fqdn=["foo.coder"]
150182
}
151183
state.upsertAgent(agent)

‎Coder-Desktop/VPN/Manager.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ actor Manager {
4040
dest: dest,
4141
urlSession:URLSession(configuration: sessionConfig)
4242
){ progressin
43-
// TODO: Debounce, somehow
4443
pushProgress(stage:.downloading, downloadProgress: progress)
4544
}
4645
}catch{
@@ -322,7 +321,7 @@ func writeVpnLog(_ log: Vpn_Log) {
322321
category: log.loggerNames.joined(separator:".")
323322
)
324323
letfields= log.fields.map{"\($0.name):\($0.value)"}.joined(separator:",")
325-
logger.log(level: level,"\(log.message, privacy:.public):\(fields, privacy:.public)")
324+
logger.log(level: level,"\(log.message, privacy:.public)\(fields.isEmpty?"":":\(fields)", privacy:.public)")
326325
}
327326

328327
privatefunc removeQuarantine(_ dest:URL)asyncthrows(ManagerError){

‎Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ public extension MutagenDaemon {
4747
}
4848
}
4949
do{
50-
// The first creation will need to transfer the agent binary
51-
// TODO: Because this is pretty long, we should show progress updates
52-
// using the prompter messages
5350
_=tryawait client!.sync.create(req, callOptions:.init(timeLimit:.timeout(sessionMgmtReqTimeout*4)))
5451
}catch{
5552
throw.grpcFailure(error)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp