feat(client/native): stats-overhaul Phase1 — 四端实时统计采集端

各原生侧把 libbox/Clash 的实时上下行/延迟经冻结的 pangolin/vpn/stats channel
生产真实帧:
- iOS/macOS 新增 StatsClient.swift(LibboxNewCommandClient,statusInterval 1s,
  uplink/downlink/总量 + urltest 延迟映射契约字段),VpnManager/VpnChannel 状态
  观察驱动 start/stop,pbxproj 登记。
- Android PangolinVpnService.kt 延迟修复。

契约字段 uploadBytes/downloadBytes/uploadSpeed/downloadSpeed/urltestResults 不改。
真机验证(连接后实时跳动)为运行时步骤,见计划 Phase1 待办。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-28 18:13:48 +08:00
parent 636a3bbf2f
commit bf84492eef
7 changed files with 459 additions and 14 deletions
@@ -101,6 +101,12 @@ class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler
/** 缓存的「可手动选择的出口组」tagselector 组)。selectOutbound 据此切换。 */
@Volatile private var selectableGroup: String = ""
/** writeGroups 捕获的全部出口组 tag(如 urltest 的 "auto"),供主动 urltest 探测。 */
@Volatile private var groupTags: List<String> = emptyList()
/** 周期主动 urltest:补偿 sing-box urltest 惰性探测(首个 interval 前 history 为空)
* 每 12s 主动触发一次,让延迟稳定及时刷新。 */
private var urlTestTimer: java.util.Timer? = null
private val stopping = AtomicBoolean(false)
private var libboxSetupDone = false
@@ -208,11 +214,29 @@ class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler
client.connect()
statsClient = client
Log.i(TAG, "stats CommandClient connected")
startUrlTestTimer()
} catch (e: Exception) {
Log.w(TAG, "stats CommandClient failed: $e")
}
}
/** 周期主动触发 urltest 探测,让延迟稳定出现(补偿惰性探测)。 */
private fun startUrlTestTimer() {
urlTestTimer?.cancel()
val timer = java.util.Timer("pangolin-urltest", true)
timer.scheduleAtFixedRate(object : java.util.TimerTask() {
override fun run() {
val client = statsClient ?: return
for (tag in groupTags) {
try { client.urlTest(tag) } catch (e: Exception) {
Log.w(TAG, "urlTest($tag) failed: $e")
}
}
}
}, 2_000L, 12_000L)
urlTestTimer = timer
}
/** 由 MainActivity(同进程)调用:切换出口节点到 [tag](在缓存的可选组内)。 */
fun selectOutbound(tag: String) {
val group = selectableGroup
@@ -256,10 +280,12 @@ class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler
override fun writeGroups(groups: OutboundGroupIterator?) {
if (groups == null) return
val urltest = mutableListOf<Pair<String, Int>>()
val tags = mutableListOf<String>()
var selected = activeOutbound
try {
while (groups.hasNext()) {
val group = groups.next()
tags.add(group.tag)
// 记录可选组的当前出口(如 selector / urltest 的 selected)。
if (group.selected.isNotEmpty()) selected = group.selected
if (group.selectable) selectableGroup = group.tag
@@ -273,6 +299,7 @@ class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler
Log.w(TAG, "writeGroups parse error: $e")
}
latestUrltest = urltest
groupTags = tags
activeOutbound = selected
}
}
@@ -283,6 +310,8 @@ class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler
if (!stopping.compareAndSet(false, true)) return
Log.i(TAG, "doStop")
try { urlTestTimer?.cancel() } catch (_: Exception) {}
urlTestTimer = null
try { statsClient?.disconnect() } catch (_: Exception) {}
statsClient = null