ae5d497f0b
把 macOS 已验证的统计架构移植到 iOS,补齐连接页延迟(此前 iOS 同 macOS 根因:扩展没注入
clash_api + app 单 client Status+Group → urltest 恒空):
- 扩展 PacketTunnelProvider:加 injectClashApi(clash_api+cache_file)+ 进程内 StatsCollector
(libbox 取速率 + clash /proxies、/group/delay 取 urltest);handleAppMessage 增 action="stats"。
- app StatsClient:改为每秒 sendProviderMessage("stats") 拉取 → VpnStatsStreamHandler 推送,
去掉直连 socket 的单 client Status+Group 坏写法。
修两处编译阻断(框架补齐后才暴露,此前 iOS 从未编译到这步):
- pbxproj 致命 bug:StatsClient.swift 与 PacketTunnelProvider.swift 复用同一 UUID
(...0002...AA/AB),Xcode 无法区分 → 文件错配 target、全模块 cannot-find 级联。
PacketTunnelProvider.swift 改用独立 ...0007... ID。
- tunFd 的 CTLIOCGINFO 单行 | 链拆成子表达式,避免 Swift 类型检查器超时。
前置:scripts/build-libbox.sh apple ios 生成 client/ios/Frameworks/Libbox.xcframework(gitignore)。
Dart 侧无需改(iOS 同走 VpnNativeBridge,本轮 macOS 的广播流/_onStats 回退/连接态跳直连实测已共享)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62 lines
2.7 KiB
Swift
62 lines
2.7 KiB
Swift
// StatsClient.swift — 主 App 侧实时统计消费者(iOS)
|
||
//
|
||
// 隧道跑在 PacketTunnel 扩展进程里;扩展用 StatsCollector 采集上/下行(libbox command.sock)
|
||
// + urltest 延迟(注入的 clash API)并缓存最新 JSON。主 App 经
|
||
// NETunnelProviderSession.sendProviderMessage(action="stats")每秒拉一次,解析后推到
|
||
// pangolin/vpn/stats EventChannel——与 macOS 同架构(扩展产出、app 经 NE 跨进程拉取)。
|
||
//
|
||
// 之前的做法(主 App 用 LibboxCommandClient 直连共享容器 socket)有两个坑:① 单 client 同时
|
||
// 订 Status+Group → Group 永不回调(libbox 一连接一命令);② 配置无 clash_api → 内核不暴露
|
||
// urltest。改走扩展 StatsCollector + sendProviderMessage 后两坑同解,与 macOS 一致。
|
||
//
|
||
// 生命周期:VPN 进入 connected → start();断开/失效 → stop()。由 VpnManager 驱动。
|
||
|
||
import Foundation
|
||
|
||
final class StatsClient: NSObject {
|
||
|
||
static let shared = StatsClient()
|
||
|
||
private let queue = DispatchQueue(label: "pangolin.stats.client")
|
||
private var timer: DispatchSourceTimer?
|
||
|
||
// ── 生命周期 ────────────────────────────────────────────────
|
||
|
||
/// 隧道已连接后调用:每秒向扩展拉一次统计。幂等。
|
||
func start() {
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
self.timer?.cancel()
|
||
let t = DispatchSource.makeTimerSource(queue: self.queue)
|
||
t.schedule(deadline: .now() + 0.2, repeating: 1.0)
|
||
t.setEventHandler { [weak self] in self?.poll() }
|
||
t.resume()
|
||
self.timer = t
|
||
NSLog("StatsClient: polling via sendProviderMessage started")
|
||
}
|
||
}
|
||
|
||
/// 隧道断开后调用:停定时器并推零帧,避免 UI 残留上一次连接的速率。幂等。
|
||
func stop() {
|
||
queue.async { [weak self] in
|
||
guard let self else { return }
|
||
self.timer?.cancel()
|
||
self.timer = nil
|
||
VpnStatsStreamHandler.shared.push([
|
||
"uploadBytes": 0, "downloadBytes": 0,
|
||
"uploadSpeed": 0.0, "downloadSpeed": 0.0,
|
||
"urltestResults": [[String: Any]](),
|
||
])
|
||
}
|
||
}
|
||
|
||
private func poll() {
|
||
let req = (try? JSONSerialization.data(withJSONObject: ["action": "stats"])) ?? Data()
|
||
VpnManager.shared.sendProviderMessage(req) { resp, _ in
|
||
guard let resp, !resp.isEmpty,
|
||
let obj = (try? JSONSerialization.jsonObject(with: resp)) as? [String: Any] else { return }
|
||
VpnStatsStreamHandler.shared.push(obj)
|
||
}
|
||
}
|
||
}
|