diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index 48e0eb5..1d1d348 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -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() diff --git a/client/macos/Runner.xcodeproj/project.pbxproj b/client/macos/Runner.xcodeproj/project.pbxproj index 0c5af93..334eb06 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 = 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;