11import Foundation
2+ import SwiftProtobuf
23import SwiftUI
34import VPNLib
45
@@ -9,6 +10,29 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
910let hosts : [ String ]
1011let wsName : String
1112let 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
1438static func < ( lhs: Agent , rhs: Agent ) -> Bool {
@@ -18,21 +42,94 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
1842return 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 lastPingelse {
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+
2192let 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 {
25110case 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
30126public var color : Color {
31127switch self {
32128case . okay: . green
33- case . warn : . yellow
34- case . error : . red
129+ case . high_latency : . yellow
130+ case . no_recent_handshake : . red
35131case . off: . secondary
132+ case . connecting: . yellow
36133}
37134}
38135
@@ -87,14 +184,27 @@ struct VPNMenuState {
87184 workspace. agents. insert ( id)
88185workspaces [ 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+ }
90199agents [ 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. maybeDateelse {
297+ return . connecting
298+ }
299+ // If last handshake was not within the last five minutes, the agent
300+ // is potentially unhealthy.
301+ guard lastHandshake>= healthyLastHandshakeMinelse {
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 hasLastPingelse {
308+ return . okay
309+ }
310+ return lastPing. latency. timeInterval< healthyPingMax? . okay: . high_latency
311+ }
312+ }