fix(client): 看门狗返工 — 意外掉线捕获+服务端down轮询,撤销TUN下假阳性的TCP探测
测试暴露:全局TUN下App的socket到节点IP被本地TUN当场接住(假1ms),客户端TCP探数据口 不可行;且节点死时内核先掉线、提示被kernel off覆盖、节点页status不刷新显示绿色。返工: - 撤销 DataPlaneProber/livePingMs;延迟改回内核urltest(sing-box经REALITY真实直连数据面 探出的RTT,唯一可靠口径),连接态urltest=0→显示—不回退旧ping。 - 看门狗两条可靠路径:A 捕获「非用户主动的内核掉线」→置「节点异常」+刷列表; B 连接态周期刷/v1/nodes,所连节点被判down→智能切/手动断。 - _offNotice 让显式断开与kernel off读同一提示字段,不再互相覆盖(修「断开但没报错」)。 - 节点页周期15s刷新,让服务端down状态浮现(tile已按isDown置灰)。 返工测试:意外掉线提示/服务端down切/断/健康保持,全过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../bridge/vpn_bridge.dart';
|
||||
import '../core/responsive/form_factor.dart';
|
||||
import '../util/format.dart';
|
||||
import '../l10n/app_text.dart';
|
||||
@@ -40,10 +41,12 @@ class ConnectPage extends ConsumerWidget {
|
||||
}
|
||||
final down = formatSpeed(stats?.downloadSpeed);
|
||||
final up = formatSpeed(stats?.uploadSpeed);
|
||||
// 延迟来源:连接后用看门狗对数据面 IP(节点 host:443)的 TCP 握手 RTT(与判活同源,
|
||||
// 见 connection_provider);首测前/不可达回退节点页 TCP 探针 ping。都没有才 —。
|
||||
var livePing = conn.phase == VpnPhase.on ? (conn.livePingMs ?? 0) : node.ping;
|
||||
if (livePing <= 0) livePing = node.ping;
|
||||
// 延迟 = client→数据面 RTT,按连接态取不同测法(全局 TUN 下 App socket 测不到真节点):
|
||||
// · 连接态:内核 urltest —— sing-box 经 REALITY 真实直连数据面探出的 RTT(唯一可靠口径);
|
||||
// 节点死 → urltest=0 → 落到 —,本身即健康信号。
|
||||
// · 断开态:节点页 TCP 探针(直接握手 节点:443,无 TUN,真实 RTT)。
|
||||
var livePing = conn.phase == VpnPhase.on ? _bestUrltest(stats) : node.ping;
|
||||
if (livePing <= 0 && conn.phase != VpnPhase.on) livePing = node.ping;
|
||||
final pingLabel = livePing > 0 ? '${livePing}ms' : '—';
|
||||
final latencyValue = livePing > 0 ? '$livePing' : '—';
|
||||
|
||||
@@ -207,6 +210,13 @@ class ConnectPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 内核 urltest 各出站中的最小正延迟(ms);无则 0。连接态用作 client→数据面 RTT。
|
||||
int _bestUrltest(VpnStatsEvent? s) {
|
||||
if (s == null) return 0;
|
||||
final ds = s.urltestResults.where((r) => r.delayMs > 0).map((r) => r.delayMs);
|
||||
return ds.isEmpty ? 0 : ds.reduce((a, b) => a < b ? a : b);
|
||||
}
|
||||
|
||||
/// 实时速率行(连接成功时显示;来自内核 statsStream 实时数据)。
|
||||
class _SpeedRow extends StatelessWidget {
|
||||
const _SpeedRow({required this.t, required this.down, required this.up, required this.latency});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// nodes_page.dart — 节点页(置顶智能选择推荐卡 + 搜索 + 列表/双列网格)
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -27,6 +29,7 @@ class NodesPage extends ConsumerStatefulWidget {
|
||||
class _NodesPageState extends ConsumerState<NodesPage> {
|
||||
String _q = '';
|
||||
bool _refreshing = false;
|
||||
Timer? _poll;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -35,6 +38,16 @@ class _NodesPageState extends ConsumerState<NodesPage> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _refresh();
|
||||
});
|
||||
// 在页期间周期刷新,让服务端节点 status(如 down)及时浮现到列表(置灰/不可用)。
|
||||
_poll = Timer.periodic(const Duration(seconds: 15), (_) {
|
||||
if (mounted) ref.read(nodesProvider.notifier).refresh();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_poll?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
|
||||
Reference in New Issue
Block a user