From 7add0259b45aa12b784149159b3a91540b9f2d8a Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 30 Jun 2026 18:03:30 +0800 Subject: [PATCH] =?UTF-8?q?chore(client/macos):=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E6=8E=92=E6=9F=A5=E7=9A=84=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E6=89=93=E7=82=B9=20+=20debug=20=E6=B3=A8=E5=85=A5(=E6=9C=80?= =?UTF-8?q?=E7=BB=88=E5=B9=B2=E5=87=80=E7=89=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 延迟显示已修通。移除排查期的临时诊断:扩展侧 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 --- client/lib/state/connection_provider.dart | 11 +--- client/lib/state/nodes_provider.dart | 15 +---- .../PacketTunnel/PacketTunnelProvider.swift | 64 ++++--------------- client/macos/Runner.xcodeproj/project.pbxproj | 12 ++-- client/macos/Runner/StatsClient.swift | 11 ---- 5 files changed, 23 insertions(+), 90 deletions(-) diff --git a/client/lib/state/connection_provider.dart b/client/lib/state/connection_provider.dart index 688b631..d8f2775 100644 --- a/client/lib/state/connection_provider.dart +++ b/client/lib/state/connection_provider.dart @@ -81,7 +81,6 @@ class ConnectionController extends StateNotifier { _statusSub = _bridge.statusStream.listen(_onKernelStatus); // 订阅内核 stats:连接态把 urltest 实测回写连接节点的 ping,让连接页与节点列表同源一致。 _statsSub = _bridge.statsStream.listen(_onStats); - logLine('Stats', 'ConnectionController constructed, statsStream subscribed'); // 登出(主动退出 / 被强制下线 / 会话过期)→ 一律断开隧道:别让已登出/被踢的设备继续用 // 数据面。一处监听 auth 覆盖所有登出路径。token 续期(仍登录)不触发。 _authSub = _ref.listen(authProvider, (prev, next) { @@ -245,24 +244,16 @@ class ConnectionController extends StateNotifier { // 连接态:取内核 urltest 最小正延迟,回写连接节点 ping。全局 TUN 下 App 直接探测节点不可行, // 内核(经 REALITY 真实直连数据面)是唯一能实测的;回写到 node.ping → 连接页/节点列表同源。 void _onStats(VpnStatsEvent e) { - // 无条件打点:确认 _onStats 是否被调用、当前 phase、urltest 是否到达 Dart 侧。 - logLine('Stats', - 'onStats phase=${state.phase.name} ' - 'urltest=[${e.urltestResults.map((r) => "${r.tag}:${r.delayMs}").join(",")}]'); if (state.phase != VpnPhase.on) return; // 自动连接/重连到已在跑的隧道时不会走 _connect → _connectedNode 为 null;回退到 // effectiveNode(当前/智能所选),否则 urltest 实测无处回写、连接页延迟一直显示 —。 final Node node = _connectedNode ?? _ref.read(effectiveNodeProvider); - final ds = e.urltestResults.where((r) => r.delayMs > 0).map((r) => r.delayMs).toList(); - logLine('Stats', - 'conn=${_connectedNode == null ? "null" : _connectedNode!.code} ' - 'eff=${node.code}/${node.uuid.isEmpty ? "EMPTY" : "ok"} ds=$ds'); if (node.uuid.isEmpty) return; + final ds = e.urltestResults.where((r) => r.delayMs > 0).map((r) => r.delayMs); if (ds.isEmpty) return; // urltest 成功:经 proxy 出站(REALITY 到节点)真实可达 → 记录时刻(供路径 A 判活)+ 回写延迟。 _lastUrltestOk = DateTime.now(); final best = ds.reduce((a, b) => a < b ? a : b); - logLine('Stats', 'setLivePing node=${node.code} = ${best}ms'); _ref.read(nodesProvider.notifier).setLivePing(node.uuid, best); } diff --git a/client/lib/state/nodes_provider.dart b/client/lib/state/nodes_provider.dart index 873b197..9793a41 100644 --- a/client/lib/state/nodes_provider.dart +++ b/client/lib/state/nodes_provider.dart @@ -14,7 +14,6 @@ import '../services/api_config.dart'; import '../services/latency_probe.dart'; import 'auth_provider.dart'; import 'connection_provider.dart'; -import '../bridge/log.dart'; // ── 节点列表 AsyncNotifier ──────────────────────────────────────── @@ -87,24 +86,14 @@ class NodesNotifier extends AsyncNotifier> { /// 回写某节点的实测延迟(ms)。连接态由 ConnectionController 用内核 urltest 回填连接节点, /// 让「连接页延迟」与「节点列表延迟」同源(都读 node.ping)、显示一致。ms<=0 忽略。 void setLivePing(String uuid, int ms) { - if (ms <= 0 || _disposed) { - logLine('Ping', 'setLivePing SKIP ms=$ms disposed=$_disposed'); - return; - } + if (ms <= 0 || _disposed) return; final list = state.valueOrNull; - if (list == null) { - logLine('Ping', 'setLivePing no-list'); - return; - } + if (list == null) return; var changed = false; final updated = [ for (final n in list) if (n.uuid == uuid && n.ping != ms) (() { changed = true; return n.copyWith(ping: ms); })() else n ]; - final match = list.any((n) => n.uuid == uuid); - logLine('Ping', - 'setLivePing ms=$ms match=$match changed=$changed ' - 'nodes=[${list.map((n) => "${n.code}:${n.ping}").join(",")}]'); if (changed) state = AsyncData(updated); } diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index f608c8a..4ffb4c4 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -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//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 diff --git a/client/macos/Runner.xcodeproj/project.pbxproj b/client/macos/Runner.xcodeproj/project.pbxproj index b3a255d..720fb5c 100644 --- a/client/macos/Runner.xcodeproj/project.pbxproj +++ b/client/macos/Runner.xcodeproj/project.pbxproj @@ -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; diff --git a/client/macos/Runner/StatsClient.swift b/client/macos/Runner/StatsClient.swift index 2a91213..7ec5376 100644 --- a/client/macos/Runner/StatsClient.swift +++ b/client/macos/Runner/StatsClient.swift @@ -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 {