Files
sing-box-for-apple/ApplicationLibrary/Views/Tools/TailscalePingViewModel.swift
T
2026-04-23 08:12:53 +08:00

90 lines
2.9 KiB
Swift

import Foundation
import Libbox
import Library
import SwiftUI
@MainActor
public final class TailscalePingViewModel: BaseViewModel {
@Published public var isRunning = false
@Published public var latencyMs: Double = 0
@Published public var isDirect: Bool = false
@Published public var derpRegionCode: String = ""
@Published public var endpoint: String = ""
@Published public var hasResult = false
@Published public var latencyHistory: [CGFloat] = []
private let maxHistorySize = 30
private var commandClient: LibboxCommandClient?
private var runningTask: Task<Void, Never>?
public func start(endpointTag: String, peerIP: String) {
latencyHistory = []
hasResult = false
isRunning = true
let client = LibboxNewStandaloneCommandClient()!
commandClient = client
let handler = PingHandler(self)
runningTask = Task { [weak self] in
await Task.detached {
try? client.startTailscalePing(endpointTag, peerIP: peerIP, handler: handler)
}.value
self?.runningTask = nil
}
}
public func stop() {
runningTask?.cancel()
runningTask = nil
try? commandClient?.disconnect()
commandClient = nil
isRunning = false
}
fileprivate func appendLatency(_ ms: Double) {
latencyHistory.append(CGFloat(ms))
if latencyHistory.count > maxHistorySize {
latencyHistory.removeFirst()
}
}
private final class PingHandler: NSObject, LibboxTailscalePingHandlerProtocol, @unchecked Sendable {
private weak var viewModel: TailscalePingViewModel?
init(_ viewModel: TailscalePingViewModel?) {
self.viewModel = viewModel
}
func onPingResult(_ result: LibboxTailscalePingResult?) {
guard let result else { return }
let latencyMs = result.latencyMs
let isDirect = result.isDirect
let derpRegionCode = result.derpRegionCode
let endpoint = result.endpoint
let error = result.error
DispatchQueue.main.async { [self] in
guard let viewModel, viewModel.isRunning else { return }
if !error.isEmpty {
return
}
viewModel.latencyMs = latencyMs
viewModel.isDirect = isDirect
viewModel.derpRegionCode = derpRegionCode
viewModel.endpoint = endpoint
viewModel.hasResult = true
viewModel.appendLatency(latencyMs)
}
}
func onError(_: String?) {
DispatchQueue.main.async { [self] in
guard let viewModel, viewModel.isRunning else { return }
viewModel.isRunning = false
viewModel.commandClient = nil
viewModel.runningTask = nil
}
}
}
}