138 lines
4.9 KiB
Dart
138 lines
4.9 KiB
Dart
// 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 = await _pingCheckBatch(lines);
|
||
// 本地线路全部失败,回退 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 也成了 dynamic,record.data.split().map().toList()
|
||
//一路 dynamic 下去,运行期是 List<dynamic>,传给 setStringList(List<String>) 直接抛。
|
||
//全程 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 '';
|
||
}
|
||
}
|