Files
pangolin/client/ios/Runner/StatsClient.swift
wangjia ae5d497f0b feat(client/ios): 实时统计+urltest 延迟与 macOS 对齐 + 修 pbxproj UUID 冲突
把 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>
2026-06-30 19:12:28 +08:00

62 lines
2.7 KiB
Swift
Raw Permalink 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 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)
}
}
}