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();
}