Files
pangolin/client/macos/Runner/StatsClient.swift
T
wangjia 03dbbf8db9 fix(client/macos): urltest 延迟改走注入的 clash API(对齐 Windows),内核侧打点
诊断:即便专开 Group client,扩展回的 urltest 数组恒空——根因是服务端下发配置无
experimental.clash_api,sing-box 不暴露出站组/urltest 历史,libbox Group 命令拿不到。

修复(与 Windows 完全同法):扩展内向配置注入 clash_api(127.0.0.1 本地监听)+ cache_file,
StatsCollector 经本地 HTTP 查 /proxies 找 URLTest 组、/group/<name>/delay 取真实延迟。
速率仍走 libbox Status。新增内核侧 NSLog 打点 kernel delay(<group>)=<tag:ms>,并把该值
塞进 stats 的 diag 字段经主 app 可读日志透出——一眼分清「内核有没有算出延迟」vs「外面没拿到」。

扩展代码变更 → CURRENT_PROJECT_VERSION 46→47。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:54:40 +08:00

85 lines
3.8 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 macOS
//
// PacketTunnel System Extension root app App Group
// root /var/root/... vs /Users/<user>/... app
// libbox command.sock NetworkExtension
// app NETunnelProviderSession.sendProviderMessage("stats")
// handleAppMessage JSON StatsCollector
//
// root/ socket pangolin/vpn/stats
// lib/bridge/vpn_bridge.dart onStats VpnChannel Dart
import Foundation
import NetworkExtension
final class StatsClient: NSObject {
/// VpnChannel statsSink
var onStats: (([String: Any]) -> Void)?
private let queue = DispatchQueue(label: "pangolin.stats.client")
private weak var session: NETunnelProviderSession?
private var timer: DispatchSourceTimer?
private var diagTick = 0 // : 5 urltest,
//
/// connected
func start(session: NETunnelProviderSession) {
queue.async { [weak self] in
guard let self else { return }
self.session = session
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("[pangolin/stats] polling via sendProviderMessage started")
}
}
func stop() {
queue.async { [weak self] in
guard let self else { return }
self.timer?.cancel()
self.timer = nil
self.session = nil
self.emit([
"uploadBytes": 0, "downloadBytes": 0,
"uploadSpeed": 0.0, "downloadSpeed": 0.0,
"urltestResults": [[String: Any]](),
])
}
}
private func poll() {
guard let session else { return }
do {
try session.sendProviderMessage(Data("stats".utf8)) { [weak self] resp in
guard let self, let resp, !resp.isEmpty else { return }
guard let obj = (try? JSONSerialization.jsonObject(with: resp)) as? [String: Any] else {
NSLog("[pangolin/stats] bad stats payload (%d bytes)", resp.count)
return
}
// : urltest ()
self.diagTick += 1
if self.diagTick % 5 == 0 {
let urls = (obj["urltestResults"] as? [[String: Any]]) ?? []
let pairs = urls.map { "\($0["tag"] ?? "?")=\($0["delayMs"] ?? -1)" }.joined(separator: ",")
NSLog("[pangolin/stats] dl=%@ ul=%@ urltest[%d]=%@ diag=%@",
"\(obj["downloadSpeed"] ?? 0)", "\(obj["uploadSpeed"] ?? 0)", urls.count, pairs,
"\(obj["diag"] ?? "-")")
}
self.emit(obj)
}
} catch {
NSLog("[pangolin/stats] sendProviderMessage failed: %@", error.localizedDescription)
}
}
private func emit(_ stats: [String: Any]) {
DispatchQueue.main.async { [weak self] in self?.onStats?(stats) }
}
}