Fix duplicate traffic updates

This commit is contained in:
世界
2026-01-16 20:08:33 +08:00
parent 751cf13634
commit 9d4ce37e94
2 changed files with 97 additions and 25 deletions
+96 -24
View File
@@ -1,6 +1,7 @@
import Foundation import Foundation
import Libbox import Libbox
import os import os
import Combine
private let logger = Logger(category: "CommandClient") private let logger = Logger(category: "CommandClient")
@@ -42,6 +43,22 @@ public enum LogLevel: Int, CaseIterable, Identifiable {
} }
} }
public struct TrafficSnapshot {
public var status: LibboxStatusMessage?
public var uplinkHistory: [CGFloat]
public var downlinkHistory: [CGFloat]
public init(
status: LibboxStatusMessage? = nil,
uplinkHistory: [CGFloat] = Array(repeating: 0, count: 30),
downlinkHistory: [CGFloat] = Array(repeating: 0, count: 30)
) {
self.status = status
self.uplinkHistory = uplinkHistory
self.downlinkHistory = downlinkHistory
}
}
public class CommandClient: ObservableObject { public class CommandClient: ObservableObject {
public enum ConnectionType { public enum ConnectionType {
case status case status
@@ -54,9 +71,18 @@ public class CommandClient: ObservableObject {
private let connectionTypes: [ConnectionType] private let connectionTypes: [ConnectionType]
private let logMaxLines: Int private let logMaxLines: Int
private var commandClient: LibboxCommandClient? private var commandClient: LibboxCommandClient?
private var connectTask: Task<Void, Error>? private var connectTask: Task<Void, Never>?
private var activeConnectionToken: UInt64 = 0
private var isConnecting = false
@Published public var isConnected: Bool @Published public var isConnected: Bool
@Published public var status: LibboxStatusMessage? // Coalesce traffic updates so SwiftUI re-renders once per status tick.
@Published private var trafficSnapshot = TrafficSnapshot()
public var status: LibboxStatusMessage? { trafficSnapshot.status }
public var statusPublisher: AnyPublisher<LibboxStatusMessage?, Never> {
$trafficSnapshot
.map(\.status)
.eraseToAnyPublisher()
}
@Published public var groups: [LibboxOutboundGroup]? @Published public var groups: [LibboxOutboundGroup]?
@Published public var logList: [LogEntry] @Published public var logList: [LogEntry]
@Published public var defaultLogLevel = 0 @Published public var defaultLogLevel = 0
@@ -70,8 +96,8 @@ public class CommandClient: ObservableObject {
@Published public var hasAnyConnection: Bool = false @Published public var hasAnyConnection: Bool = false
private var connectionsStore: LibboxConnections? private var connectionsStore: LibboxConnections?
@Published public var uplinkHistory: [CGFloat] = Array(repeating: 0, count: 30) public var uplinkHistory: [CGFloat] { trafficSnapshot.uplinkHistory }
@Published public var downlinkHistory: [CGFloat] = Array(repeating: 0, count: 30) public var downlinkHistory: [CGFloat] { trafficSnapshot.downlinkHistory }
// Batch processing for logs // Batch processing for logs
private var pendingLogs: [LogEntry] = [] private var pendingLogs: [LogEntry] = []
@@ -95,18 +121,26 @@ public class CommandClient: ObservableObject {
isConnected = true isConnected = true
clashModeList = ["rule", "global", "direct"] clashModeList = ["rule", "global", "direct"]
clashMode = "rule" clashMode = "rule"
uplinkHistory = Array(repeating: CGFloat(1000), count: 30) trafficSnapshot = TrafficSnapshot(
downlinkHistory = Array(repeating: CGFloat(5000), count: 30) uplinkHistory: Array(repeating: CGFloat(1000), count: 30),
downlinkHistory: Array(repeating: CGFloat(5000), count: 30)
)
hasAnyConnection = true hasAnyConnection = true
} }
public func connect() { public func connect() {
if isConnected { if isConnected || isConnecting {
return return
} }
disconnect() if let commandClient {
connectTask = Task { try? commandClient.disconnect()
await performConnection() self.commandClient = nil
}
isConnecting = true
activeConnectionToken &+= 1
let token = activeConnectionToken
connectTask = Task { [weak self] in
await self?.performConnection(token: token)
} }
} }
@@ -115,10 +149,15 @@ public class CommandClient: ObservableObject {
connectTask.cancel() connectTask.cancel()
self.connectTask = nil self.connectTask = nil
} }
isConnecting = false
activeConnectionToken &+= 1
if let commandClient { if let commandClient {
try? commandClient.disconnect() try? commandClient.disconnect()
self.commandClient = nil self.commandClient = nil
} }
if isConnected {
isConnected = false
}
} }
private func flushPendingLogs() { private func flushPendingLogs() {
@@ -180,7 +219,7 @@ public class CommandClient: ObservableObject {
} }
} }
private nonisolated func performConnection() async { private nonisolated func performConnection(token: UInt64) async {
if connectionTypes.contains(.connections) { if connectionTypes.contains(.connections) {
await initializeConnectionFilterState() await initializeConnectionFilterState()
} }
@@ -201,26 +240,50 @@ public class CommandClient: ObservableObject {
} }
} }
clientOptions.statusInterval = Int64(NSEC_PER_SEC) clientOptions.statusInterval = Int64(NSEC_PER_SEC)
let client = LibboxNewCommandClient(clientHandler(self), clientOptions)! let client = LibboxNewCommandClient(clientHandler(self, connectionToken: token), clientOptions)!
do { do {
try client.connect() try client.connect()
} catch { } catch {
await finishConnectionAttempt(token: token, client: nil)
return return
} }
await MainActor.run { await finishConnectionAttempt(token: token, client: client)
commandClient = client }
private func finishConnectionAttempt(token: UInt64, client: LibboxCommandClient?) async {
await MainActor.run { [self] in
defer {
isConnecting = false
connectTask = nil
}
guard token == activeConnectionToken else {
if let client {
try? client.disconnect()
}
return
}
if let client {
commandClient = client
}
} }
} }
private class clientHandler: NSObject, LibboxCommandClientHandlerProtocol { private class clientHandler: NSObject, LibboxCommandClientHandlerProtocol {
private let commandClient: CommandClient private let commandClient: CommandClient
private let connectionToken: UInt64
init(_ commandClient: CommandClient) { init(_ commandClient: CommandClient, connectionToken: UInt64) {
self.commandClient = commandClient self.commandClient = commandClient
self.connectionToken = connectionToken
}
private func isActiveConnection() -> Bool {
commandClient.activeConnectionToken == connectionToken
} }
func connected() { func connected() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
if commandClient.connectionTypes.contains(.log) { if commandClient.connectionTypes.contains(.log) {
commandClient.logList = [] commandClient.logList = []
} }
@@ -230,6 +293,7 @@ public class CommandClient: ObservableObject {
func disconnected(_ message: String?) { func disconnected(_ message: String?) {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
commandClient.isConnected = false commandClient.isConnected = false
} }
if let message { if let message {
@@ -239,12 +303,14 @@ public class CommandClient: ObservableObject {
func setDefaultLogLevel(_ level: Int32) { func setDefaultLogLevel(_ level: Int32) {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
commandClient.defaultLogLevel = Int(level) commandClient.defaultLogLevel = Int(level)
} }
} }
func clearLogs() { func clearLogs() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
commandClient.clearLogs() commandClient.clearLogs()
} }
} }
@@ -253,6 +319,7 @@ public class CommandClient: ObservableObject {
guard let messageList else { guard let messageList else {
return return
} }
guard isActiveConnection() else { return }
// Collect new logs // Collect new logs
var newLogs: [LogEntry] = [] var newLogs: [LogEntry] = []
@@ -264,6 +331,7 @@ public class CommandClient: ObservableObject {
guard !newLogs.isEmpty else { return } guard !newLogs.isEmpty else { return }
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
commandClient.pendingLogs.append(contentsOf: newLogs) commandClient.pendingLogs.append(contentsOf: newLogs)
if commandClient.logBatchTimer == nil { if commandClient.logBatchTimer == nil {
let workItem = DispatchWorkItem { [weak commandClient] in let workItem = DispatchWorkItem { [weak commandClient] in
@@ -278,18 +346,17 @@ public class CommandClient: ObservableObject {
func writeStatus(_ message: LibboxStatusMessage?) { func writeStatus(_ message: LibboxStatusMessage?) {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
commandClient.status = message guard isActiveConnection() else { return }
var snapshot = commandClient.trafficSnapshot
snapshot.status = message
if let message, message.trafficAvailable { if let message, message.trafficAvailable {
var newUplink = commandClient.uplinkHistory snapshot.uplinkHistory.removeFirst()
newUplink.removeFirst() snapshot.uplinkHistory.append(CGFloat(message.uplink))
newUplink.append(CGFloat(message.uplink))
commandClient.uplinkHistory = newUplink
var newDownlink = commandClient.downlinkHistory snapshot.downlinkHistory.removeFirst()
newDownlink.removeFirst() snapshot.downlinkHistory.append(CGFloat(message.downlink))
newDownlink.append(CGFloat(message.downlink))
commandClient.downlinkHistory = newDownlink
} }
commandClient.trafficSnapshot = snapshot
} }
} }
@@ -297,17 +364,20 @@ public class CommandClient: ObservableObject {
guard let groups else { guard let groups else {
return return
} }
guard isActiveConnection() else { return }
var newGroups: [LibboxOutboundGroup] = [] var newGroups: [LibboxOutboundGroup] = []
while groups.hasNext() { while groups.hasNext() {
newGroups.append(groups.next()!) newGroups.append(groups.next()!)
} }
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
commandClient.groups = newGroups commandClient.groups = newGroups
} }
} }
func initializeClashMode(_ modeList: LibboxStringIteratorProtocol?, currentMode: String?) { func initializeClashMode(_ modeList: LibboxStringIteratorProtocol?, currentMode: String?) {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
commandClient.clashModeList = modeList!.toArray() commandClient.clashModeList = modeList!.toArray()
commandClient.clashMode = currentMode! commandClient.clashMode = currentMode!
} }
@@ -315,6 +385,7 @@ public class CommandClient: ObservableObject {
func updateClashMode(_ newMode: String?) { func updateClashMode(_ newMode: String?) {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
commandClient.clashMode = newMode! commandClient.clashMode = newMode!
} }
} }
@@ -324,6 +395,7 @@ public class CommandClient: ObservableObject {
return return
} }
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
guard isActiveConnection() else { return }
if commandClient.connectionsStore == nil { if commandClient.connectionsStore == nil {
commandClient.connectionsStore = LibboxNewConnections() commandClient.connectionsStore = LibboxNewConnections()
} }
+1 -1
View File
@@ -495,7 +495,7 @@ public class StatusBarController: NSObject, NSMenuDelegate {
if shouldConnect { if shouldConnect {
if commandClient == nil { if commandClient == nil {
commandClient = CommandClient(.status) commandClient = CommandClient(.status)
commandClient!.$status commandClient!.statusPublisher
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] status in .sink { [weak self] status in
self?.updateSpeedDisplay(status: status) self?.updateSpeedDisplay(status: status)