diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index f32fe49..09dfca9 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -380,7 +380,6 @@ final class StatsCollector: NSObject { // clash API(扩展内注入,127.0.0.1 本地监听):取出站组 urltest 真实延迟。 private var clashBase = "" private var clashSecret = "" - private var groupNames: [String] = [] private let lock = NSLock() private var latest: [String: Any] = StatsCollector.zero @@ -424,7 +423,6 @@ final class StatsCollector: NSObject { if let c = self.statusClient { try? c.disconnect() } self.statusClient = nil self.started = false - self.groupNames = [] self.lock.lock() self.latest = StatsCollector.zero self.latestUrltest = [] @@ -447,48 +445,25 @@ final class StatsCollector: NSObject { } private func pollClash() { - if groupNames.isEmpty { - clashGet("/proxies", query: nil) { [weak self] obj, raw in - guard let self else { return } - let names = StatsCollector.extractUrltestGroups(obj) - self.queue.async { - self.groupNames = names - if names.isEmpty { self.setDiag("groups=[] raw=\(raw.prefix(120))") } - else { self.setDiag("groups=\(names)") } - self.triggerGroupDelays() - } - } - } else { - triggerGroupDelays() - } - } - - private func triggerGroupDelays() { - for name in groupNames { - clashGet("/group/\(name)/delay", - query: ["url": "http://www.gstatic.com/generate_204", "timeout": "3000"]) { [weak self] obj, raw in - guard let self else { return } - var urltest: [(tag: String, delayMs: Int)] = [] - if let dict = obj as? [String: Any] { - for (tag, v) in dict { - if let d = (v as? NSNumber)?.intValue { - urltest.append((tag: tag, delayMs: d)) // 扁平 {tag:ms} - } else if let inner = v as? [String: Any], - let d = (inner["delay"] as? NSNumber)?.intValue { - urltest.append((tag: tag, delayMs: d)) // 嵌套 {tag:{delay:ms}} - } - } - } - NSLog("[pangolin/collector] kernel delay(%@) n=%d raw=%@", name, urltest.count, raw) - self.queue.async { + // 主源:读 /proxies 里各 URLTest 成员的 history 缓存延迟(稳定,active 探测超时也不丢)。 + // 同时对 URLTest 组主动跑一次 /group//delay 刷新 history(跳过 GLOBAL 选择器组)。 + clashGet("/proxies", query: nil) { [weak self] obj, _ in + guard let self else { return } + let results = StatsCollector.extractUrltestResults(obj) + let groups = StatsCollector.extractUrltestGroups(obj).filter { $0 != "GLOBAL" } + self.queue.async { + if !results.isEmpty { self.lock.lock() - if !urltest.isEmpty { - self.latestUrltest = urltest - self.latest["urltestResults"] = urltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] } - } - self.latest["diag"] = "d(\(name)) n=\(urltest.count) raw=\(raw.prefix(120))" + self.latestUrltest = results + self.latest["urltestResults"] = results.map { ["tag": $0.tag, "delayMs": $0.delayMs] } self.lock.unlock() } + self.setDiag("hist=\(results.map { "\($0.tag):\($0.delayMs)" }.joined(separator: ","))") + for name in groups { + self.clashGet("/group/\(name)/delay", + query: ["url": "http://www.gstatic.com/generate_204", + "timeout": "5000"]) { _, _ in } + } } } } @@ -529,6 +504,27 @@ final class StatsCollector: NSObject { } return names } + + /// 从 /proxies 提取各 URLTest 组成员的最新 history 延迟(与 Windows extractUrltestResults 同法)。 + static func extractUrltestResults(_ obj: Any?) -> [(tag: String, delayMs: Int)] { + guard let root = obj as? [String: Any], + let proxies = root["proxies"] as? [String: Any] else { return [] } + var results: [(tag: String, delayMs: Int)] = [] + for (_, v) in proxies { + guard let group = v as? [String: Any], + let type = group["type"] as? String, type == "URLTest", + let members = group["all"] as? [Any] else { continue } + for m in members { + guard let tag = m as? String, + let member = proxies[tag] as? [String: Any], + let history = member["history"] as? [Any], + let last = history.last as? [String: Any], + let delay = (last["delay"] as? NSNumber)?.intValue, delay > 0 else { continue } + results.append((tag: tag, delayMs: delay)) + } + } + return results + } } extension StatsCollector: LibboxCommandClientHandlerProtocol { diff --git a/client/macos/Runner.xcodeproj/project.pbxproj b/client/macos/Runner.xcodeproj/project.pbxproj index 1315f69..b084235 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 = 48; + CURRENT_PROJECT_VERSION = 49; 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 = 48; + CURRENT_PROJECT_VERSION = 49; 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 = 48; + CURRENT_PROJECT_VERSION = 49; 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 = 48; + CURRENT_PROJECT_VERSION = 49; 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 = 48; + CURRENT_PROJECT_VERSION = 49; 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 = 48; + CURRENT_PROJECT_VERSION = 49; DEVELOPMENT_TEAM = BYL4KQHMTN; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES;