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>
This commit is contained in:
wangjia
2026-06-30 14:58:07 +08:00
parent 211fda37e3
commit e3fb5239a6
3 changed files with 36 additions and 15 deletions
@@ -130,6 +130,11 @@ 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
@@ -374,6 +379,7 @@ 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?
@@ -396,23 +402,24 @@ final class StatsCollector: NSObject {
guard let self, !self.started else { return }
self.clashBase = "http://127.0.0.1:\(clashPort)"
self.clashSecret = clashSecret
self.statusClient = self.connectStatusClient()
self.statusClient = self.connectCommand(LibboxCommandStatus, "status")
self.logClient = self.connectCommand(LibboxCommandLog, "log") // urltest
self.started = true
self.startClashTimer()
}
}
/// libbox Status client()command.sock ,
private func connectStatusClient() -> LibboxCommandClient? {
/// libbox client command.sock ,
private func connectCommand(_ command: Int32, _ label: String) -> LibboxCommandClient? {
let options = LibboxCommandClientOptions()
options.statusInterval = 1_000_000_000 // 1s
options.addCommand(LibboxCommandStatus)
options.addCommand(command)
guard let c = LibboxNewCommandClient(self, options) else { return nil }
for _ in 0..<20 {
do { try c.connect(); NSLog("[pangolin/collector] status connected"); return c }
do { try c.connect(); NSLog("[pangolin/collector] %@ connected", label); return c }
catch { Thread.sleep(forTimeInterval: 0.25) }
}
NSLog("[pangolin/collector] status gave up")
NSLog("[pangolin/collector] %@ gave up", label)
return nil
}
@@ -421,7 +428,9 @@ 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
@@ -564,7 +573,19 @@ extension StatsCollector: LibboxCommandClientHandlerProtocol {
func connected() {}
func disconnected(_ message: String?) {}
func clearLogs() {}
func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) {}
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 setDefaultLogLevel(_ level: Int32) {}
func initializeClashMode(_ modeList: (any LibboxStringIteratorProtocol)?, currentMode: String?) {}
func updateClashMode(_ newMode: String?) {}