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>
This commit is contained in:
@@ -30,7 +30,7 @@ final class VpnChannel: NSObject {
|
||||
private var statusSink: FlutterEventSink?
|
||||
private var statsSink: FlutterEventSink?
|
||||
private var statusObserver: NSObjectProtocol?
|
||||
private var statsTimer: Timer?
|
||||
private let statsClient = StatsClient()
|
||||
private var manager: NETunnelProviderManager?
|
||||
private var sysextDelegate: SysExtActivationDelegate?
|
||||
|
||||
@@ -142,13 +142,42 @@ final class VpnChannel: NSObject {
|
||||
|
||||
// ── 状态 / 速率回传 ─────────────────────────────────────────────
|
||||
private func observeStatus() {
|
||||
// StatsClient 拿到实时帧 → 推当前 statsSink。
|
||||
statsClient.onStats = { [weak self] stats in self?.statsSink?(stats) }
|
||||
statusObserver = NotificationCenter.default.addObserver(
|
||||
forName: .NEVPNStatusDidChange, object: nil, queue: .main
|
||||
) { [weak self] note in
|
||||
guard let conn = note.object as? NEVPNConnection else { return }
|
||||
guard let self, let conn = note.object as? NEVPNConnection else { return }
|
||||
let s = Self.statusString(conn.status)
|
||||
vpnLog("NEVPNStatus 变化 → \(s) (raw=\(conn.status.rawValue))")
|
||||
self?.statusSink?(s)
|
||||
self.statusSink?(s)
|
||||
// 实时统计 CommandClient 跟随连接状态启停。
|
||||
switch conn.status {
|
||||
case .connected:
|
||||
self.statsClient.start()
|
||||
case .disconnected, .invalid:
|
||||
self.statsClient.stop()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
// 隧道(sysext)常驻:app 重启时它可能早已 connected,此后不会再发 .connected
|
||||
// 状态变化 → 上面的 observer 不触发、statsClient 永不 start。启动时主动探一次
|
||||
// 现存连接:拿到 manager(让 onStatsListen 兜底也能判定),已连则立刻拉起统计。
|
||||
Task { await self.primeExistingConnection() }
|
||||
}
|
||||
|
||||
/// 启动时若隧道已在连接,主动装配 manager 并拉起实时统计。幂等(StatsClient.start 自带去重)。
|
||||
private func primeExistingConnection() async {
|
||||
guard let all = try? await NETunnelProviderManager.loadAllFromPreferences(),
|
||||
let mgr = all.first else { return }
|
||||
await MainActor.run {
|
||||
self.manager = mgr
|
||||
if mgr.connection.status == .connected {
|
||||
vpnLog("启动时检测到隧道已连接(raw=\(mgr.connection.status.rawValue)),主动启动实时统计")
|
||||
self.statusSink?(Self.statusString(mgr.connection.status))
|
||||
self.statsClient.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,12 +189,11 @@ final class VpnChannel: NSObject {
|
||||
|
||||
fileprivate func onStatsListen(_ sink: @escaping FlutterEventSink) {
|
||||
statsSink = sink
|
||||
// TODO(P1): 接 libbox CommandClient 订阅实时速率;骨架先每秒推占位 0。
|
||||
statsTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
self?.statsSink?(["up": 0, "down": 0, "uplinkTotal": 0, "downlinkTotal": 0])
|
||||
}
|
||||
// 实时速率由 StatsClient(libbox CommandClient)在隧道 connected 时产出并经
|
||||
// onStats 回调推送;此处仅登记 sink。若已连接则补一次拉起。
|
||||
if manager?.connection.status == .connected { statsClient.start() }
|
||||
}
|
||||
fileprivate func onStatsCancel() { statsTimer?.invalidate(); statsTimer = nil; statsSink = nil }
|
||||
fileprivate func onStatsCancel() { statsSink = nil }
|
||||
|
||||
private static func statusString(_ s: NEVPNStatus) -> String {
|
||||
switch s {
|
||||
|
||||
Reference in New Issue
Block a user