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

feat: include ping and network stats on status tooltip#181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
ethanndickson wants to merge4 commits intomain
base:main
Choose a base branch
Loading
fromethan/ping-tooltip
Open
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletionsCoder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,6 +84,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

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

Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,21 +5,21 @@ import SwiftUI
final class PreviewVPN: Coder_Desktop.VPNService {
@Published var state: Coder_Desktop.VPNServiceState = .connected
@Published var menuState: VPNMenuState = .init(agents: [
UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2",
UUID(): Agent(id: UUID(), name: "dev", status: .no_recent_handshake, hosts: ["asdf.coder"], wsName: "dogfood2",
wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"],
wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc",
UUID(): Agent(id: UUID(), name: "dev", status: .high_latency, hosts: ["asdf.coder"], wsName: "opensrc",
wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor",
wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "example",
wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2",
UUID(): Agent(id: UUID(), name: "dev", status: .no_recent_handshake, hosts: ["asdf.coder"], wsName: "dogfood2",
wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"],
wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc",
UUID(): Agent(id: UUID(), name: "dev", status: .high_latency, hosts: ["asdf.coder"], wsName: "opensrc",
wsID: UUID(), primaryHost: "asdf.coder"),
UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor",
wsID: UUID(), primaryHost: "asdf.coder"),
Expand Down
1 change: 1 addition & 0 deletionsCoder-Desktop/Coder-Desktop/Theme.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ enum Theme {

enum Animation {
static let collapsibleDuration = 0.2
static let tooltipDelay: Int = 250 // milliseconds
}

static let defaultVisibleAgents = 5
Expand Down
170 changes: 163 additions & 7 deletionsCoder-Desktop/Coder-Desktop/VPN/MenuState.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import Foundation
import SwiftProtobuf
import SwiftUI
import VPNLib

Expand All@@ -9,6 +10,29 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
let hosts: [String]
let wsName: String
let wsID: UUID
let lastPing: LastPing?
let lastHandshake: Date?

init(id: UUID,
name: String,
status: AgentStatus,
hosts: [String],
wsName: String,
wsID: UUID,
lastPing: LastPing? = nil,
lastHandshake: Date? = nil,
primaryHost: String)
{
self.id = id
self.name = name
self.status = status
self.hosts = hosts
self.wsName = wsName
self.wsID = wsID
self.lastPing = lastPing
self.lastHandshake = lastHandshake
self.primaryHost = primaryHost
}

// Agents are sorted by status, and then by name
static func < (lhs: Agent, rhs: Agent) -> Bool {
Expand All@@ -18,21 +42,94 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
return lhs.wsName.localizedCompare(rhs.wsName) == .orderedAscending
}

var statusString: String {
switch status {
case .okay, .high_latency:
break
default:
return status.description
}

guard let lastPing else {
// Either:
// - Old coder deployment
// - We haven't received any pings yet
return status.description
}

let highLatencyWarning = status == .high_latency ? "(High latency)" : ""

var str: String
if lastPing.didP2p {
str = """
You're connected peer-to-peer. \(highLatencyWarning)

You ↔ \(lastPing.latency.prettyPrintMs) ↔ \(wsName)
"""
} else {
str = """
You're connected through a DERP relay. \(highLatencyWarning)
We'll switch over to peer-to-peer when available.

Total latency: \(lastPing.latency.prettyPrintMs)
"""
// We're not guranteed to have the preferred DERP latency
if let preferredDerpLatency = lastPing.preferredDerpLatency {
str += "\nYou ↔ \(lastPing.preferredDerp): \(preferredDerpLatency.prettyPrintMs)"
let derpToWorkspaceEstLatency = lastPing.latency - preferredDerpLatency
// We're not guaranteed the preferred derp latency is less than
// the total, as they might have been recorded at slightly
// different times, and we don't want to show a negative value.
if derpToWorkspaceEstLatency > 0 {
str += "\n\(lastPing.preferredDerp) ↔ \(wsName): \(derpToWorkspaceEstLatency.prettyPrintMs)"
}
}
}
str += "\n\nLast handshake: \(lastHandshake?.relativeTimeString ?? "Unknown")"
return str
}

let primaryHost: String
}

extension TimeInterval {
var prettyPrintMs: String {
let milliseconds = self * 1000
return "\(milliseconds.formatted(.number.precision(.fractionLength(2)))) ms"
}
}

struct LastPing: Equatable, Hashable {
let latency: TimeInterval
let didP2p: Bool
let preferredDerp: String
let preferredDerpLatency: TimeInterval?
}

enum AgentStatus: Int, Equatable, Comparable {
case okay = 0
case warn = 1
case error = 2
case off = 3
case connecting = 1
case high_latency = 2
case no_recent_handshake = 3
case off = 4

public var description: String {
switch self {
case .okay: "Connected"
case .connecting: "Connecting..."
case .high_latency: "Connected, but with high latency" // Message currently unused
case .no_recent_handshake: "Could not establish a connection to the agent. Retrying..."
case .off: "Offline"
}
}

public var color: Color {
switch self {
case .okay: .green
case .warn: .yellow
case .error: .red
case .high_latency: .yellow
case .no_recent_handshake: .red
case .off: .secondary
case .connecting: .yellow
}
}

Expand DownExpand Up@@ -87,14 +184,27 @@ struct VPNMenuState {
workspace.agents.insert(id)
workspaces[wsID] = workspace

var lastPing: LastPing?
if agent.hasLastPing {
lastPing = LastPing(
latency: agent.lastPing.latency.timeInterval,
didP2p: agent.lastPing.didP2P,
preferredDerp: agent.lastPing.preferredDerp,
preferredDerpLatency:
agent.lastPing.hasPreferredDerpLatency
? agent.lastPing.preferredDerpLatency.timeInterval
: nil
)
}
agents[id] = Agent(
id: id,
name: agent.name,
// If last handshake was not within last five minutes, the agent is unhealthy
status: agent.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .warn,
status: agent.status,
hosts: nonEmptyHosts,
wsName: workspace.name,
wsID: wsID,
lastPing: lastPing,
lastHandshake: agent.lastHandshake.maybeDate,
// Hosts arrive sorted by length, the shortest looks best in the UI.
primaryHost: nonEmptyHosts.first!
)
Expand DownExpand Up@@ -154,3 +264,49 @@ struct VPNMenuState {
workspaces.removeAll()
}
}

extension Date {
var relativeTimeString: String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .full
if Date.now.timeIntervalSince(self) < 1.0 {
// Instead of showing "in 0 seconds"
return "Just now"
}
return formatter.localizedString(for: self, relativeTo: Date.now)
}
}

extension SwiftProtobuf.Google_Protobuf_Timestamp {
var maybeDate: Date? {
guard seconds > 0 else { return nil }
return date
}
}

extension Vpn_Agent {
var healthyLastHandshakeMin: Date {
Date.now.addingTimeInterval(-300) // 5 minutes ago
}

var healthyPingMax: TimeInterval { 0.15 } // 150ms

var status: AgentStatus {
// Initially the handshake is missing
guard let lastHandshake = lastHandshake.maybeDate else {
return .connecting
}
// If last handshake was not within the last five minutes, the agent
// is potentially unhealthy.
guard lastHandshake >= healthyLastHandshakeMin else {
return .no_recent_handshake
}
// No ping data, but we have a recent handshake.
// We show green for backwards compatibility with old Coder
// deployments.
guard hasLastPing else {
return .okay
}
return lastPing.latency.timeInterval < healthyPingMax ? .okay : .high_latency
}
}
8 changes: 8 additions & 0 deletionsCoder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
}
}

var statusString: String {
switch self {
case let .agent(agent): agent.statusString
case .offlineWorkspace: status.description
}
}

var id: UUID {
switch self {
case let .agent(agent): agent.id
Expand DownExpand Up@@ -224,6 +231,7 @@ struct MenuItemIcons: View {
StatusDot(color: item.status.color)
.padding(.trailing, 3)
.padding(.top, 1)
.help(item.statusString)
MenuItemIconButton(systemName: "doc.on.doc", action: copyToClipboard)
.font(.system(size: 9))
.symbolVariant(.fill)
Expand Down
1 change: 1 addition & 0 deletionsCoder-Desktop/Coder-DesktopTests/AgentsTests.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ struct AgentsTests {
hosts: ["a\($0).coder"],
wsName: "ws\($0)",
wsID: UUID(),
lastPing: nil,
primaryHost: "a\($0).coder"
)
return (agent.id, agent)
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp