1
1
import Foundation
2
+ import SwiftProtobuf
2
3
import SwiftUI
3
4
import VPNLib
4
5
@@ -9,6 +10,29 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
9
10
let hosts : [ String ]
10
11
let wsName : String
11
12
let wsID : UUID
13
+ let lastPing : LastPing ?
14
+ let lastHandshake : 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
+ }
12
36
13
37
// Agents are sorted by status, and then by name
14
38
static func < ( lhs: Agent , rhs: Agent ) -> Bool {
@@ -18,21 +42,94 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
18
42
return lhs. wsName. localizedCompare ( rhs. wsName) == . orderedAscending
19
43
}
20
44
45
+ var statusString : String {
46
+ switch status {
47
+ case . okay, . high_latency:
48
+ break
49
+ default :
50
+ return status. description
51
+ }
52
+
53
+ guard let lastPing else {
54
+ // Either:
55
+ // - Old coder deployment
56
+ // - We haven't received any pings yet
57
+ return status. description
58
+ }
59
+
60
+ let highLatencyWarning = status == . high_latency ? " (High latency) " : " "
61
+
62
+ var str : String
63
+ if lastPing. didP2p {
64
+ str = """
65
+ You're connected peer-to-peer. \( highLatencyWarning)
66
+
67
+ You ↔ \( lastPing. latency. prettyPrintMs) ↔ \( wsName)
68
+ """
69
+ } else {
70
+ str = """
71
+ You're connected through a DERP relay. \( highLatencyWarning)
72
+ We'll switch over to peer-to-peer when available.
73
+
74
+ Total latency: \( lastPing. latency. prettyPrintMs)
75
+ """
76
+ // We're not guranteed to have the preferred DERP latency
77
+ if let preferredDerpLatency = lastPing. preferredDerpLatency {
78
+ str += " \n You ↔ \( lastPing. preferredDerp) : \( preferredDerpLatency. prettyPrintMs) "
79
+ let derpToWorkspaceEstLatency = lastPing. latency - preferredDerpLatency
80
+ // We're not guaranteed the preferred derp latency is less than
81
+ // the total, as they might have been recorded at slightly
82
+ // different times, and we don't want to show a negative value.
83
+ if derpToWorkspaceEstLatency > 0 {
84
+ str += " \n \( lastPing. preferredDerp) ↔ \( wsName) : \( derpToWorkspaceEstLatency. prettyPrintMs) "
85
+ }
86
+ }
87
+ }
88
+ str += " \n \n Last handshake: \( lastHandshake? . relativeTimeString ?? " Unknown " ) "
89
+ return str
90
+ }
91
+
21
92
let primaryHost : String
22
93
}
23
94
95
+ extension TimeInterval {
96
+ var prettyPrintMs : String {
97
+ let milliseconds = self * 1000
98
+ return " \( milliseconds. formatted ( . number. precision ( . fractionLength( 2 ) ) ) ) ms "
99
+ }
100
+ }
101
+
102
+ struct LastPing : Equatable , Hashable {
103
+ let latency : TimeInterval
104
+ let didP2p : Bool
105
+ let preferredDerp : String
106
+ let preferredDerpLatency : TimeInterval ?
107
+ }
108
+
24
109
enum AgentStatus : Int , Equatable , Comparable {
25
110
case okay = 0
26
- case warn = 1
27
- case error = 2
28
- case off = 3
111
+ case connecting = 1
112
+ case high_latency = 2
113
+ case no_recent_handshake = 3
114
+ case off = 4
115
+
116
+ public var description : String {
117
+ switch self {
118
+ case . okay: " Connected "
119
+ case . connecting: " Connecting... "
120
+ case . high_latency: " Connected, but with high latency " // Message currently unused
121
+ case . no_recent_handshake: " Could not establish a connection to the agent. Retrying... "
122
+ case . off: " Offline "
123
+ }
124
+ }
29
125
30
126
public var color : Color {
31
127
switch self {
32
128
case . okay: . green
33
- case . warn : . yellow
34
- case . error : . red
129
+ case . high_latency : . yellow
130
+ case . no_recent_handshake : . red
35
131
case . off: . secondary
132
+ case . connecting: . yellow
36
133
}
37
134
}
38
135
@@ -87,14 +184,27 @@ struct VPNMenuState {
87
184
workspace. agents. insert ( id)
88
185
workspaces [ wsID] = workspace
89
186
187
+ var lastPing : LastPing ?
188
+ if agent. hasLastPing {
189
+ lastPing = LastPing (
190
+ latency: agent. lastPing. latency. timeInterval,
191
+ didP2p: agent. lastPing. didP2P,
192
+ preferredDerp: agent. lastPing. preferredDerp,
193
+ preferredDerpLatency:
194
+ agent. lastPing. hasPreferredDerpLatency
195
+ ? agent. lastPing. preferredDerpLatency. timeInterval
196
+ : nil
197
+ )
198
+ }
90
199
agents [ id] = Agent (
91
200
id: id,
92
201
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,
202
+ status: agent. status,
95
203
hosts: nonEmptyHosts,
96
204
wsName: workspace. name,
97
205
wsID: wsID,
206
+ lastPing: lastPing,
207
+ lastHandshake: agent. lastHandshake. maybeDate,
98
208
// Hosts arrive sorted by length, the shortest looks best in the UI.
99
209
primaryHost: nonEmptyHosts. first!
100
210
)
@@ -154,3 +264,49 @@ struct VPNMenuState {
154
264
workspaces. removeAll ( )
155
265
}
156
266
}
267
+
268
+ extension Date {
269
+ var relativeTimeString : String {
270
+ let formatter = RelativeDateTimeFormatter ( )
271
+ formatter. unitsStyle = . full
272
+ if Date . now. timeIntervalSince ( self ) < 1.0 {
273
+ // Instead of showing "in 0 seconds"
274
+ return " Just now "
275
+ }
276
+ return formatter. localizedString ( for: self , relativeTo: Date . now)
277
+ }
278
+ }
279
+
280
+ extension SwiftProtobuf . Google_Protobuf_Timestamp {
281
+ var maybeDate : Date ? {
282
+ guard seconds > 0 else { return nil }
283
+ return date
284
+ }
285
+ }
286
+
287
+ extension Vpn_Agent {
288
+ var healthyLastHandshakeMin : Date {
289
+ Date . now. addingTimeInterval ( - 300 ) // 5 minutes ago
290
+ }
291
+
292
+ var healthyPingMax : TimeInterval { 0.15 } // 150ms
293
+
294
+ var status : AgentStatus {
295
+ // Initially the handshake is missing
296
+ guard let lastHandshake = lastHandshake. maybeDate else {
297
+ return . connecting
298
+ }
299
+ // If last handshake was not within the last five minutes, the agent
300
+ // is potentially unhealthy.
301
+ guard lastHandshake >= healthyLastHandshakeMin else {
302
+ return . no_recent_handshake
303
+ }
304
+ // No ping data, but we have a recent handshake.
305
+ // We show green for backwards compatibility with old Coder
306
+ // deployments.
307
+ guard hasLastPing else {
308
+ return . okay
309
+ }
310
+ return lastPing. latency. timeInterval < healthyPingMax ? . okay : . high_latency
311
+ }
312
+ }
0 commit comments