fix(client/macos): urltest 延迟改走注入的 clash API(对齐 Windows),内核侧打点
诊断:即便专开 Group client,扩展回的 urltest 数组恒空——根因是服务端下发配置无 experimental.clash_api,sing-box 不暴露出站组/urltest 历史,libbox Group 命令拿不到。 修复(与 Windows 完全同法):扩展内向配置注入 clash_api(127.0.0.1 本地监听)+ cache_file, StatsCollector 经本地 HTTP 查 /proxies 找 URLTest 组、/group/<name>/delay 取真实延迟。 速率仍走 libbox Status。新增内核侧 NSLog 打点 kernel delay(<group>)=<tag:ms>,并把该值 塞进 stats 的 diag 字段经主 app 可读日志透出——一眼分清「内核有没有算出延迟」vs「外面没拿到」。 扩展代码变更 → CURRENT_PROJECT_VERSION 46→47。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,14 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
do {
|
do {
|
||||||
let configContent = try self.resolveConfig(options)
|
// urltest 延迟走 clash API:服务端下发的配置没有 clash_api,sing-box 不暴露
|
||||||
|
// 出站组/urltest 历史 → libbox Group 命令返回空 → 连接页延迟为 —。这里在扩展内
|
||||||
|
// 注入 clash_api(127.0.0.1 本地监听)+ cache_file,StatsCollector 经本地 HTTP
|
||||||
|
// 查 /proxies + /group/<name>/delay 取真实延迟(与 Windows 同法)。
|
||||||
|
let clashPort = Int.random(in: 20000...60000)
|
||||||
|
let clashSecret = UUID().uuidString
|
||||||
|
let configContent = Self.injectClashApi(try self.resolveConfig(options),
|
||||||
|
port: clashPort, secret: clashSecret)
|
||||||
|
|
||||||
// libbox 数据目录用扩展自己的 App Group 容器(root 运行 → /var/root/...)。
|
// libbox 数据目录用扩展自己的 App Group 容器(root 运行 → /var/root/...)。
|
||||||
// ⚠️ 不要试图把它指向用户容器:扩展以 root 运行,command.sock 的目录/属主
|
// ⚠️ 不要试图把它指向用户容器:扩展以 root 运行,command.sock 的目录/属主
|
||||||
@@ -69,9 +76,8 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
try server.startOrReloadService(configContent, options: LibboxOverrideOptions())
|
try server.startOrReloadService(configContent, options: LibboxOverrideOptions())
|
||||||
self.commandServer = server
|
self.commandServer = server
|
||||||
|
|
||||||
// 进程内 stats 采集:连本进程自己的 command.sock(同容器,root 可达),
|
// 进程内 stats 采集:libbox command.sock 取上/下行;clash API 取 urltest 延迟。
|
||||||
// 缓存最新上/下行 + urltest,供 handleAppMessage 回给主 app。
|
self.stats.start(clashPort: clashPort, clashSecret: clashSecret)
|
||||||
self.stats.start()
|
|
||||||
|
|
||||||
log.info("startTunnel: service started")
|
log.info("startTunnel: service started")
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
@@ -108,6 +114,27 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在 sing-box 配置里注入 experimental.clash_api(本地监听)+ cache_file,让内核暴露
|
||||||
|
// 出站组/urltest 历史,供 StatsCollector 经本地 HTTP 取真实延迟。已有则不覆盖。
|
||||||
|
static func injectClashApi(_ configJSON: String, port: Int, secret: String) -> String {
|
||||||
|
guard var root = (try? JSONSerialization.jsonObject(with: Data(configJSON.utf8)))
|
||||||
|
as? [String: Any] else { return configJSON }
|
||||||
|
var experimental = (root["experimental"] as? [String: Any]) ?? [:]
|
||||||
|
if experimental["clash_api"] == nil {
|
||||||
|
experimental["clash_api"] = [
|
||||||
|
"external_controller": "127.0.0.1:\(port)",
|
||||||
|
"secret": secret,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if experimental["cache_file"] == nil {
|
||||||
|
experimental["cache_file"] = ["enabled": true]
|
||||||
|
}
|
||||||
|
root["experimental"] = experimental
|
||||||
|
guard let data = try? JSONSerialization.data(withJSONObject: root),
|
||||||
|
let out = String(data: data, encoding: .utf8) else { return configJSON }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
private func resolveConfig(_ options: [String: NSObject]?) throws -> String {
|
private func resolveConfig(_ options: [String: NSObject]?) throws -> String {
|
||||||
if let inline = options?["configContent"] as? String, !inline.isEmpty {
|
if let inline = options?["configContent"] as? String, !inline.isEmpty {
|
||||||
return inline
|
return inline
|
||||||
@@ -338,24 +365,26 @@ private func simpleError(_ msg: String) -> NSError {
|
|||||||
NSError(domain: "pangolin.tunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: msg])
|
NSError(domain: "pangolin.tunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: msg])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── StatsCollector:扩展进程内的 libbox 命令客户端 ───────────────────────────
|
// ── StatsCollector:扩展进程内统计采集 ──────────────────────────────────────
|
||||||
// 连「本进程自己的」command.sock(同容器、root 可达,无跨进程/跨用户问题),订阅
|
// 上/下行速率:连本进程自己的 command.sock(libbox Status 命令)。
|
||||||
// status/group,缓存最新上/下行 + urltest;由 handleAppMessage 回给主 app。等价于旧的
|
// urltest 延迟:经注入的 clash API(本地 HTTP)查 /proxies + /group/<name>/delay——
|
||||||
// 主 app 端 StatsClient,但跑在扩展里——根治主 app 跨 root/用户容器连不上 socket 的死局。
|
// 服务端配置无 clash_api,sing-box 不暴露组/urltest 历史,libbox Group 命令返回空;
|
||||||
|
// 注入 clash_api 后用与 Windows 完全一致的取法(getGroupDelay)拿内核实测延迟。
|
||||||
|
// 缓存的 latest 由 handleAppMessage 回给主 app。
|
||||||
final class StatsCollector: NSObject {
|
final class StatsCollector: NSObject {
|
||||||
private let queue = DispatchQueue(label: "pangolin.stats.collector")
|
private let queue = DispatchQueue(label: "pangolin.stats.collector")
|
||||||
// ⚠️ libbox 的 command 协议是「一连接一命令」:一个 client 只订阅一种命令。把 Status 与
|
|
||||||
// Group addCommand 到同一 client,只有第一种(Status)生效,Group 永不回调 → urltest 为空、
|
|
||||||
// 连接页延迟显示 —。对齐 sing-box-for-apple:每种命令各开一个 client。
|
|
||||||
private var statusClient: LibboxCommandClient?
|
private var statusClient: LibboxCommandClient?
|
||||||
private var groupClient: LibboxCommandClient?
|
|
||||||
private var started = false
|
private var started = false
|
||||||
private var urlTestTimer: DispatchSourceTimer?
|
private var clashTimer: DispatchSourceTimer?
|
||||||
|
|
||||||
|
// clash API(扩展内注入,127.0.0.1 本地监听):取出站组 urltest 真实延迟。
|
||||||
|
private var clashBase = ""
|
||||||
|
private var clashSecret = ""
|
||||||
|
private var groupNames: [String] = []
|
||||||
|
|
||||||
private let lock = NSLock()
|
private let lock = NSLock()
|
||||||
private var latest: [String: Any] = StatsCollector.zero
|
private var latest: [String: Any] = StatsCollector.zero
|
||||||
private var latestUrltest: [(tag: String, delayMs: Int)] = []
|
private var latestUrltest: [(tag: String, delayMs: Int)] = []
|
||||||
private var groupTags: [String] = []
|
|
||||||
|
|
||||||
private static let zero: [String: Any] = [
|
private static let zero: [String: Any] = [
|
||||||
"uploadBytes": 0, "downloadBytes": 0,
|
"uploadBytes": 0, "downloadBytes": 0,
|
||||||
@@ -363,52 +392,42 @@ final class StatsCollector: NSObject {
|
|||||||
"urltestResults": [[String: Any]](),
|
"urltestResults": [[String: Any]](),
|
||||||
]
|
]
|
||||||
|
|
||||||
func start() {
|
func start(clashPort: Int, clashSecret: String) {
|
||||||
queue.async { [weak self] in
|
queue.async { [weak self] in
|
||||||
guard let self, !self.started else { return }
|
guard let self, !self.started else { return }
|
||||||
self.statusClient = self.connectClient(command: LibboxCommandStatus, label: "status")
|
self.clashBase = "http://127.0.0.1:\(clashPort)"
|
||||||
self.groupClient = self.connectClient(command: LibboxCommandGroup, label: "group")
|
self.clashSecret = clashSecret
|
||||||
guard self.statusClient != nil || self.groupClient != nil else { return }
|
self.statusClient = self.connectStatusClient()
|
||||||
self.started = true
|
self.started = true
|
||||||
self.startUrlTestTimer()
|
self.startClashTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 为单一命令开一个 client 并连上(command.sock 刚建好可能稍滞后,重试)。
|
/// libbox Status client(取速率)。command.sock 刚建好可能稍滞后,重试。
|
||||||
private func connectClient(command: Int32, label: String) -> LibboxCommandClient? {
|
private func connectStatusClient() -> LibboxCommandClient? {
|
||||||
let options = LibboxCommandClientOptions()
|
let options = LibboxCommandClientOptions()
|
||||||
options.statusInterval = 1_000_000_000 // 1s
|
options.statusInterval = 1_000_000_000 // 1s
|
||||||
options.addCommand(command)
|
options.addCommand(LibboxCommandStatus)
|
||||||
guard let c = LibboxNewCommandClient(self, options) else {
|
guard let c = LibboxNewCommandClient(self, options) else { return nil }
|
||||||
NSLog("[pangolin/collector] %@ LibboxNewCommandClient nil", label)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _ in 0..<20 {
|
for _ in 0..<20 {
|
||||||
do {
|
do { try c.connect(); NSLog("[pangolin/collector] status connected"); return c }
|
||||||
try c.connect()
|
catch { Thread.sleep(forTimeInterval: 0.25) }
|
||||||
NSLog("[pangolin/collector] %@ connected", label)
|
|
||||||
return c
|
|
||||||
} catch {
|
|
||||||
Thread.sleep(forTimeInterval: 0.25)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
NSLog("[pangolin/collector] %@ gave up connecting", label)
|
NSLog("[pangolin/collector] status gave up")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
queue.async { [weak self] in
|
queue.async { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.urlTestTimer?.cancel(); self.urlTestTimer = nil
|
self.clashTimer?.cancel(); self.clashTimer = nil
|
||||||
if let c = self.statusClient { try? c.disconnect() }
|
if let c = self.statusClient { try? c.disconnect() }
|
||||||
if let c = self.groupClient { try? c.disconnect() }
|
|
||||||
self.statusClient = nil
|
self.statusClient = nil
|
||||||
self.groupClient = nil
|
|
||||||
self.started = false
|
self.started = false
|
||||||
|
self.groupNames = []
|
||||||
self.lock.lock()
|
self.lock.lock()
|
||||||
self.latest = StatsCollector.zero
|
self.latest = StatsCollector.zero
|
||||||
self.latestUrltest = []
|
self.latestUrltest = []
|
||||||
self.groupTags = []
|
|
||||||
self.lock.unlock()
|
self.lock.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,17 +437,86 @@ final class StatsCollector: NSObject {
|
|||||||
return (try? JSONSerialization.data(withJSONObject: snap)) ?? Data("{}".utf8)
|
return (try? JSONSerialization.data(withJSONObject: snap)) ?? Data("{}".utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startUrlTestTimer() {
|
// ── clash API 轮询:取 urltest 延迟(与 Windows getGroupDelay 同法)─────────
|
||||||
urlTestTimer?.cancel()
|
private func startClashTimer() {
|
||||||
let timer = DispatchSource.makeTimerSource(queue: queue)
|
let timer = DispatchSource.makeTimerSource(queue: queue)
|
||||||
timer.schedule(deadline: .now() + 2, repeating: 10)
|
timer.schedule(deadline: .now() + 3, repeating: 8)
|
||||||
timer.setEventHandler { [weak self] in
|
timer.setEventHandler { [weak self] in self?.pollClash() }
|
||||||
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()
|
timer.resume()
|
||||||
urlTestTimer = timer
|
clashTimer = timer
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pollClash() {
|
||||||
|
if groupNames.isEmpty {
|
||||||
|
clashGet("/proxies", query: nil) { [weak self] obj in
|
||||||
|
guard let self else { return }
|
||||||
|
let names = StatsCollector.extractUrltestGroups(obj)
|
||||||
|
self.queue.async {
|
||||||
|
self.groupNames = names
|
||||||
|
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 in
|
||||||
|
guard let self, let dict = obj as? [String: Any] else { return }
|
||||||
|
var urltest: [(tag: String, delayMs: Int)] = []
|
||||||
|
for (tag, v) in dict {
|
||||||
|
if let d = (v as? NSNumber)?.intValue { urltest.append((tag: tag, delayMs: d)) }
|
||||||
|
}
|
||||||
|
let summary = urltest.map { "\($0.tag):\($0.delayMs)" }.joined(separator: ",")
|
||||||
|
NSLog("[pangolin/collector] kernel delay(%@)=%@", name, summary) // 内核侧实测打点
|
||||||
|
self.queue.async {
|
||||||
|
self.lock.lock()
|
||||||
|
self.latestUrltest = urltest
|
||||||
|
self.latest["urltestResults"] = urltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] }
|
||||||
|
self.latest["diag"] = "delay(\(name))=\(summary)"
|
||||||
|
self.lock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setDiag(_ s: String) {
|
||||||
|
lock.lock(); latest["diag"] = s; lock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clashGet(_ path: String, query: [String: String]?,
|
||||||
|
completion: @escaping (Any?) -> Void) {
|
||||||
|
guard var comps = URLComponents(string: clashBase + path) else { completion(nil); return }
|
||||||
|
if let query { comps.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) } }
|
||||||
|
guard let url = comps.url else { completion(nil); return }
|
||||||
|
var req = URLRequest(url: url)
|
||||||
|
req.timeoutInterval = 8
|
||||||
|
if !clashSecret.isEmpty {
|
||||||
|
req.setValue("Bearer \(clashSecret)", forHTTPHeaderField: "Authorization")
|
||||||
|
}
|
||||||
|
URLSession.shared.dataTask(with: req) { data, _, err in
|
||||||
|
if let err { NSLog("[pangolin/collector] clash %@ err=%@", path, err.localizedDescription) }
|
||||||
|
guard let data else { completion(nil); return }
|
||||||
|
completion(try? JSONSerialization.jsonObject(with: data))
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 /proxies 提取 URLTest/Fallback 组名(clash 约定类型名首字母大写)。
|
||||||
|
static func extractUrltestGroups(_ obj: Any?) -> [String] {
|
||||||
|
guard let root = obj as? [String: Any],
|
||||||
|
let proxies = root["proxies"] as? [String: Any] else { return [] }
|
||||||
|
var names: [String] = []
|
||||||
|
for (name, v) in proxies {
|
||||||
|
if let m = v as? [String: Any], let type = m["type"] as? String,
|
||||||
|
type == "URLTest" || type == "Fallback" {
|
||||||
|
names.append(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,34 +532,14 @@ extension StatsCollector: LibboxCommandClientHandlerProtocol {
|
|||||||
|
|
||||||
func writeStatus(_ message: LibboxStatusMessage?) {
|
func writeStatus(_ message: LibboxStatusMessage?) {
|
||||||
guard let m = message else { return }
|
guard let m = message else { return }
|
||||||
|
// 逐键更新(不整盘重建),保留 clash 轮询写入的 urltestResults / diag。
|
||||||
lock.lock()
|
lock.lock()
|
||||||
latest = [
|
latest["uploadBytes"] = m.uplinkTotal
|
||||||
"uploadBytes": m.uplinkTotal,
|
latest["downloadBytes"] = m.downlinkTotal
|
||||||
"downloadBytes": m.downlinkTotal,
|
latest["uploadSpeed"] = Double(m.uplink)
|
||||||
"uploadSpeed": Double(m.uplink),
|
latest["downloadSpeed"] = Double(m.downlink)
|
||||||
"downloadSpeed": Double(m.downlink),
|
|
||||||
"urltestResults": latestUrltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] },
|
|
||||||
]
|
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeGroups(_ message: (any LibboxOutboundGroupIteratorProtocol)?) {
|
func writeGroups(_ message: (any LibboxOutboundGroupIteratorProtocol)?) {}
|
||||||
guard let groups = message else { return }
|
|
||||||
var urltest: [(tag: String, delayMs: Int)] = []
|
|
||||||
var tags: [String] = []
|
|
||||||
while groups.hasNext() {
|
|
||||||
guard let group = groups.next() else { break }
|
|
||||||
tags.append(group.tag)
|
|
||||||
guard let items = group.getItems() else { continue }
|
|
||||||
while items.hasNext() {
|
|
||||||
guard let item = items.next() else { break }
|
|
||||||
urltest.append((tag: item.tag, delayMs: Int(item.urlTestDelay)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lock.lock()
|
|
||||||
latestUrltest = urltest
|
|
||||||
latest["urltestResults"] = urltest.map { ["tag": $0.tag, "delayMs": $0.delayMs] }
|
|
||||||
lock.unlock()
|
|
||||||
queue.async { [weak self] in self?.groupTags = tags }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = 46;
|
CURRENT_PROJECT_VERSION = 47;
|
||||||
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 = 46;
|
CURRENT_PROJECT_VERSION = 47;
|
||||||
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 = 46;
|
CURRENT_PROJECT_VERSION = 47;
|
||||||
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 = 46;
|
CURRENT_PROJECT_VERSION = 47;
|
||||||
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 = 46;
|
CURRENT_PROJECT_VERSION = 47;
|
||||||
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 = 46;
|
CURRENT_PROJECT_VERSION = 47;
|
||||||
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
DEVELOPMENT_TEAM = BYL4KQHMTN;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ final class StatsClient: NSObject {
|
|||||||
if self.diagTick % 5 == 0 {
|
if self.diagTick % 5 == 0 {
|
||||||
let urls = (obj["urltestResults"] as? [[String: Any]]) ?? []
|
let urls = (obj["urltestResults"] as? [[String: Any]]) ?? []
|
||||||
let pairs = urls.map { "\($0["tag"] ?? "?")=\($0["delayMs"] ?? -1)" }.joined(separator: ",")
|
let pairs = urls.map { "\($0["tag"] ?? "?")=\($0["delayMs"] ?? -1)" }.joined(separator: ",")
|
||||||
NSLog("[pangolin/stats] dl=%@ ul=%@ urltest[%d]=%@",
|
NSLog("[pangolin/stats] dl=%@ ul=%@ urltest[%d]=%@ diag=%@",
|
||||||
"\(obj["downloadSpeed"] ?? 0)", "\(obj["uploadSpeed"] ?? 0)", urls.count, pairs)
|
"\(obj["downloadSpeed"] ?? 0)", "\(obj["uploadSpeed"] ?? 0)", urls.count, pairs,
|
||||||
|
"\(obj["diag"] ?? "-")")
|
||||||
}
|
}
|
||||||
self.emit(obj)
|
self.emit(obj)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user