// connection_provider.dart — 连接状态机(严格三态,状态由内核事件驱动) // // 设计约定: // - UI 调用 toggle(),控制器内部读取有效节点 + 认证令牌,调用 ConnectApi // 并启动 VpnBridge 子进程。 // - 严禁乐观翻转:VpnPhase.on 必须由 bridge.statusStream 确认后才置。 // - 状态来源:bridge.statusStream(来自内核回调),非 Timer 模拟。 import 'dart:async'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../bridge/vpn_bridge.dart'; import '../bridge/vpn_bridge_provider.dart'; import '../l10n/app_text.dart'; import '../models/node.dart'; import '../services/api_config.dart'; import '../services/connect_api.dart'; import '../services/device_identity.dart'; import 'app_providers.dart'; import 'auth_provider.dart'; import 'nodes_provider.dart'; import 'settings_provider.dart'; // 设备 ID 由 deviceIdentityProvider 提供(secure storage 持久化的稳定 UUID)。 // API base URL 统一用 api_config.dart 的 kApiBaseUrl(单一来源,勿再重复声明)。 // ── 连接阶段枚举 ────────────────────────────────────────────────── /// 连接阶段。严格三态,与设计稿一致。 enum VpnPhase { off, connecting, on } // ── 连接状态快照 ────────────────────────────────────────────────── class ConnectionState { const ConnectionState({required this.phase, this.elapsed = Duration.zero, this.error}); final VpnPhase phase; final Duration elapsed; /// 连接失败原因(已本地化);null = 无错误。供 UI 提示,不再静默吞掉。 final String? error; ConnectionState copyWith({VpnPhase? phase, Duration? elapsed}) => ConnectionState(phase: phase ?? this.phase, elapsed: elapsed ?? this.elapsed); @override bool operator ==(Object other) => other is ConnectionState && other.phase == phase && other.elapsed == elapsed && other.error == error; @override int get hashCode => Object.hash(phase, elapsed, error); } // ── 连通看门狗 ───────────────────────────────────────────────────── // // 已连接(本地 TUN 已起)≠ 远端节点数据面可用:节点 sing-box 崩了/数据口不通而 // agent 仍在线时,客户端会一直显示「已连接」但流量全失败。看门狗周期性经隧道做 // 连通性探测,连续失败即判当前节点不可用 → 智能选择自动切节点 / 手动选定的只断开提示。 /// 连通探测接缝:true=经隧道能到外网。默认走海外 204 端点;测试可注入假实现。 typedef HealthProber = Future Function(); const _kWatchdogInterval = Duration(seconds: 15); const _kHealthFailLimit = 3; // 连续 N 次失败(≈45s)才判定不可用,防抖 /// 默认探测:GET 海外 generate_204(全局 TUN 下走隧道 → 真实检验代理出网)。 /// 任一端点 2xx/204 即视为通;多端点容错,避免单点误判。 Future defaultHealthProbe() async { const urls = [ 'https://www.gstatic.com/generate_204', 'https://cp.cloudflare.com/generate_204', ]; for (final u in urls) { final client = HttpClient()..connectionTimeout = const Duration(seconds: 4); try { final req = await client.getUrl(Uri.parse(u)).timeout(const Duration(seconds: 5)); final resp = await req.close().timeout(const Duration(seconds: 5)); await resp.drain(); if (resp.statusCode >= 200 && resp.statusCode < 300) return true; } catch (_) { // 该端点不通,试下一个 } finally { client.close(force: true); } } return false; } // ── 状态机 ─────────────────────────────────────────────────────── class ConnectionController extends StateNotifier { ConnectionController(this._ref, this._bridge, {HealthProber? prober}) : _prober = prober ?? defaultHealthProbe, super(const ConnectionState(phase: VpnPhase.off)) { // 订阅桥状态流:状态由内核事件驱动,严禁 UI 乐观翻转。 _statusSub = _bridge.statusStream.listen(_onKernelStatus); } final Ref _ref; final VpnBridge _bridge; final HealthProber _prober; StreamSubscription? _statusSub; Timer? _elapsed; Timer? _watchdog; int _healthFails = 0; bool _probing = false; ConnectApi? _api; // ── 公有 API ─────────────────────────────────────────────────── /// 用户点击连接键:按当前状态决定动作,握手中忽略(禁乐观)。 void toggle() { switch (state.phase) { case VpnPhase.off: _connect(); case VpnPhase.on: _disconnect(); case VpnPhase.connecting: break; // 握手进行中,不响应 } } /// 节点切换时触发:已连接则重连。 void onNodeChanged() { if (state.phase == VpnPhase.on || state.phase == VpnPhase.connecting) { _disconnect().then((_) => _connect()); } } // ── 内部 ───────────────────────────────────────────────────── Future _connect() async { state = const ConnectionState(phase: VpnPhase.connecting); final node = _ref.read(effectiveNodeProvider); final zh = _ref.read(localeProvider) == AppLang.zh; // 节点未就绪(列表加载中/为空):不连接,提示用户。 if (node.uuid.isEmpty) { if (mounted) { state = ConnectionState( phase: VpnPhase.off, error: zh ? '节点尚未就绪,请稍候重试' : 'Nodes not ready, please retry', ); } return; } try { final configJson = await _fetchConfigWithRefresh(node.uuid); // bridge.start() 不阻塞至连接建立;on 状态由 statusStream 回调驱动。 await _bridge.start(configJson); } on ConnectApiException catch (e) { // 把后端/网络错误冒泡到 UI(原静默回 off,用户不知所以)。 if (mounted) state = ConnectionState(phase: VpnPhase.off, error: zh ? e.messageZh : e.messageEn); } catch (e) { if (mounted) { state = ConnectionState( phase: VpnPhase.off, error: zh ? '连接失败,请重试' : 'Connection failed, please retry', ); } } } /// 取配置;access token 过期(401)时用 refresh token 续期后**重试一次**。 /// 续期失败(refresh 也过期 / 被拒)由 authProvider.refresh() 触发登出 → UI 回登录页。 Future _fetchConfigWithRefresh(String nodeUuid) async { final deviceId = await _ref.read(deviceIdentityProvider).deviceId(); Future doFetch() { final token = _ref.read(authProvider).accessToken ?? ''; _api?.dispose(); _api = _ref.read(connectApiFactoryProvider)(token); return _api!.fetchConfig( nodeId: nodeUuid, deviceId: deviceId, // smartRoute 偏好 → 国内分流(#5):国内 IP/域名直连,不走隧道。 splitCN: _ref.read(settingsProvider).smartRoute, ); } try { return await doFetch(); } on ConnectApiException catch (e) { if (e.statusCode == 401) { // token 过期 → 续期后重试一次;续期成功则用新 token 再取。 final ok = await _ref.read(authProvider.notifier).refresh(); if (ok) return await doFetch(); } rethrow; } } Future _disconnect() async { _stopElapsed(); try { await _bridge.stop(); } catch (_) {} if (mounted) state = const ConnectionState(phase: VpnPhase.off); } void _onKernelStatus(VpnStatus s) { if (!mounted) return; switch (s) { case VpnStatus.on: state = state.copyWith(phase: VpnPhase.on); _startElapsed(); _startWatchdog(); case VpnStatus.connecting: state = state.copyWith(phase: VpnPhase.connecting); _stopElapsed(); _stopWatchdog(); case VpnStatus.off: case VpnStatus.error: state = const ConnectionState(phase: VpnPhase.off); _stopElapsed(); _stopWatchdog(); } } // ── 连通看门狗 ─────────────────────────────────────────────── void _startWatchdog() { _watchdog?.cancel(); _healthFails = 0; _watchdog = Timer.periodic(_kWatchdogInterval, (_) => _checkHealth()); } void _stopWatchdog() { _watchdog?.cancel(); _watchdog = null; _healthFails = 0; _probing = false; } Future _checkHealth() async { if (_probing || !mounted || state.phase != VpnPhase.on) return; _probing = true; final ok = await _prober(); _probing = false; if (!mounted || state.phase != VpnPhase.on) return; if (ok) { _healthFails = 0; return; } _healthFails++; if (_healthFails >= _kHealthFailLimit) { _stopWatchdog(); await _onNodeUnhealthy(); } } /// 当前节点连续探测失败:智能选择 → 切到其他最优可用节点重连;手动选定 → 断开并提示。 Future _onNodeUnhealthy() async { final t = _ref.read(appTextProvider); final smart = _ref.read(selectedNodeCodeProvider) == kSmartNodeCode; final altCode = smart ? _pickAlternativeCode(_ref.read(effectiveNodeProvider).code) : null; if (smart && altCode != null) { _ref.read(selectedNodeCodeProvider.notifier).state = altCode; await _disconnect(); await _connect(); // 重连进行中给出「已自动切换」提示(connecting 态显示,连上后随 copyWith 清除)。 if (mounted && state.phase != VpnPhase.off) { state = ConnectionState(phase: state.phase, error: t.nodeUnhealthySwitched); } return; } // 手动选定节点(或智能模式无其他可用节点):断开并提示,尊重用户选择、不自动换。 await _disconnect(); if (mounted) state = ConnectionState(phase: VpnPhase.off, error: t.nodeUnhealthyError); } /// 选延迟最优、可用(status up)、非当前节点的 code;无则 null。 String? _pickAlternativeCode(String excludeCode) { final nodes = (_ref.read(nodesProvider).valueOrNull ?? const []) .where((n) => !n.isDown && n.uuid.isNotEmpty && n.code != excludeCode) .toList(); if (nodes.isEmpty) return null; nodes.sort((a, b) { final pa = a.ping > 0 ? a.ping : 1 << 30; final pb = b.ping > 0 ? b.ping : 1 << 30; return pa.compareTo(pb); }); return nodes.first.code; } void _startElapsed() { _elapsed?.cancel(); _elapsed = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted && state.phase == VpnPhase.on) { state = state.copyWith(elapsed: state.elapsed + const Duration(seconds: 1)); } }); } void _stopElapsed() { _elapsed?.cancel(); _elapsed = null; } @override void dispose() { _statusSub?.cancel(); _stopElapsed(); _stopWatchdog(); _api?.dispose(); super.dispose(); } } // ── Provider ────────────────────────────────────────────────────── final connectionProvider = StateNotifierProvider( (ref) => ConnectionController(ref, ref.watch(vpnBridgeProvider)), ); /// 内核实时统计流(上/下行瞬时速率、字节数)。连接页速度行的真实数据源。 final vpnStatsProvider = StreamProvider( (ref) => ref.watch(vpnBridgeProvider).statsStream, ); /// 按 authToken 造 ConnectApi 的工厂(支柱 1:接缝即接口)。默认走真控制面; /// 测试 override 注入 MockClient,即可驱动「连接成功」路径而不打真网络。 typedef ConnectApiFactory = ConnectApi Function(String authToken); final connectApiFactoryProvider = Provider( (_) => (authToken) => ConnectApi(baseUrl: kApiBaseUrl, authToken: authToken), );