fix(client/macos): StatsCollector 用独立 client 订阅 Group,修 urltest 延迟为空

诊断确认:连接页延迟显示 — 是因为扩展回给主 app 的 urltest 数组恒为空(speeds 正常)。
根因:libbox command 协议是「一连接一命令」,把 Status 与 Group addCommand 到同一 client
只有 Status 生效,Group 永不回调 → 无 urltest。对齐 sing-box-for-apple:每命令各开一个
client。新增 groupClient 专订 Group + 跑 urlTest,延迟即随统计帧回到连接页(与 Windows 同源)。

扩展代码变更 → CURRENT_PROJECT_VERSION 45→46,让 sysextd 重装。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-30 12:33:00 +08:00
parent a43f8a93de
commit 783687d7c2
2 changed files with 45 additions and 33 deletions
@@ -344,7 +344,11 @@ private func simpleError(_ msg: String) -> NSError {
// app StatsClient, app root/ socket
final class StatsCollector: NSObject {
private let queue = DispatchQueue(label: "pangolin.stats.collector")
private var client: LibboxCommandClient?
// libbox command : client Status
// Group addCommand client,(Status),Group urltest
// sing-box-for-apple: client
private var statusClient: LibboxCommandClient?
private var groupClient: LibboxCommandClient?
private var started = false
private var urlTestTimer: DispatchSourceTimer?
@@ -362,37 +366,44 @@ final class StatsCollector: NSObject {
func start() {
queue.async { [weak self] in
guard let self, !self.started else { return }
let options = LibboxCommandClientOptions()
options.statusInterval = 1_000_000_000 // 1s
options.addCommand(LibboxCommandStatus)
options.addCommand(LibboxCommandGroup)
guard let c = LibboxNewCommandClient(self, options) else {
NSLog("[pangolin/collector] LibboxNewCommandClient nil")
return
}
// command.sock server.start() ,,
for _ in 0..<20 {
do {
try c.connect()
self.client = c
self.started = true
self.startUrlTestTimer()
NSLog("[pangolin/collector] connected to own command.sock")
return
} catch {
Thread.sleep(forTimeInterval: 0.25)
}
}
NSLog("[pangolin/collector] gave up connecting to own command.sock")
self.statusClient = self.connectClient(command: LibboxCommandStatus, label: "status")
self.groupClient = self.connectClient(command: LibboxCommandGroup, label: "group")
guard self.statusClient != nil || self.groupClient != nil else { return }
self.started = true
self.startUrlTestTimer()
}
}
/// client (command.sock ,)
private func connectClient(command: Int32, label: String) -> LibboxCommandClient? {
let options = LibboxCommandClientOptions()
options.statusInterval = 1_000_000_000 // 1s
options.addCommand(command)
guard let c = LibboxNewCommandClient(self, options) else {
NSLog("[pangolin/collector] %@ LibboxNewCommandClient nil", label)
return nil
}
for _ in 0..<20 {
do {
try c.connect()
NSLog("[pangolin/collector] %@ connected", label)
return c
} catch {
Thread.sleep(forTimeInterval: 0.25)
}
}
NSLog("[pangolin/collector] %@ gave up connecting", label)
return nil
}
func stop() {
queue.async { [weak self] in
guard let self else { return }
self.urlTestTimer?.cancel(); self.urlTestTimer = nil
if let c = self.client { try? c.disconnect() }
self.client = nil
if let c = self.statusClient { try? c.disconnect() }
if let c = self.groupClient { try? c.disconnect() }
self.statusClient = nil
self.groupClient = nil
self.started = false
self.lock.lock()
self.latest = StatsCollector.zero
@@ -410,9 +421,10 @@ final class StatsCollector: NSObject {
private func startUrlTestTimer() {
urlTestTimer?.cancel()
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + 2, repeating: 12)
timer.schedule(deadline: .now() + 2, repeating: 10)
timer.setEventHandler { [weak self] in
guard let self, let c = self.client else { return }
guard let self, let c = self.groupClient else { return }
// groupTags writeGroups ; urltest, sing-box
for tag in self.groupTags { try? c.urlTest(tag) }
}
timer.resume()
@@ -659,7 +659,7 @@
baseConfigurationReference = C57BBFD43F9175D1E23685EF /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 45;
CURRENT_PROJECT_VERSION = 46;
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 = 45;
CURRENT_PROJECT_VERSION = 46;
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 = 45;
CURRENT_PROJECT_VERSION = 46;
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 = 45;
CURRENT_PROJECT_VERSION = 46;
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 = 45;
CURRENT_PROJECT_VERSION = 46;
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 = 45;
CURRENT_PROJECT_VERSION = 46;
DEVELOPMENT_TEAM = BYL4KQHMTN;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;