Files
pangolin/client/macos/Runner/StatsClient.swift
T
wangjia e3fb5239a6 diag(client/macos): 抓 sing-box urltest 内核日志,定位探测为何失败
确认 urltest 为空是内核探测 100% 失败(传输/读取链路均正常)。本次注入 log.level=debug,
StatsCollector 新增 Log 命令客户端,抓取内核日志里 urltest/gstatic 相关行(失败原因:
DNS/连接/超时),塞进 stats 的 klog 字段经主 app 可读日志透出 —— 直接看到内核报错。

CURRENT_PROJECT_VERSION 51→52。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:58:07 +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=%@ klog=%@",
"\(obj["downloadSpeed"] ?? 0)", "\(obj["uploadSpeed"] ?? 0)", urls.count, pairs,
"\(obj["diag"] ?? "-")", "\(obj["klog"] ?? "-")")
}
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) }
}
}