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,90 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
1842 return lhs. wsName. localizedCompare ( rhs. wsName) == . orderedAscending
1943 }
2044
45+ var statusString : String {
46+ if status == . error {
47+ return status. description
48+ }
49+
50+ guard let lastPing else {
51+ // either:
52+ // - old coder deployment
53+ // - we haven't received any pings yet
54+ return status. description
55+ }
56+
57+ var str : 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+ if let preferredDerpLatency = lastPing. preferredDerpLatency {
73+ str += " \n You ↔ \( lastPing. preferredDerp) : \( preferredDerpLatency. prettyPrintMs) "
74+ let derpToWorkspaceEstLatency = 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 \n Last handshake: \( lastHandshake? . relativeTimeString ?? " Unknown " ) "
84+ return str
85+ }
86+
2187 let primaryHost : String
2288}
2389
90+ extension TimeInterval {
91+ var prettyPrintMs : String {
92+ Measurement ( value: self * 1000 , unit: UnitDuration . milliseconds)
93+ . formatted ( . measurement( width: . abbreviated,
94+ numberFormatStyle: . number. precision ( . fractionLength( 2 ) ) ) )
95+ }
96+ }
97+
98+ struct LastPing : Equatable , Hashable {
99+ let latency : TimeInterval
100+ let didP2p : Bool
101+ let preferredDerp : String
102+ let preferredDerpLatency : TimeInterval ?
103+ }
104+
24105enum AgentStatus : 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+ public var description : String {
113+ switch self {
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 public var color : Color {
31123 switch self {
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+ var lastPing : 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+ extension Date {
265+ var relativeTimeString : String {
266+ let formatter = RelativeDateTimeFormatter ( )
267+ formatter. unitsStyle = . full
268+ if Date . 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+ extension SwiftProtobuf . Google_Protobuf_Timestamp {
277+ var maybeDate : Date ? {
278+ guard seconds > 0 else { return nil }
279+ return date
280+ }
281+ }
282+
283+ extension Vpn_Agent {
284+ var healthyLastHandshakeMin : Date {
285+ Date . now. addingTimeInterval ( - 500 ) // 5 minutes ago
286+ }
287+
288+ var healthyPingMax : TimeInterval { 0.15 } // 150ms
289+
290+ var status : AgentStatus {
291+ guard let lastHandshake = lastHandshake. maybeDate else {
292+ // Initially the handshake is missing
293+ return . connecting
294+ }
295+
296+ return if lastHandshake < healthyLastHandshakeMin {
297+ // If last handshake was not within the last five minutes, the agent
298+ // is potentially unhealthy.
299+ . error
300+ } else if hasLastPing, lastPing. latency. timeInterval < healthyPingMax {
301+ // If latency is less than 150ms
302+ . okay
303+ } else if 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+ }
0 commit comments