diag(client/macos): 把 /group/delay 原始响应塞进 diag,定位 urltest 为空
v47 诊断:clash 注入生效(组名 auto 已识别),但 /group/auto/delay 返回无数字延迟。
本次把该接口原始响应体(含 HTTP 状态码)透出到 diag,并兼容嵌套 {tag:{delay:ms}} 形态,
一眼区分:内核探测失败(raw={} / 错误消息)还是返回结构不符(解析问题)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -448,12 +448,13 @@ final class StatsCollector: NSObject {
|
|||||||
|
|
||||||
private func pollClash() {
|
private func pollClash() {
|
||||||
if groupNames.isEmpty {
|
if groupNames.isEmpty {
|
||||||
clashGet("/proxies", query: nil) { [weak self] obj in
|
clashGet("/proxies", query: nil) { [weak self] obj, raw in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
let names = StatsCollector.extractUrltestGroups(obj)
|
let names = StatsCollector.extractUrltestGroups(obj)
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
self.groupNames = names
|
self.groupNames = names
|
||||||
self.setDiag("groups=\(names)")
|
if names.isEmpty { self.setDiag("groups=[] raw=\(raw.prefix(120))") }
|
||||||
|
else { self.setDiag("groups=\(names)") }
|
||||||
self.triggerGroupDelays()
|
self.triggerGroupDelays()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -465,19 +466,27 @@ final class StatsCollector: NSObject {
|
|||||||
private func triggerGroupDelays() {
|
private func triggerGroupDelays() {
|
||||||
for name in groupNames {
|
for name in groupNames {
|
||||||
clashGet("/group/\(name)/delay",
|
clashGet("/group/\(name)/delay",
|
||||||
query: ["url": "http://www.gstatic.com/generate_204", "timeout": "3000"]) { [weak self] obj in
|
query: ["url": "http://www.gstatic.com/generate_204", "timeout": "3000"]) { [weak self] obj, raw in
|
||||||
guard let self, let dict = obj as? [String: Any] else { return }
|
guard let self else { return }
|
||||||
var urltest: [(tag: String, delayMs: Int)] = []
|
var urltest: [(tag: String, delayMs: Int)] = []
|
||||||
for (tag, v) in dict {
|
if let dict = obj as? [String: Any] {
|
||||||
if let d = (v as? NSNumber)?.intValue { urltest.append((tag: tag, delayMs: d)) }
|
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}}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let summary = urltest.map { "\($0.tag):\($0.delayMs)" }.joined(separator: ",")
|
NSLog("[pangolin/collector] kernel delay(%@) n=%d raw=%@", name, urltest.count, raw)
|
||||||
NSLog("[pangolin/collector] kernel delay(%@)=%@", name, summary) // 内核侧实测打点
|
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
self.lock.lock()
|
self.lock.lock()
|
||||||
self.latestUrltest = urltest
|
if !urltest.isEmpty {
|
||||||
self.latest["urltestResults"] = urltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] }
|
self.latestUrltest = urltest
|
||||||
self.latest["diag"] = "delay(\(name))=\(summary)"
|
self.latest["urltestResults"] = urltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] }
|
||||||
|
}
|
||||||
|
self.latest["diag"] = "d(\(name)) n=\(urltest.count) raw=\(raw.prefix(120))"
|
||||||
self.lock.unlock()
|
self.lock.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,19 +498,21 @@ final class StatsCollector: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func clashGet(_ path: String, query: [String: String]?,
|
private func clashGet(_ path: String, query: [String: String]?,
|
||||||
completion: @escaping (Any?) -> Void) {
|
completion: @escaping (Any?, String) -> Void) {
|
||||||
guard var comps = URLComponents(string: clashBase + path) else { completion(nil); return }
|
guard var comps = URLComponents(string: clashBase + path) else { completion(nil, "badurl"); return }
|
||||||
if let query { comps.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) } }
|
if let query { comps.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) } }
|
||||||
guard let url = comps.url else { completion(nil); return }
|
guard let url = comps.url else { completion(nil, "badurl"); return }
|
||||||
var req = URLRequest(url: url)
|
var req = URLRequest(url: url)
|
||||||
req.timeoutInterval = 8
|
req.timeoutInterval = 8
|
||||||
if !clashSecret.isEmpty {
|
if !clashSecret.isEmpty {
|
||||||
req.setValue("Bearer \(clashSecret)", forHTTPHeaderField: "Authorization")
|
req.setValue("Bearer \(clashSecret)", forHTTPHeaderField: "Authorization")
|
||||||
}
|
}
|
||||||
URLSession.shared.dataTask(with: req) { data, _, err in
|
URLSession.shared.dataTask(with: req) { data, resp, err in
|
||||||
if let err { NSLog("[pangolin/collector] clash %@ err=%@", path, err.localizedDescription) }
|
if let err { completion(nil, "err:\(err.localizedDescription)"); return }
|
||||||
guard let data else { completion(nil); return }
|
let code = (resp as? HTTPURLResponse)?.statusCode ?? -1
|
||||||
completion(try? JSONSerialization.jsonObject(with: data))
|
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)")
|
||||||
}.resume()
|
}.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -659,7 +659,7 @@
|
|||||||
baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */;
|
baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
||||||
@@ -674,7 +674,7 @@
|
|||||||
baseConfigurationReference = F8904897A48DC81799B8752E /* Pods-RunnerTests.release.xcconfig */;
|
baseConfigurationReference = F8904897A48DC81799B8752E /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
||||||
@@ -689,7 +689,7 @@
|
|||||||
baseConfigurationReference = B21E68FC1F5D33DD67A0DF5E /* Pods-RunnerTests.profile.xcconfig */;
|
baseConfigurationReference = B21E68FC1F5D33DD67A0DF5E /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
PRODUCT_BUNDLE_IDENTIFIER = com.pangolin.pangolin.RunnerTests;
|
||||||
@@ -960,7 +960,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -1010,7 +1010,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Developer ID Application";
|
CODE_SIGN_IDENTITY = "Developer ID Application";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
@@ -1059,7 +1059,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
|||||||
Reference in New Issue
Block a user