fix(client): 自动连接死锁兜底(轮询)+ 逐次门控日志(1.0.27)

复现:armed→设置已加载后无 ready。死锁:selLoaded 靠 selectedNode controller 的 loaded 标志,
而该标志完成时若选中值未变(==默认)不会发通知,若 nodeSub 已先于它触发则此后无人再调 tryGo。
修:_maybeAutoConnect 加 Timer.periodic(400ms,≤12s)兜底复查,decided 后停;并恢复每次 tryGo
打 check 日志(settingsLoaded/selLoaded/node uuid)以确认卡点。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 19:58:53 +08:00
parent 277d1c6e97
commit ada99e6c3a
3 changed files with 22 additions and 5 deletions
+20 -3
View File
@@ -1,5 +1,6 @@
// main.dart — 入口(演示态)
// 串接:登录 → 引导 → 主框架(4 Tab);状态层统一走 Riverpod。
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
@@ -133,7 +134,14 @@ class _RootFlowState extends ConsumerState<RootFlow> {
ProviderSubscription<AsyncValue<List<Node>>>? nodeSub;
ProviderSubscription<AppSettings>? settingsSub;
ProviderSubscription<String>? selSub;
Timer? poll;
var decided = false;
void finish() {
nodeSub?.close();
settingsSub?.close();
selSub?.close();
poll?.cancel();
}
void tryGo() {
if (decided) return;
final settings = ref.read(settingsProvider);
@@ -142,11 +150,11 @@ class _RootFlowState extends ConsumerState<RootFlow> {
// 否则可能在 effectiveNode 尚是占位/未就绪时就触发,_connect 报「节点尚未就绪」。
// 还要等设置 + 选中节点都从持久化恢复完,否则可能用到未恢复的默认(智能)而非上次选择。
final node = ref.read(effectiveNodeProvider);
logLine('AutoConnect',
'check: settingsLoaded=${settings.loaded} selLoaded=$selLoaded node=${node.code} uuid=${node.uuid.isEmpty ? "EMPTY" : "ok"}');
if (!settings.loaded || !selLoaded || node.uuid.isEmpty) return;
decided = true;
nodeSub?.close();
settingsSub?.close();
selSub?.close();
finish();
final phase = ref.read(connectionProvider).phase;
logLine('AutoConnect', 'ready: autoConnect=${settings.autoConnect} phase=$phase node=${node.code} uuid=${node.uuid.isEmpty ? "EMPTY" : "ok"}');
if (settings.autoConnect && phase == VpnPhase.off) {
@@ -163,6 +171,15 @@ class _RootFlowState extends ConsumerState<RootFlow> {
nodeSub = ref.listenManual(nodesProvider, (_, __) => tryGo());
settingsSub = ref.listenManual(settingsProvider, (_, __) => tryGo());
selSub = ref.listenManual(selectedNodeCodeProvider, (_, __) => tryGo());
// 兜底轮询:有些就绪信号(如选中节点 _load 完成但值未变,不会发通知)不会触发上面的监听,
// 靠定时复查避免「明明都就绪了却没人再调 tryGo」的死锁。最多等 ~12s。
poll = Timer.periodic(const Duration(milliseconds: 400), (t) {
if (decided || t.tick > 30) {
t.cancel();
return;
}
tryGo();
});
tryGo();
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: pangolin_vpn
description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。
publish_to: "none"
version: 1.0.26+27
version: 1.0.27+28
environment:
sdk: ^3.5.0
+1 -1
View File
@@ -3,7 +3,7 @@
; 前置: 先在 client/ 跑 `flutter build windows`(Release),产物在
; ..\..\build\windows\x64\runner\Release
#define MyAppName "穿山甲 Pangolin"
#define MyAppVersion "1.0.26"
#define MyAppVersion "1.0.27"
#define MyAppPublisher "Pangolin"
#define MyAppExeName "pangolin_vpn.exe"
#define BuildDir "..\..\build\windows\x64\runner\Release"