11import Foundation
2+ import SwiftProtobuf
23import SwiftUI
34import VPNLib
45
@@ -9,6 +10,29 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
910 let hosts : [ String ]
1011 let wsName : String
1112 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+ }
1236
1337 // Agents are sorted by status, and then by name
1438 static func < ( lhs: Agent , rhs: Agent ) -> Bool {
@@ -18,21 +42,94 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
1842 return lhs. wsName. localizedCompare ( rhs. wsName) == . orderedAscending
1943 }
2044
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+
2192 let primaryHost : String
2293}
2394
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+
24109enum AgentStatus : Int , Equatable , Comparable {
25110 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+ }
29125
30126 public var color : Color {
31127 switch self {
32128 case . okay: . green
33- case . warn : . yellow
34- case . error : . red
129+ case . high_latency : . yellow
130+ case . no_recent_handshake : . red
35131 case . off: . secondary
132+ case . connecting: . yellow
36133 }
37134 }
38135
@@ -87,14 +184,27 @@ struct VPNMenuState {
87184 workspace. agents. insert ( id)
88185 workspaces [ wsID] = workspace
89186
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+ }
90199 agents [ id] = Agent (
91200 id: id,
92201 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,
95203 hosts: nonEmptyHosts,
96204 wsName: workspace. name,
97205 wsID: wsID,
206+ lastPing: lastPing,
207+ lastHandshake: agent. lastHandshake. maybeDate,
98208 // Hosts arrive sorted by length, the shortest looks best in the UI.
99209 primaryHost: nonEmptyHosts. first!
100210 )
@@ -154,3 +264,49 @@ struct VPNMenuState {
154264 workspaces. removeAll ( )
155265 }
156266}
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