chore(client/macos): 清理延迟排查的诊断打点 + debug 注入(最终干净版)
延迟显示已修通。移除排查期的临时诊断:扩展侧 Log 客户端/klog 捕获、log.level=debug 注入、 diag 字段/setDiag;主 app StatsClient 每5帧的 dl/ul/urltest 打印;Dart 侧 [Stats]/[Ping] 打点。 保留全部修复:clash_api 注入取 urltest、sendProviderMessage 统计通道、/proxies history+active 缓存、共享广播流、_onStats 回退 effectiveNode、连接态跳过直连实测。 CURRENT_PROJECT_VERSION 52→53。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -130,11 +130,6 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
experimental["cache_file"] = ["enabled": true]
|
||||
}
|
||||
root["experimental"] = experimental
|
||||
// 诊断:开 debug 日志,让 urltest 探测的失败原因(DNS/连接/超时)出现在内核日志里,
|
||||
// 由 StatsCollector 的 Log 客户端抓取透出。
|
||||
var logCfg = (root["log"] as? [String: Any]) ?? [:]
|
||||
logCfg["level"] = "debug"
|
||||
root["log"] = logCfg
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: root),
|
||||
let out = String(data: data, encoding: .utf8) else { return configJSON }
|
||||
return out
|
||||
@@ -379,7 +374,6 @@ private func simpleError(_ msg: String) -> NSError {
|
||||
final class StatsCollector: NSObject {
|
||||
private let queue = DispatchQueue(label: "pangolin.stats.collector")
|
||||
private var statusClient: LibboxCommandClient?
|
||||
private var logClient: LibboxCommandClient?
|
||||
private var started = false
|
||||
private var clashTimer: DispatchSourceTimer?
|
||||
|
||||
@@ -403,7 +397,6 @@ final class StatsCollector: NSObject {
|
||||
self.clashBase = "http://127.0.0.1:\(clashPort)"
|
||||
self.clashSecret = clashSecret
|
||||
self.statusClient = self.connectCommand(LibboxCommandStatus, "status")
|
||||
self.logClient = self.connectCommand(LibboxCommandLog, "log") // 抓内核 urltest 报错
|
||||
self.started = true
|
||||
self.startClashTimer()
|
||||
}
|
||||
@@ -428,9 +421,7 @@ final class StatsCollector: NSObject {
|
||||
guard let self else { return }
|
||||
self.clashTimer?.cancel(); self.clashTimer = nil
|
||||
if let c = self.statusClient { try? c.disconnect() }
|
||||
if let c = self.logClient { try? c.disconnect() }
|
||||
self.statusClient = nil
|
||||
self.logClient = nil
|
||||
self.started = false
|
||||
self.lock.lock()
|
||||
self.latest = StatsCollector.zero
|
||||
@@ -458,26 +449,19 @@ final class StatsCollector: NSObject {
|
||||
// · /group/<name>/delay 只为「让内核重测并写进 history」,其结果也顺手缓存(双保险);
|
||||
// 单次探测失败不影响显示——下一拍仍读到 history 里上次成功的值。
|
||||
private func pollClash() {
|
||||
clashGet("/proxies", query: nil) { [weak self] obj, raw in
|
||||
clashGet("/proxies") { [weak self] obj in
|
||||
guard let self else { return }
|
||||
let hist = StatsCollector.extractUrltestResults(obj)
|
||||
let groups = StatsCollector.extractUrltestGroups(obj).filter { $0 != "GLOBAL" }
|
||||
self.queue.async {
|
||||
if !hist.isEmpty {
|
||||
self.cacheUrltest(hist, src: "hist")
|
||||
} else {
|
||||
self.setDiag("hist= groups=\(groups) raw=\(raw.prefix(70))")
|
||||
}
|
||||
self.cacheUrltest(hist)
|
||||
for name in groups {
|
||||
self.clashGet("/group/\(name)/delay",
|
||||
query: ["url": "http://www.gstatic.com/generate_204",
|
||||
"timeout": "5000"]) { [weak self] dobj, draw in
|
||||
"timeout": "5000"]) { [weak self] dobj in
|
||||
guard let self else { return }
|
||||
let active = StatsCollector.parseDelayMap(dobj)
|
||||
self.queue.async {
|
||||
if !active.isEmpty { self.cacheUrltest(active, src: "active") }
|
||||
else { self.setDiag("active(\(name)) empty raw=\(draw.prefix(70))") }
|
||||
}
|
||||
self.queue.async { self.cacheUrltest(active) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,35 +469,27 @@ final class StatsCollector: NSObject {
|
||||
}
|
||||
|
||||
/// 缓存延迟(从 history 或 active 任一来源),非空才更新——失败/空不覆盖旧值。
|
||||
private func cacheUrltest(_ list: [(tag: String, delayMs: Int)], src: String) {
|
||||
private func cacheUrltest(_ list: [(tag: String, delayMs: Int)]) {
|
||||
guard !list.isEmpty else { return }
|
||||
lock.lock()
|
||||
latestUrltest = list
|
||||
latest["urltestResults"] = list.map { ["tag": $0.tag, "delayMs": $0.delayMs] }
|
||||
latest["diag"] = "\(src)=\(list.map { "\($0.tag):\($0.delayMs)" }.joined(separator: ","))"
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
private func setDiag(_ s: String) {
|
||||
lock.lock(); latest["diag"] = s; lock.unlock()
|
||||
}
|
||||
|
||||
private func clashGet(_ path: String, query: [String: String]?,
|
||||
completion: @escaping (Any?, String) -> Void) {
|
||||
guard var comps = URLComponents(string: clashBase + path) else { completion(nil, "badurl"); return }
|
||||
private func clashGet(_ path: String, query: [String: String]? = nil,
|
||||
completion: @escaping (Any?) -> Void) {
|
||||
guard var comps = URLComponents(string: clashBase + path) else { completion(nil); return }
|
||||
if let query { comps.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) } }
|
||||
guard let url = comps.url else { completion(nil, "badurl"); return }
|
||||
guard let url = comps.url else { completion(nil); return }
|
||||
var req = URLRequest(url: url)
|
||||
req.timeoutInterval = 8
|
||||
if !clashSecret.isEmpty {
|
||||
req.setValue("Bearer \(clashSecret)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
URLSession.shared.dataTask(with: req) { data, resp, err in
|
||||
if let err { completion(nil, "err:\(err.localizedDescription)"); return }
|
||||
let code = (resp as? HTTPURLResponse)?.statusCode ?? -1
|
||||
guard let data else { completion(nil, "http\(code):nodata"); return }
|
||||
let raw = String(data: data, encoding: .utf8) ?? "<\(data.count)B>"
|
||||
completion(try? JSONSerialization.jsonObject(with: data), "http\(code):\(raw)")
|
||||
URLSession.shared.dataTask(with: req) { data, _, _ in
|
||||
guard let data else { completion(nil); return }
|
||||
completion(try? JSONSerialization.jsonObject(with: data))
|
||||
}.resume()
|
||||
}
|
||||
|
||||
@@ -573,19 +549,7 @@ extension StatsCollector: LibboxCommandClientHandlerProtocol {
|
||||
func connected() {}
|
||||
func disconnected(_ message: String?) {}
|
||||
func clearLogs() {}
|
||||
func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) {
|
||||
guard let it = messageList else { return }
|
||||
while it.hasNext() {
|
||||
guard let entry = it.next() else { break }
|
||||
let msg = entry.message
|
||||
let low = msg.lowercased()
|
||||
// 只抓 urltest 探测相关行(含失败原因:DNS/连接/超时)。
|
||||
guard low.contains("urltest") || low.contains("url-test")
|
||||
|| low.contains("gstatic") || low.contains("generate_204") else { continue }
|
||||
NSLog("[pangolin/klog] %@", msg)
|
||||
lock.lock(); latest["klog"] = String(msg.prefix(180)); lock.unlock()
|
||||
}
|
||||
}
|
||||
func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) {}
|
||||
func setDefaultLogLevel(_ level: Int32) {}
|
||||
func initializeClashMode(_ modeList: (any LibboxStringIteratorProtocol)?, currentMode: String?) {}
|
||||
func updateClashMode(_ newMode: String?) {}
|
||||
@@ -593,7 +557,7 @@ extension StatsCollector: LibboxCommandClientHandlerProtocol {
|
||||
|
||||
func writeStatus(_ message: LibboxStatusMessage?) {
|
||||
guard let m = message else { return }
|
||||
// 逐键更新(不整盘重建),保留 clash 轮询写入的 urltestResults / diag。
|
||||
// 逐键更新(不整盘重建),保留 clash 轮询写入的 urltestResults。
|
||||
lock.lock()
|
||||
latest["uploadBytes"] = m.uplinkTotal
|
||||
latest["downloadBytes"] = m.downlinkTotal
|
||||
|
||||
@@ -659,7 +659,7 @@
|
||||
baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
||||
@@ -674,7 +674,7 @@
|
||||
baseConfigurationReference = F8904897A48DC81799B8752E /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
||||
@@ -689,7 +689,7 @@
|
||||
baseConfigurationReference = B21E68FC1F5D33DD67A0DF5E /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
||||
@@ -960,7 +960,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -1010,7 +1010,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -1059,7 +1059,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
||||
@@ -20,7 +20,6 @@ final class StatsClient: NSObject {
|
||||
private let queue = DispatchQueue(label: "pangolin.stats.client")
|
||||
private weak var session: NETunnelProviderSession?
|
||||
private var timer: DispatchSourceTimer?
|
||||
private var diagTick = 0 // 诊断:每 5 帧打一次 urltest,排查延迟为何不刷新
|
||||
|
||||
// ── 生命周期 ────────────────────────────────────────────────
|
||||
|
||||
@@ -59,18 +58,8 @@ final class StatsClient: NSObject {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user