Fix duplicate traffic updates
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user