Files
pangolin/client/ios/Runner/StatsClient.swift
T
wangjia bf84492eef feat(client/native): stats-overhaul Phase1 — 四端实时统计采集端
各原生侧把 libbox/Clash 的实时上下行/延迟经冻结的 pangolin/vpn/stats channel
生产真实帧:
- iOS/macOS 新增 StatsClient.swift(LibboxNewCommandClient,statusInterval 1s,
  uplink/downlink/总量 + urltest 延迟映射契约字段),VpnManager/VpnChannel 状态
  观察驱动 start/stop,pbxproj 登记。
- Android PangolinVpnService.kt 延迟修复。

契约字段 uploadBytes/downloadBytes/uploadSpeed/downloadSpeed/urltestResults 不改。
真机验证(连接后实时跳动)为运行时步骤,见计划 Phase1 待办。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:13:48 +08:00

179 lines
7.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// StatsClient.swift App
//
// iOS PacketTunnel LibboxSetup App Group
// LibboxCommandServer/ + App LibboxCommandClient
// group CommandServer socket status/group
// sing-box-for-apple(SFI) Android StatsHandler
//
// CommandServer statusInterval client 广 box client
// writeStatus/writeGroups pangolin/vpn/stats
// ( lib/bridge/vpn_bridge.dart) VpnStatsStreamHandler Dart
//
// VPN connected start()/ stop() VpnManager
import Foundation
import Libbox
/// LibboxCommandClient stats EventChannel
final class StatsClient: NSObject {
static let shared = StatsClient()
// iOS App Group PacketTunnelProvider setup
private let appGroup = "group.com.pangolin.pangolinVpn"
private let queue = DispatchQueue(label: "pangolin.stats.client")
private var client: LibboxCommandClient?
private var started = false
// libbox setup
private static var didSetup = false
/// writeGroups urltest (tag delayMs)
/// writeStatus ( Androidstatus group )
private var latestUrltest: [(tag: String, delayMs: Int)] = []
/// writeGroups tag urltest "auto"
private var groupTags: [String] = []
/// urltestsing-box urltest ( interval history )
/// 12s
private var urlTestTimer: DispatchSourceTimer?
//
/// CommandClient
func start() {
queue.async { [weak self] in
guard let self, !self.started else { return }
guard self.ensureSetup() else { return }
let options = LibboxCommandClientOptions()
options.statusInterval = 1_000_000_000 // 1s
options.addCommand(LibboxCommandStatus) // / +
options.addCommand(LibboxCommandGroup) // urltest
guard let c = LibboxNewCommandClient(self, options) else {
NSLog("StatsClient: LibboxNewCommandClient returned nil")
return
}
// CommandServer ready
for attempt in 0..<10 {
do {
try c.connect()
self.client = c
self.started = true
NSLog("StatsClient: connected (attempt %d)", attempt + 1)
self.startUrlTestTimer()
return
} catch {
NSLog("StatsClient: connect failed (attempt %d): %@", attempt + 1,
error.localizedDescription)
Thread.sleep(forTimeInterval: 0.5)
}
}
NSLog("StatsClient: gave up connecting after retries")
}
}
/// CommandClient
/// urltest queue
private func startUrlTestTimer() {
urlTestTimer?.cancel()
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + 2, repeating: 12)
timer.setEventHandler { [weak self] in
guard let self, let c = self.client else { return }
for tag in self.groupTags {
try? c.urlTest(tag)
}
}
timer.resume()
urlTestTimer = timer
}
func stop() {
queue.async { [weak self] in
guard let self else { return }
self.urlTestTimer?.cancel()
self.urlTestTimer = nil
if let c = self.client {
try? c.disconnect()
}
self.client = nil
self.started = false
self.latestUrltest = []
self.groupTags = []
// UI
VpnStatsStreamHandler.shared.push([
"uploadBytes": 0, "downloadBytes": 0,
"uploadSpeed": 0.0, "downloadSpeed": 0.0,
"urltestResults": [[String: Any]](),
])
}
}
// libbox setup group
private func ensureSetup() -> Bool {
if Self.didSetup { return true }
guard let base = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
NSLog("StatsClient: no app group container")
return false
}
let work = base.appendingPathComponent("work", isDirectory: true)
let setup = LibboxSetupOptions()
setup.basePath = base.path
setup.workingPath = work.path
setup.tempPath = NSTemporaryDirectory()
var err: NSError?
LibboxSetup(setup, &err)
if let err {
NSLog("StatsClient: LibboxSetup failed: %@", err.localizedDescription)
return false
}
Self.didSetup = true
return true
}
}
// LibboxCommandClientHandler
extension StatsClient: LibboxCommandClientHandlerProtocol {
func connected() { NSLog("StatsClient: server connected") }
func disconnected(_ message: String?) { NSLog("StatsClient: server disconnected: %@", message ?? "") }
func clearLogs() {}
func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) {}
func setDefaultLogLevel(_ level: Int32) {}
func initializeClashMode(_ modeList: (any LibboxStringIteratorProtocol)?, currentMode: String?) {}
func updateClashMode(_ newMode: String?) {}
func write(_ events: LibboxConnectionEvents?) {}
func writeStatus(_ message: LibboxStatusMessage?) {
guard let m = message else { return }
let stats: [String: Any] = [
"uploadBytes": m.uplinkTotal,
"downloadBytes": m.downlinkTotal,
"uploadSpeed": Double(m.uplink),
"downloadSpeed": Double(m.downlink),
"urltestResults": latestUrltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] },
]
VpnStatsStreamHandler.shared.push(stats)
}
func writeGroups(_ message: (any LibboxOutboundGroupIteratorProtocol)?) {
guard let groups = message else { return }
var urltest: [(tag: String, delayMs: Int)] = []
var tags: [String] = []
while groups.hasNext() {
guard let group = groups.next() else { break }
tags.append(group.tag)
guard let items = group.getItems() else { continue }
while items.hasNext() {
guard let item = items.next() else { break }
urltest.append((tag: item.tag, delayMs: Int(item.urlTestDelay)))
}
}
latestUrltest = urltest
queue.async { [weak self] in self?.groupTags = tags }
}
}