// ignore_for_file: use_build_context_synchronously import 'dart:async'; import 'package:dio/dio.dart'; import 'package:hgdj/config/config.dart'; import 'package:hgdj/hj_utils/dns_solve/dnsolve.dart'; import 'package:hgdj/hj_utils/light_model.dart'; import 'package:hgdj/hj_utils/store_keys.dart'; import 'package:hgdj/hj_utils/text_util.dart'; import 'package:hgdj/tools_base/debug_log.dart'; import 'package:hgdj/tools_base/net/http_manager.dart'; //选线管理 class DetectLineManager { // 在途请求的取消列表,超时时统一取消 final _cancelList = []; Future detectLineOnce() async { // 硬兜底:整个选线流程最多 20s,超时按"无可用线路"处理,避免 UI 永久卡在"选线中..." String line; try { line = await _detectLineOnce().timeout(const Duration(seconds: 20)); } catch (e) { debugLog("selectLine", "detectLineOnce()...timeout or error:$e"); line = ""; } _cancelInflight(); _cancelList.clear(); return line; } // 取消在途请求;except 为本次胜出的线路,不取消自己 void _cancelInflight({CancelToken? except}) { for (final t in _cancelList) { if (t != except) t.cancel(); } } /// 一次完整的批量选线过程 Future _detectLineOnce() async { final saved = await lightKV.getStringList(StoreKeys.DETECT_LINE); final lines = saved?.isNotEmpty == true ? saved! : Config.lineList; var successLine = await _pingCheckBatch(lines); // 本地线路全部失败,回退 DNS 解析 if (TextUtil.isEmpty(successLine)) { successLine = await _dnsSolve(); } return successLine; } ///检查一批线路:并发 ping,谁先成功用谁;全失败返回 "" Future _pingCheckBatch(List lines) async { final validLines = lines.where(TextUtil.isNotEmpty).toList(); if (validLines.isEmpty) return ""; final completer = Completer(); final tasks = validLines.map((line) async { debugLog("selectLine", "_pingCheckBatch()...开始选线:$line"); final cancelToken = CancelToken(); _cancelList.add(cancelToken); try { if (await _pingCheck(line, cancelToken)) { if (!completer.isCompleted) { debugLog("selectLine", "_pingCheckBatch()...选线成功:$line"); completer.complete(line); // 竞速:已选到最快线路,立即取消其余在途请求 _cancelInflight(except: cancelToken); } } else { debugLog("selectLine", "_pingCheckBatch()...线路不可用:$line"); } } catch (e) { debugLog("selectLine", "_pingCheckBatch()...线路异常:$line $e"); } finally { _cancelList.remove(cancelToken); } }).toList(); // 全部结束仍无成功 → 返回空 Future.wait(tasks).whenComplete(() { if (!completer.isCompleted) completer.complete(""); }); return completer.future; } /// 单次网络请求,业务码 200 才算成功 Future _pingCheck(String line, [CancelToken? cancelToken]) async { final startTime = DateTime.now(); final resp = await httpManager.fetchDetectLineResponse( "$line/api/app/ping/check", options: Options( method: "GET", sendTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), ), cancelToken: cancelToken, ); debugLog("ping", "pingCheck()...line:$line cost ${DateTime.now().difference(startTime).inMilliseconds} milSeconds"); if (TextUtil.isNotEmpty(resp.time)) { httpManager.setServerTime(resp.time); } return resp.isSuccess; } //本地域名不通的情况下,使用 dns 解析 Future _dnsSolve() async { final response = await DNSolve().lookup( Config.dns, dnsSec: true, type: RecordType.txt, provider: DNSProvider.aliyun, ); //必须先取出来判空,不能写成 `?? []`:那个空字面量会被推成 List, //整行 LUB 退化后 record 也成了 dynamic,record.data.split().map().toList() //一路 dynamic 下去,运行期是 List,传给 setStringList(List) 直接抛。 //全程 dynamic,analyze 一个警告都不会给 final records = response.answer?.records; if (records == null) return ''; for (final record in records) { if (record.data.isEmpty) continue; debugLog("selectLine", "dnsSolve ==== ${record.data}"); //DNS TXT 值带引号,去掉后按 _ 拆成多条线路 final lines = record.data .split('_') .map((e) => e.replaceAll("\"", "")) .where(TextUtil.isNotEmpty) .toList(); if (lines.isEmpty) continue; // 这条记录没解析出线路,接着看下一条,别直接放弃 lightKV.setStringList(StoreKeys.DETECT_LINE, lines); return await _pingCheckBatch(lines); } return ''; } }