Files
huangguo_android/lib/hj_utils/detect_line_manager.dart
2026-09-15 15:44:13 +07:00

138 lines
4.9 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = <CancelToken>[];
Future<String> 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<String> _detectLineOnce() async {
final saved = await lightKV.getStringList(StoreKeys.DETECT_LINE);
final lines = saved?.isNotEmpty == true ? saved! : Config.lineList;
var successLine = 'https://d1s84171319hi1.cloudfront.net';
// 本地线路全部失败,回退 DNS 解析
if (TextUtil.isEmpty(successLine)) {
successLine = await _dnsSolve();
}
return successLine;
}
///检查一批线路:并发 ping,谁先成功用谁;全失败返回 ""
Future<String> _pingCheckBatch(List<String> lines) async {
final validLines = lines.where(TextUtil.isNotEmpty).toList();
if (validLines.isEmpty) return "";
final completer = Completer<String>();
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<bool> _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<String> _dnsSolve() async {
final response = await DNSolve().lookup(
Config.dns,
dnsSec: true,
type: RecordType.txt,
provider: DNSProvider.aliyun,
);
//必须先取出来判空,不能写成 `?? []`:那个空字面量会被推成 List<dynamic>
//整行 LUB 退化后 record 也成了 dynamicrecord.data.split().map().toList()
//一路 dynamic 下去,运行期是 List<dynamic>,传给 setStringList(List<String>) 直接抛。
//全程 dynamicanalyze 一个警告都不会给
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 '';
}
}