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:
@@ -101,6 +101,12 @@ class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler
|
||||
/** 缓存的「可手动选择的出口组」tag(selector 组)。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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user