hi-client/lib/app/services/singbox_imp/kr_sing_box_imp.dart
2026-01-07 17:33:55 -08:00

3233 lines
136 KiB
Dart
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'dart:ffi';
// ✅ dart:isolate 已移除(原用于 DNS 恢复的 Isolate现由 libcore 管理)
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fpdart/fpdart.dart';
import 'package:get/get.dart';
import 'package:synchronized/synchronized.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
import 'package:kaer_with_panels/singbox/service/singbox_service_provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import '../../../core/model/directories.dart';
import '../../../singbox/model/singbox_config_option.dart';
import '../../../singbox/model/singbox_outbound.dart';
import '../../../singbox/model/singbox_stats.dart';
import '../../../singbox/model/singbox_status.dart';
import '../../utils/kr_country_util.dart';
import '../../utils/kr_log_util.dart';
import '../../utils/kr_file_logger.dart';
import '../../utils/kr_secure_storage.dart';
// ✅ DNS 由 libcore.dll 自动管理,已移除 kr_windows_dns_util.dart
import '../../utils/kr_windows_process_util.dart';
import '../../common/app_run_data.dart';
import 'package:flutter/foundation.dart';
enum KRConnectionType {
global,
rule,
// direct,
}
// 🔧 P0-1: 订阅类型标识 - 用于精确管理订阅
class _SubscriptionType {
static const String groups = 'groups';
static const String stats = 'stats';
static const String status = 'status';
}
// 🔧 _WindowsProxyRestoreWorker 类已删除 - 改用 WinINet API 直接在主线程执行(无需 Isolate
// 优势:
// ✅ 毫秒级速度(无进程启动开销)
// ✅ 无黑窗问题
// ✅ 代码更简洁
class KRSingBoxImp {
/// 私有构造函数
KRSingBoxImp._();
/// 单例实例
static final KRSingBoxImp _instance = KRSingBoxImp._();
/// 工厂构造函数
factory KRSingBoxImp() => _instance;
/// 获取实例的静态方法
static KRSingBoxImp get instance => _instance;
/// 配置文件目录
late Directories kr_configDics;
/// 配置文件名称
String kr_configName = "hiFastVPN";
/// 存储键:用户选择的节点
static const String _keySelectedNode = 'SELECTED_NODE_TAG';
/// 通道方法
final _kr_methodChannel = const MethodChannel("com.hi.app/platform");
final _kr_container = ProviderContainer();
/// 核心服务
late SingboxService kr_singBox;
/// more配置
Map<String, dynamic> kr_configOption = {};
List<Map<String, dynamic>> kr_outbounds = [];
/// 首次启动
RxBool kr_isFristStart = false.obs;
/// 状态
final kr_status = SingboxStatus.stopped().obs;
/// 拦截广告
final kr_blockAds = false.obs;
/// 是否自动自动选择线路
final kr_isAutoOutbound = false.obs;
/// 连接类型
final kr_connectionType = KRConnectionType.rule.obs;
String _cutPath = "";
/// 端口
int kr_port = 51213;
// Windows 下 old command server 使用固定 TCP 端口libbox hard-coded: 127.0.0.1:8964
static const int _krWindowsCommandServerPort = 8964;
/// 统计
final kr_stats = SingboxStats(
connectionsIn: 0,
connectionsOut: 0,
uplink: 0,
downlink: 0,
uplinkTotal: 0,
downlinkTotal: 0,
).obs;
/// 活动的出站分组
RxList<SingboxOutboundGroup> kr_activeGroups = <SingboxOutboundGroup>[].obs;
// ✅ Windows 管理员权限检查缓存(避免重复执行系统命令)
bool? _cachedAdminPrivilege;
DateTime? _cachedAdminPrivilegeTime;
static const Duration _adminPrivilegeCacheDuration = Duration(hours: 1);
// ✅ DNS 和系统代理都由 Go 后端 (libcore.dll) 自动管理
// 参考 Hiddify 实现Dart 层不再手动处理 DNS 和代理设置
// sing-box 内部有完整的 DNS 代理机制local-dns-port、remote-dns-address 等)
/// 所有的出站分组
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
/// Stream 订阅管理器
final List<StreamSubscription<dynamic>> _kr_subscriptions = [];
// 🔧 P0-1 + P2-11: 改进订阅管理 - 使用 Map 而非 List防止重复订阅
// P2-11: hashCode 去重不可靠 - 改为使用类型安全的 Map避免基于 hashCode.toString().contains() 的脆弱判断
/// 精确的订阅管理Map
final Map<String, StreamSubscription<dynamic>> _subscriptionMap = {};
// ✅ 系统代理操作已移交给 Go 后端 (libcore.dll) 处理
// 不再需要 Dart 层的代理操作互斥锁
/// 初始化标志,防止重复初始化
bool _kr_isInitialized = false;
// 🔧 P1-3: 命令客户端初始化锁,防止并发调用导致重复初始化
final Lock _commandClientInitLock = Lock();
// 🔧 防止 kr_start() 和 kr_stop() 并发执行导致卡顿
// 快速点击VPN开关时通过此 Lock 确保上一个操作完成后才能开始新操作
final Lock _startStopLock = Lock();
// 【关键】停止请求标志
// 用于通知后台任务停止执行,避免竞态条件
// 当 kr_stop() 开始时设置为 true让正在执行或即将执行的后台任务立即返回
bool _stopRequested = false;
// ✅ DNS 相关的方法已移除
// 参考 Hiddify 实现sing-box 内部有完整的 DNS 代理机制
// 通过 local-dns-port、remote-dns-address、direct-dns-address 等配置处理 DNS
/// 当前混合代理端口是否就绪
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
String? _lastProxyRule;
/// 构建 Dart HttpClient 可识别的代理规则字符串
///
/// 当 sing-box 尚未启动时返回 `DIRECT`,启动后返回
/// `PROXY 127.0.0.1:<port>`,可选附加 DIRECT 回落。
String kr_buildProxyRule({bool includeDirectFallback = false}) {
if (!kr_isProxyReady) {
const directRule = 'DIRECT';
if (_lastProxyRule != directRule) {
KRLogUtil.kr_i('⏳ sing-box 未就绪,使用 DIRECT 直连', tag: 'SingBox');
_lastProxyRule = directRule;
}
return directRule;
}
final proxyRule = StringBuffer('PROXY 127.0.0.1:$kr_port');
if (includeDirectFallback) {
proxyRule.write('; DIRECT');
}
final ruleString = proxyRule.toString();
if (_lastProxyRule != ruleString) {
KRLogUtil.kr_i('🛠️ 使用代理规则: $ruleString', tag: 'SingBox');
_lastProxyRule = ruleString;
}
return ruleString;
}
/// 初始化
Future<void> init() async {
// 防止重复初始化
if (_kr_isInitialized) {
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
return;
}
try {
KRLogUtil.kr_i('开始初始化 SingBox');
// 在应用启动时初始化
await KRCountryUtil.kr_init();
KRLogUtil.kr_i('国家工具初始化完成');
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
KRLogUtil.kr_i('配置选项初始化完成');
KRLogUtil.kr_i('开始初始化 SingBox 服务');
kr_singBox = await _kr_container.read(singboxServiceProvider);
await _kr_container.read(singboxServiceProvider).init();
KRLogUtil.kr_i('SingBox 服务初始化完成');
KRLogUtil.kr_i('开始初始化目录');
/// 初始化目录
if (Platform.isIOS) {
final paths = await _kr_methodChannel.invokeMethod<Map>("get_paths");
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
kr_configDics = (
baseDir: Directory(paths?["base"]! as String),
workingDir: Directory(paths?["working"]! as String),
tempDir: Directory(paths?["temp"]! as String),
);
} else {
final baseDir = await getApplicationSupportDirectory();
final workingDir =
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
final tempDir = await getTemporaryDirectory();
// Windows 路径规范化:确保使用正确的路径分隔符
Directory normalizePath(Directory dir) {
if (Platform.isWindows) {
final normalized = dir.path.replaceAll('/', '\\');
if (normalized != dir.path) {
KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized', tag: 'SingBox');
return Directory(normalized);
}
}
return dir;
}
kr_configDics = (
baseDir: normalizePath(baseDir),
workingDir: normalizePath(workingDir!),
tempDir: normalizePath(tempDir),
);
KRLogUtil.kr_i('其他平台路径初始化完成');
}
KRLogUtil.kr_i('开始创建目录');
KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
// 确保所有目录都存在
if (!kr_configDics.baseDir.existsSync()) {
await kr_configDics.baseDir.create(recursive: true);
KRLogUtil.kr_i('已创建 baseDir', tag: 'SingBox');
}
if (!kr_configDics.workingDir.existsSync()) {
await kr_configDics.workingDir.create(recursive: true);
KRLogUtil.kr_i('已创建 workingDir', tag: 'SingBox');
}
if (!kr_configDics.tempDir.existsSync()) {
await kr_configDics.tempDir.create(recursive: true);
KRLogUtil.kr_i('已创建 tempDir', tag: 'SingBox');
}
// 创建 libcore 数据库所需的 data 目录(在 workingDir 下)
// 注意libcore 的 Setup 会调用 os.Chdir(workingPath),所以 data 目录必须在 workingDir 下
final dataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
// 强制确保 data 目录存在Windows 可能需要多次尝试)
int retryCount = 0;
const maxRetries = 5;
while (!dataDir.existsSync() && retryCount < maxRetries) {
try {
await dataDir.create(recursive: true);
// 等待文件系统同步Windows 上可能需要一点时间)
await Future.delayed(const Duration(milliseconds: 100));
// 验证目录确实创建成功
if (dataDir.existsSync()) {
KRLogUtil.kr_i('✅ 已创建 data 目录: ${dataDir.path}', tag: 'SingBox');
break;
} else {
retryCount++;
KRLogUtil.kr_i('⚠️ data 目录创建后验证失败,重试 $retryCount/$maxRetries', tag: 'SingBox');
await Future.delayed(const Duration(milliseconds: 200));
}
} catch (e) {
retryCount++;
KRLogUtil.kr_e('❌ 创建 data 目录失败 (尝试 $retryCount/$maxRetries): $e', tag: 'SingBox');
if (retryCount >= maxRetries) {
throw Exception('无法创建 libcore 数据库目录: ${dataDir.path},错误: $e');
}
final delayMs = 200 * retryCount;
await Future.delayed(Duration(milliseconds: delayMs));
}
}
if (!dataDir.existsSync()) {
final error = 'data 目录不存在: ${dataDir.path}';
KRLogUtil.kr_e('$error', tag: 'SingBox');
throw Exception(error);
}
// 验证目录权限(尝试创建一个测试文件)
try {
final testFile = File(p.join(dataDir.path, '.test_write'));
await testFile.writeAsString('test');
await testFile.delete();
KRLogUtil.kr_i('✅ data 目录写入权限验证通过', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('⚠️ data 目录写入权限验证失败: $e', tag: 'SingBox');
// 不抛出异常,让 libcore 自己处理
}
// 在 Windows 上额外等待,确保文件系统操作完成
if (Platform.isWindows) {
await Future.delayed(const Duration(milliseconds: 300));
KRLogUtil.kr_i('⏳ Windows 文件系统同步等待完成', tag: 'SingBox');
}
// 最终验证:在 setup() 之前再次确认 workingDir 和 data 目录都存在且可访问
// libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data"
// 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败
if (!kr_configDics.workingDir.existsSync()) {
final error = '❌ workingDir 不存在,无法调用 setup(): ${kr_configDics.workingDir.path}';
KRLogUtil.kr_e(error, tag: 'SingBox');
throw Exception(error);
}
// 验证 workingDir 可读可写
try {
final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
await testWorkingFile.writeAsString('test');
await testWorkingFile.delete();
KRLogUtil.kr_i('✅ workingDir 写入权限验证通过', tag: 'SingBox');
} catch (e) {
final error = '❌ workingDir 无写入权限: ${kr_configDics.workingDir.path}, 错误: $e';
KRLogUtil.kr_e(error, tag: 'SingBox');
throw Exception(error);
}
final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
if (!finalDataDir.existsSync()) {
KRLogUtil.kr_e('❌ 最终验证失败data 目录不存在', tag: 'SingBox');
KRLogUtil.kr_e('路径: ${finalDataDir.path}', tag: 'SingBox');
KRLogUtil.kr_e('workingDir 是否存在: ${kr_configDics.workingDir.existsSync()}', tag: 'SingBox');
if (kr_configDics.workingDir.existsSync()) {
try {
final workingDirContents = kr_configDics.workingDir.listSync();
KRLogUtil.kr_e('workingDir 内容: ${workingDirContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('无法列出 workingDir 内容: $e', tag: 'SingBox');
}
}
throw Exception('data 目录在 setup() 前验证失败: ${finalDataDir.path}');
}
// 再次尝试写入测试,确保目录确实可用
try {
final verifyFile = File(p.join(finalDataDir.path, '.verify_setup'));
await verifyFile.writeAsString('verify');
await verifyFile.delete();
KRLogUtil.kr_i('✅ setup() 前最终验证通过', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ setup() 前最终验证失败: $e', tag: 'SingBox');
// 不抛出异常,让 setup() 自己处理
}
final configsDir = Directory(p.join(kr_configDics.workingDir.path, "configs"));
if (!configsDir.existsSync()) {
try {
await configsDir.create(recursive: true);
KRLogUtil.kr_i('✅ 已创建 configs 目录: ${configsDir.path}', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('⚠️ configs 目录创建失败: $e', tag: 'SingBox');
// 不抛出异常,继续初始化
}
} else {
KRLogUtil.kr_i('✅ configs 目录已存在: ${configsDir.path}', tag: 'SingBox');
}
// 特别处理 extensionData.db 文件 (Windows特定)
if (Platform.isWindows) {
try {
final extensionDataDbPath = p.join(finalDataDir.path, 'extensionData.db');
KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath', tag: 'SingBox');
// 确保 extensionData.db 的父目录存在
final extensionDataParent = Directory(p.dirname(extensionDataDbPath));
if (!extensionDataParent.existsSync()) {
await extensionDataParent.create(recursive: true);
KRLogUtil.kr_i('✅ 已创建 extensionData.db 父目录', tag: 'SingBox');
}
// 测试文件创建权限
final testFile = File(p.join(extensionDataParent.path, '.test_extension'));
await testFile.writeAsString('test');
await testFile.delete();
KRLogUtil.kr_i('✅ extensionData 目录权限验证通过', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('⚠️ extensionData 目录处理失败: $e', tag: 'SingBox');
// 不抛出异常,继续初始化
}
}
KRLogUtil.kr_i('✅ 目录创建完成', tag: 'SingBox');
KRLogUtil.kr_i('开始设置 SingBox', tag: 'SingBox');
KRLogUtil.kr_i(' - baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(' - workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}', tag: 'SingBox');
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox');
// 在 Windows 上,列出 data 目录内容(如果有文件)
if (Platform.isWindows && finalDataDir.existsSync()) {
try {
final dataContents = finalDataDir.listSync();
if (dataContents.isNotEmpty) {
KRLogUtil.kr_i(' - data 目录现有文件: ${dataContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox');
} else {
KRLogUtil.kr_i(' - data 目录为空', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_e(' - 无法列出 data 目录内容: $e', tag: 'SingBox');
}
}
KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox');
// Windows 特定:验证路径格式是否正确
if (Platform.isWindows) {
final workingPath = kr_configDics.workingDir.path;
if (workingPath.contains('/')) {
KRLogUtil.kr_e('⚠️ 警告Windows 路径包含正斜杠,可能导致问题: $workingPath', tag: 'SingBox');
}
// 确保路径使用反斜杠Windows 标准)
final normalizedPath = workingPath.replaceAll('/', '\\');
if (normalizedPath != workingPath) {
KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath', tag: 'SingBox');
}
}
// 🔑 关键步骤:调用 setup() 将 NativePort 传递给 libcore
// 这样 libcore 才能通过 GoDart_PostCObject() 向 Dart 发送消息
await KRFileLogger.log('[黑屏调试] ⏳ 准备调用 FFI setup()...');
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
final setupStartTime = DateTime.now();
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
final setupDuration = DateTime.now().difference(setupStartTime).inMilliseconds;
await KRFileLogger.log('[黑屏调试] ✅ FFI setup() 完成: 耗时 ${setupDuration}ms');
setupResult.match(
(error) async {
await KRFileLogger.log('[黑屏调试] ❌ FFI setup() 失败: $error');
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
throw Exception('FFI setup 失败: $error');
},
(_) {
KRLogUtil.kr_i('✅ setup() 成功FFI 端口已注册', tag: 'SingBox');
},
);
KRLogUtil.kr_i('✅ SingBox 初始化完成');
// 🔧 提取 geosite 文件到 workingDir
KRLogUtil.kr_i('📦 开始提取 geosite 文件...', tag: 'SingBox');
await _kr_extractGeositeFiles();
KRLogUtil.kr_i('✅ geosite 文件提取完成', tag: 'SingBox');
_kr_isInitialized = true;
// 🔑 关键:在初始化完成后立即订阅状态变化流
// 这样可以确保 UI 始终与 libcore 的实际状态同步
_kr_subscribeToStatus();
// ✅ DNS 由 libcore.dll 自动管理,无需手动备份
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
} catch (e, stackTrace) {
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
// 添加额外的诊断信息
if (Platform.isWindows) {
try {
final workingDir = kr_configDics.workingDir;
final dataDir = Directory(p.join(workingDir.path, 'data'));
final configsDir = Directory(p.join(workingDir.path, 'configs'));
KRLogUtil.kr_e('🔍 Windows 路径诊断信息:', tag: 'SingBox');
KRLogUtil.kr_e(' - workingDir: ${workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_e(' - workingDir 存在: ${workingDir.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_e(' - data 目录: ${dataDir.path}', tag: 'SingBox');
KRLogUtil.kr_e(' - data 目录存在: ${dataDir.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_e(' - configs 目录: ${configsDir.path}', tag: 'SingBox');
KRLogUtil.kr_e(' - configs 目录存在: ${configsDir.existsSync()}', tag: 'SingBox');
// 检查父目录内容
if (workingDir.existsSync()) {
try {
final contents = workingDir.listSync();
KRLogUtil.kr_e(' - workingDir 内容: ${contents.map((e) => "${e.path.split(Platform.pathSeparator).last}${e is Directory ? "/" : ""}").join(", ")}', tag: 'SingBox');
} catch (listErr) {
KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr', tag: 'SingBox');
}
}
} catch (diagErr) {
KRLogUtil.kr_e(' - 诊断信息收集失败: $diagErr', tag: 'SingBox');
}
}
// 如果初始化失败,允许下次重试
_kr_isInitialized = false;
rethrow;
}
}
/// 提取 geosite 文件到 workingDir
Future<void> _kr_extractGeositeFiles() async {
try {
// 创建 geosite 目录
final geositeDir = Directory(p.join(kr_configDics.workingDir.path, 'geosite'));
if (!geositeDir.existsSync()) {
await geositeDir.create(recursive: true);
KRLogUtil.kr_i('✅ 已创建 geosite 目录: ${geositeDir.path}', tag: 'SingBox');
}
// 需要提取的文件列表
final files = ['geoip-cn.srs', 'geosite-cn.srs'];
for (final filename in files) {
final assetPath = 'assets/geosite/$filename';
final targetPath = p.join(geositeDir.path, filename);
final targetFile = File(targetPath);
// 检查文件是否已存在
if (targetFile.existsSync()) {
final fileSize = await targetFile.length();
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox');
continue;
}
// 从 assets 加载文件
KRLogUtil.kr_i('📥 正在提取 $filename...', tag: 'SingBox');
final byteData = await rootBundle.load(assetPath);
final bytes = byteData.buffer.asUint8List();
// 写入文件系统
await targetFile.writeAsBytes(bytes);
final writtenSize = await targetFile.length();
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox');
}
KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox');
} catch (e, stackTrace) {
KRLogUtil.kr_e('❌ 提取 geosite 文件失败: $e', tag: 'SingBox');
KRLogUtil.kr_e('堆栈: $stackTrace', tag: 'SingBox');
// 不抛出异常,让应用继续运行(使用远程规则集作为后备)
}
}
Map<String, dynamic> _getConfigOption() {
// 不使用缓存,每次都重新生成配置
// if (kr_configOption.isNotEmpty) {
// return kr_configOption;
// }
// 🔧 关键修复全局代理模式下region 强制设为 'other'libcore 就不会生成国家直连规则
final String effectiveRegion;
if (kr_connectionType.value == KRConnectionType.global) {
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other所有流量走代理', tag: 'SingBox');
} else {
effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox');
}
final useWindowsDnsDefaults = Platform.isWindows;
final remoteDnsAddress = useWindowsDnsDefaults
? 'udp://1.1.1.1'
: 'https://dns.google/dns-query';
final directDnsAddress = useWindowsDnsDefaults
? (effectiveRegion == 'cn' ? 'udp://223.5.5.5' : 'udp://1.1.1.1')
: 'local';
final dnsDomainStrategy = useWindowsDnsDefaults ? '' : 'prefer_ipv4';
const enableDnsRouting = true;
final op = {
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
"use-xray-core-when-possible": false,
"execute-config-as-is": false, // ✅ 按 hiddify-app 方式交给 libcore 生成完整配置
"log-level": "info", // 调试阶段使用 info生产环境改为 warn
"resolve-destination": false,
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
"remote-dns-address": remoteDnsAddress, // 使用 Google DoH避免中转节点 DNS 死锁
"remote-dns-domain-strategy": dnsDomainStrategy,
"direct-dns-address": directDnsAddress, // 使用系统 DNS确保中转服务器域名能被解析
"direct-dns-domain-strategy": dnsDomainStrategy,
"mixed-port": kr_port,
"tproxy-port": kr_port,
"local-dns-port": 36450,
"tun-implementation": "gvisor",
"mtu": 9000,
"strict-route": true,
"connection-test-url": "http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点
"url-test-interval": 30,
"enable-clash-api": true,
"clash-api-port": 36756,
// 🔧 关键改动:根据连接类型选择使用 TUN 还是系统代理
// 全局代理(global) → 启用 TUN 模式强制代理Chrome 插件无法劫持)
// 智能代理(rule) → 使用系统代理(智能分流)
// ✅ 参考 HiddifyWindows/macOS/Linux 全局模式都启用 TUN
"enable-tun": (Platform.isIOS || Platform.isAndroid) ||
((Platform.isWindows || Platform.isMacOS || Platform.isLinux) && kr_connectionType.value == KRConnectionType.global),
"enable-tun-service": false,
"set-system-proxy":
(Platform.isWindows || Platform.isLinux || Platform.isMacOS) &&
kr_connectionType.value != KRConnectionType.global,
"bypass-lan": false,
"allow-connection-from-lan": false,
"enable-fake-dns": false,
"enable-dns-routing": enableDnsRouting,
"independent-dns-cache": true,
"rules": [
// ✅ 自定义域名直连规则 - 添加到 HiddifyOptions.Rules 中
// 🔧 修复: 空规则列表,避免 "missing conditions" 错误
// 如果需要添加规则,必须确保所有必需字段都存在
],
"mux": {
"enable": false,
"padding": false,
"max-streams": 8,
"protocol": "h2mux"
},
"tls-tricks": {
"enable-fragment": false,
"fragment-size": "10-30",
"fragment-sleep": "2-8",
"mixed-sni-case": false,
"enable-padding": false,
"padding-size": "1-1500"
},
"warp": {
"enable": false,
"mode": "proxy_over_warp",
"wireguard-config": "",
"license-key": "",
"account-id": "",
"access-token": "",
"clean-ip": "auto",
"clean-port": 0,
"noise": "1-3",
"noise-size": "10-30",
"noise-delay": "10-30",
"noise-mode": "m4"
},
"warp2": {
"enable": false,
"mode": "proxy_over_warp",
"wireguard-config": "",
"license-key": "",
"account-id": "",
"access-token": "",
"clean-ip": "auto",
"clean-port": 0,
"noise": "1-3",
"noise-size": "10-30",
"noise-delay": "10-30",
"noise-mode": "m4"
}
};
kr_configOption = op;
// 🔧 调试日志:确认自定义规则已添加
final rules = op["rules"] as List?;
KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', tag: 'SingBox');
if (rules != null && rules.isNotEmpty) {
for (var rule in rules) {
final ruleMap = rule as Map<String, dynamic>;
KRLogUtil.kr_i(' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', tag: 'SingBox');
}
}
return op;
}
List<Map<String, dynamic>> _kr_buildHiddifyRules() {
final rules = <Map<String, dynamic>>[];
rules.add({"domains": "domain:api.hifast.biz", "outbound": "bypass"});
final nodeDomains = _kr_collectNodeDomains();
for (final d in nodeDomains) {
rules.add({"domains": "domain:$d", "outbound": "bypass"});
}
KRLogUtil.kr_i('✅ 节点域名白名单数量: ${nodeDomains.length}', tag: 'SingBox');
KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}',
tag: 'SingBox');
return rules;
}
Set<String> _kr_collectNodeDomains() {
final set = <String>{};
void addFromOutbound(Map<String, dynamic> o) {
final server = o['server']?.toString();
if (server != null &&
server.isNotEmpty &&
InternetAddress.tryParse(server) == null) {
set.add(server.toLowerCase());
}
final tls = o['tls'];
if (tls is Map) {
final sni = tls['server_name']?.toString();
if (sni != null &&
sni.isNotEmpty &&
InternetAddress.tryParse(sni) == null) {
set.add(sni.toLowerCase());
}
}
}
for (final g in kr_outbounds) {
addFromOutbound(g);
}
return set;
}
/// 订阅状态变化流
/// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI
void _kr_subscribeToStatus() {
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 _kr_subscribeToStatus 被调用,重新订阅状态流');
}
KRLogUtil.kr_i('🔵 _kr_subscribeToStatus 被调用', tag: 'SingBox');
// 取消之前的状态订阅
for (var sub in _kr_subscriptions) {
if (sub.hashCode.toString().contains('Status')) {
sub.cancel();
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 已取消旧的状态订阅');
}
}
}
_kr_subscriptions
.removeWhere((sub) => sub.hashCode.toString().contains('Status'));
_kr_subscriptions.add(
kr_singBox.watchStatus().listen(
(status) {
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 收到 Native 状态更新: ${status.runtimeType}');
}
KRLogUtil.kr_i('📡 收到状态更新: $status', tag: 'SingBox');
kr_status.value = status;
// ✅ 系统代理由 Go 后端 (libcore.dll) 自动管理
// libcore 在 stop() 时会自动恢复系统代理,无需在 Dart 层处理
},
onError: (error) {
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 状态流错误: $error');
}
KRLogUtil.kr_e('📡 状态流错误: $error', tag: 'SingBox');
},
cancelOnError: false,
),
);
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 状态流订阅完成');
}
}
/// 订阅统计数据流
/// 🔧 UI阻塞修复改为异步方法在FFI调用前让出控制权
Future<void> _kr_subscribeToStats() async {
// 🔧 P0-1: 先取消旧的 stats 订阅
_subscriptionMap[_SubscriptionType.stats]?.cancel();
// 移除列表中的旧订阅(向后兼容)
for (var sub in _kr_subscriptions) {
if (sub.hashCode.toString().contains('Stats')) {
sub.cancel();
}
}
_kr_subscriptions
.removeWhere((sub) => sub.hashCode.toString().contains('Stats'));
// 🔧 关键修复在调用同步FFI之前让出控制权给UI一帧渲染时间
await Future.delayed(Duration.zero);
// ⚠️ 关键watchStats() 内部会调用 FFI startCommandClient
// 如果此时 command.sock 未就绪,会抛出异常
// 所以外层必须有 try-catch
final stream = kr_singBox.watchStats();
final subscription = stream.listen(
(stats) {
kr_stats.value = stats;
},
onError: (error) {
KRLogUtil.kr_e('统计数据监听错误: $error', tag: 'SingBox');
},
cancelOnError: false,
);
// 🔧 P0-1: 使用 Map 精确管理订阅
_subscriptionMap[_SubscriptionType.stats] = subscription;
_kr_subscriptions.add(subscription);
KRLogUtil.kr_i('✅ Stats 订阅已建立', tag: 'SingBox');
}
/// 订阅分组数据流
/// 🔧 UI阻塞修复改为异步方法在FFI调用前让出控制权
Future<void> _kr_subscribeToGroups() async {
KRFileLogger.log('[黑屏调试] [_kr_subscribeToGroups] 🚀 开始订阅分组数据流');
// 🔧 P0-1: 先取消旧的 groups 订阅
_subscriptionMap[_SubscriptionType.groups]?.cancel();
// 移除列表中的旧订阅(向后兼容)
for (var sub in _kr_subscriptions) {
if (sub.hashCode.toString().contains('Groups')) {
sub.cancel();
}
}
_kr_subscriptions
.removeWhere((sub) => sub.hashCode.toString().contains('Groups'));
// 🔧 关键修复在调用同步FFI之前让出控制权给UI一帧渲染时间
await Future.delayed(Duration.zero);
// 订阅活动分组
final activeGroupsSubscription = kr_singBox.watchActiveGroups().listen(
(groups) {
KRFileLogger.log('[黑屏调试] [watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
kr_activeGroups.value = groups;
// 详细打印每个组的信息
for (int i = 0; i < groups.length; i++) {
final group = groups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
}
}
KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox');
},
onError: (error) {
KRFileLogger.log('[黑屏调试] [watchActiveGroups] ❌ 活动分组监听错误: $error');
KRLogUtil.kr_e('❌ 活动分组监听错误: $error', tag: 'SingBox');
},
cancelOnError: false,
);
_kr_subscriptions.add(activeGroupsSubscription);
// 🔧 关键修复在两个FFI调用之间让出控制权避免连续阻塞UI
await Future.delayed(Duration.zero);
// 订阅所有分组
final allGroupsSubscription = kr_singBox.watchGroups().listen(
(groups) {
KRFileLogger.log('[黑屏调试] [watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
kr_allGroups.value = groups;
// 打印每个组的基本信息
for (int i = 0; i < groups.length; i++) {
final group = groups[i];
KRFileLogger.log('[黑屏调试] [watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}');
}
},
onError: (error) {
KRFileLogger.log('[黑屏调试] [watchGroups] ❌ 所有分组监听错误: $error');
KRLogUtil.kr_e('所有分组监听错误: $error');
},
cancelOnError: false,
);
_kr_subscriptions.add(allGroupsSubscription);
// 🔧 P0-1: 使用 Map 精确管理订阅(将两个都归为 'groups' 类型,因为它们共享生命周期)
_subscriptionMap[_SubscriptionType.groups] = activeGroupsSubscription;
KRFileLogger.log('[黑屏调试] [_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}');
KRLogUtil.kr_i('✅ Groups 订阅已建立', tag: 'SingBox');
}
/// 带重试机制的节点选择
///
/// 确保 command.sock 准备好后再执行节点选择
Future<void> _kr_selectOutboundWithRetry(
String groupTag,
String outboundTag, {
int maxAttempts = 3,
int initialDelay = 100,
}) async {
int attempt = 0;
int delay = initialDelay;
while (attempt < maxAttempts) {
attempt++;
KRLogUtil.kr_i(
'🔄 尝试选择节点 $outboundTag (第 $attempt/$maxAttempts 次)',
tag: 'SingBox',
);
// 只在失败后才延迟,首次尝试立即执行
if (attempt > 1) {
await Future.delayed(Duration(milliseconds: delay));
}
try {
await kr_singBox.selectOutbound(groupTag, outboundTag).run();
KRLogUtil.kr_i('✅ 节点选择成功: $outboundTag', tag: 'SingBox');
return;
} catch (e) {
KRLogUtil.kr_w(
'⚠️ 第 $attempt 次节点选择失败: $e',
tag: 'SingBox',
);
if (attempt >= maxAttempts) {
KRLogUtil.kr_e(
'❌ 节点选择失败,已达到最大重试次数',
tag: 'SingBox',
);
throw Exception('节点选择失败: $e');
}
// 指数退避
delay = delay * 2;
}
}
}
/// 带重试机制的命令客户端初始化
///
/// command.sock 需要时间创建,因此需要重试机制
/// maxAttempts: 最大重试次数默认10次针对macOS优化
/// initialDelay: 初始延迟毫秒数默认1000ms针对macOS优化
Future<void> _kr_initializeCommandClientsWithRetry({
int maxAttempts = 10,
int initialDelay = 1000,
}) async {
int attempt = 0;
int delay = initialDelay;
while (attempt < maxAttempts) {
attempt++;
KRLogUtil.kr_i(
'🔄 尝试初始化命令客户端 (第 $attempt/$maxAttempts 次,延迟 ${delay}ms)',
tag: 'SingBox',
);
await Future.delayed(Duration(milliseconds: delay));
try {
// 先验证 command.sock 是否可访问
final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock'));
KRLogUtil.kr_i('🔍 检查 socket 文件: ${socketFile.path}', tag: 'SingBox');
if (!socketFile.existsSync()) {
throw Exception('command.sock 文件不存在: ${socketFile.path}');
}
KRLogUtil.kr_i('✅ socket 文件存在,开始订阅...', tag: 'SingBox');
// 尝试订阅统计和分组数据
// ⚠️ 关键:分别 try-catch避免一个失败影响另一个
bool statsSubscribed = false;
bool groupsSubscribed = false;
// 🔧 UI阻塞修复订阅方法现在是异步的会在FFI调用前让出控制权
try {
KRLogUtil.kr_i('📊 订阅统计数据流...', tag: 'SingBox');
await _kr_subscribeToStats(); // 🔧 修复使用await调用异步方法
statsSubscribed = true;
KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('⚠️ 统计数据流订阅失败: $e', tag: 'SingBox');
}
try {
KRLogUtil.kr_i('📋 订阅分组数据流...', tag: 'SingBox');
await _kr_subscribeToGroups(); // 🔧 修复使用await调用异步方法
groupsSubscribed = true;
KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('⚠️ 分组数据流订阅失败: $e', tag: 'SingBox');
}
// 至少有一个订阅成功才算初始化成功
if (!statsSubscribed && !groupsSubscribed) {
throw Exception('所有订阅都失败了');
}
// 等待更长时间验证订阅是否成功,并实际接收到数据
KRLogUtil.kr_i('⏳ 等待800ms验证订阅状态...', tag: 'SingBox');
await Future.delayed(const Duration(milliseconds: 800));
// 验证订阅是否真正工作(检查是否有数据流)
if (_kr_subscriptions.isEmpty) {
throw Exception('订阅列表为空command client 未成功连接');
}
KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}', tag: 'SingBox');
return;
} catch (e, stackTrace) {
// 详细记录失败原因和堆栈信息
KRLogUtil.kr_w(
'⚠️ 第 $attempt 次尝试失败',
tag: 'SingBox',
);
KRLogUtil.kr_w('❌ 错误类型: ${e.runtimeType}', tag: 'SingBox');
KRLogUtil.kr_w('❌ 错误信息: $e', tag: 'SingBox');
KRLogUtil.kr_w('📚 错误堆栈: $stackTrace', tag: 'SingBox');
if (attempt >= maxAttempts) {
KRLogUtil.kr_e(
'❌ 命令客户端初始化失败,已达到最大重试次数 ($maxAttempts)',
tag: 'SingBox',
);
KRLogUtil.kr_e('💡 提示: command.sock 可能未准备好或权限不足', tag: 'SingBox');
throw Exception('命令客户端初始化失败: $e');
}
// 指数退避每次失败后延迟翻倍1000ms -> 2000ms -> 4000ms -> 8000ms
delay = delay * 2;
}
}
}
/// 在后台尝试预连接 command client不阻塞启动流程
///
/// 借鉴 Hiddify 的设计思想:
/// - watchStats/watchGroups 是懒加载的,首次调用时才会 startCommandClient()
/// - 但我们可以在后台提前尝试连接,提高成功率
/// - 即使失败也不影响主流程,真正的订阅会在 UI 调用时发生
void _kr_tryPreconnectCommandClientsInBackground() {
KRLogUtil.kr_i('🔄 后台启动 command client 预连接任务...', tag: 'SingBox');
// 使用 Future.microtask 确保不阻塞当前执行
Future.microtask(() async {
try {
// 等待 command.sock 就绪macOS 需要更长时间)
await Future.delayed(const Duration(milliseconds: 1500));
// 检查 socket 文件
final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock'));
if (!socketFile.existsSync()) {
KRLogUtil.kr_w('⚠️ command.sock 尚未创建,预连接取消', tag: 'SingBox');
return;
}
KRLogUtil.kr_i('✅ command.sock 已就绪,尝试预订阅...', tag: 'SingBox');
// 🔧 UI阻塞修复订阅方法现在是异步的会在FFI调用前让出控制权
// 如果失败UI 调用时会重新尝试
try {
await _kr_subscribeToStats(); // 🔧 修复使用await
KRLogUtil.kr_i('✅ 统计流预订阅成功', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('⚠️ 统计流预订阅失败正常UI 调用时会重试): $e', tag: 'SingBox');
}
try {
await _kr_subscribeToGroups(); // 🔧 修复使用await
KRLogUtil.kr_i('✅ 分组流预订阅成功', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('⚠️ 分组流预订阅失败正常UI 调用时会重试): $e', tag: 'SingBox');
}
} catch (e) {
// 静默失败,不影响主流程
KRLogUtil.kr_w('⚠️ 后台预连接任务失败(不影响正常使用): $e', tag: 'SingBox');
}
});
}
/// 确保 command client 已初始化(通过触发订阅来初始化)
///
/// 借鉴 Hiddify:watchGroups() 会在首次调用时自动 startCommandClient()
Future<void> _kr_ensureCommandClientInitialized() async {
// 🔧 P0-1 + P2新: 改进订阅检查 - 检查是否真正有 groups 和 stats 订阅
// 而不是仅检查列表是否非空Status 订阅也会在列表中,容易误判)
final hasGroupsSubscription = _subscriptionMap[_SubscriptionType.groups] != null;
final hasStatsSubscription = _subscriptionMap[_SubscriptionType.stats] != null;
if (hasGroupsSubscription && hasStatsSubscription) {
KRLogUtil.kr_i('✅ Command client 已初始化groups: ✓, stats: ✓)', tag: 'SingBox');
return;
}
KRLogUtil.kr_i('⚠️ Command client 未初始化或订阅丢失,重新初始化...', tag: 'SingBox');
// 🔧 P1-3: 使用 Mutex 保护初始化过程,防止并发调用导致重复初始化(内存泄漏)
await _commandClientInitLock.synchronized(() async {
try {
// 再次检查double-check locking 模式,避免在等待锁期间被其他线程初始化)
final hasGroupsSubscription = _subscriptionMap[_SubscriptionType.groups] != null;
final hasStatsSubscription = _subscriptionMap[_SubscriptionType.stats] != null;
if (hasGroupsSubscription && hasStatsSubscription) {
KRLogUtil.kr_i('✅ Command client 在等待期间已被初始化,跳过重复初始化', tag: 'SingBox');
return;
}
// 🔧 UI阻塞修复订阅方法现在是异步的会在FFI调用前让出控制权
await _kr_subscribeToGroups(); // 🔧 修复使用await
await _kr_subscribeToStats(); // 🔧 修复使用await
// 等待订阅建立完成
await Future.delayed(const Duration(milliseconds: 100));
// 验证订阅是否真的建立了
if (_subscriptionMap[_SubscriptionType.groups] == null ||
_subscriptionMap[_SubscriptionType.stats] == null) {
throw Exception('订阅失败command client 未初始化');
}
KRLogUtil.kr_i('✅ Command client 初始化成功', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ Command client 初始化失败: $e', tag: 'SingBox');
rethrow;
}
});
}
/// 在后台恢复用户保存的节点选择(不阻塞启动流程)
///
/// selectOutbound() 依赖 command client必须延迟执行
void _kr_restoreSavedNodeInBackground() {
KRLogUtil.kr_i('🔄 启动节点恢复后台任务...', tag: 'SingBox');
Future.microtask(() async {
try {
// 等待 command client 初始化
await Future.delayed(const Duration(milliseconds: 2000));
final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto') {
KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox');
try {
await _kr_selectOutboundWithRetry("select", savedNode);
KRLogUtil.kr_i('✅ 节点恢复完成', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e', tag: 'SingBox');
}
} else {
KRLogUtil.kr_i(' 使用默认节点选择 (auto)', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_w('⚠️ 节点恢复后台任务失败: $e', tag: 'SingBox');
}
});
}
/// 监听活动组的详细实现
// Future<void> watchActiveGroups() async {
// try {
// print("开始监听活动组详情...");
// final status = await kr_singBox.status();
// print("服务状态: ${status.toJson()}");
// final outbounds = await kr_singBox.listOutbounds();
// print("出站列表: ${outbounds.toJson()}");
// for (var outbound in outbounds.outbounds) {
// print("出站配置: ${outbound.toJson()}");
// // 检查出站是否活动
// final isActive = await kr_singBox.isOutboundActive(outbound.tag);
// print("出站 ${outbound.tag} 活动状态: $isActive");
// }
// } catch (e, stack) {
// print("监听活动组详情时出错: $e");
// print("错误堆栈: $stack");
// }
// }
/// 保存配置文件
void kr_saveOutbounds(List<Map<String, dynamic>> outbounds) async {
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
// 打印每个节点的详细配置
for (int i = 0; i < outbounds.length; i++) {
final outbound = outbounds[i];
KRLogUtil.kr_i('📋 节点[$i] 配置:', tag: 'SingBox');
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
KRLogUtil.kr_i(' - server_port: ${outbound['server_port']}', tag: 'SingBox');
if (outbound['method'] != null) {
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
}
if (outbound['password'] != null) {
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
}
if (outbound['uuid'] != null) {
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
}
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
}
// ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃
kr_outbounds = outbounds.where((outbound) {
final type = outbound['type'];
if (type == 'hysteria2' || type == 'hysteria') {
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
return false;
}
return true;
}).toList();
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
// Use hiddify-app flow: save only base outbounds.
// libcore builds full config from HiddifyOptions.
final Map<String, dynamic> baseConfig = {
"outbounds": [
if (kr_outbounds.isNotEmpty)
{
"type": "selector",
"tag": "proxy",
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
"default": kr_outbounds[0]['tag'],
},
...kr_outbounds,
],
};
final file = _file(kr_configName);
final temp = _tempFile(kr_configName);
final mapStr = jsonEncode(baseConfig);
KRLogUtil.kr_i('Config length: ${mapStr.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
await file.writeAsString(mapStr);
await temp.writeAsString(mapStr);
_cutPath = file.path;
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
// Align with hiddify-app: send HiddifyOptions before validate.
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
await kr_singBox.changeOptions(oOption).mapLeft((err) {
KRLogUtil.kr_e('Config changeOptions failed before validate: $err', tag: 'SingBox');
}).run();
await kr_singBox
.validateConfigByPath(file.path, temp.path, false)
.mapLeft((err) {
KRLogUtil.kr_e('❌ 保存配置文件失败: $err', tag: 'SingBox');
}).run();
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
}
/// 构建 DNS 规则
List<Map<String, dynamic>> _kr_buildDnsRules() {
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
final rules = <Map<String, dynamic>>[];
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
rules.add({
"rule_set": [
"geoip-$currentCountryCode",
"geosite-$currentCountryCode",
],
"server": "dns-direct"
});
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox');
} else if (kr_connectionType.value == KRConnectionType.global) {
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则所有DNS查询走代理', tag: 'SingBox');
}
return rules;
}
/// 构建路由规则
List<Map<String, dynamic>> _kr_buildRouteRules() {
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
final rules = <Map<String, dynamic>>[];
// 基础规则: DNS 查询走 dns-out
rules.add({
"protocol": "dns",
"outbound": "dns-out"
});
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
rules.add({
"rule_set": [
"geoip-$currentCountryCode",
"geosite-$currentCountryCode",
],
"outbound": "direct"
});
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox');
} else if (kr_connectionType.value == KRConnectionType.global) {
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox');
}
return rules;
}
/// 构建规则集配置
List<Map<String, dynamic>> _kr_buildRuleSets() {
final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode();
final ruleSets = <Map<String, dynamic>>[];
// 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集
// 如果是"全局代理"模式,即使选择了国家也不加载规则集
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
// 检查本地文件是否存在
final geoipFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geoip-$currentCountryCode.srs'));
final geositeFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geosite-$currentCountryCode.srs'));
if (geoipFile.existsSync() && geositeFile.existsSync()) {
// ✅ 使用本地文件
ruleSets.add({
"type": "local",
"tag": "geoip-$currentCountryCode",
"format": "binary",
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
});
ruleSets.add({
"type": "local",
"tag": "geosite-$currentCountryCode",
"format": "binary",
"path": "./geosite/geosite-$currentCountryCode.srs"
});
KRLogUtil.kr_i('✅ 使用本地规则集: $currentCountryCode', tag: 'SingBox');
KRLogUtil.kr_i(' - geoip: ./geosite/geoip-$currentCountryCode.srs', tag: 'SingBox');
KRLogUtil.kr_i(' - geosite: ./geosite/geosite-$currentCountryCode.srs', tag: 'SingBox');
} else {
// ❌ 本地文件不存在,使用远程规则集作为后备
KRLogUtil.kr_w('⚠️ 本地规则集不存在,使用远程规则集', tag: 'SingBox');
KRLogUtil.kr_w(' - geoip 文件存在: ${geoipFile.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_w(' - geosite 文件存在: ${geositeFile.existsSync()}', tag: 'SingBox');
ruleSets.add({
"type": "remote",
"tag": "geoip-$currentCountryCode",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs",
"download_detour": "direct",
"update_interval": "7d"
});
ruleSets.add({
"type": "remote",
"tag": "geosite-$currentCountryCode",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
"download_detour": "direct",
"update_interval": "7d"
});
}
}
return ruleSets;
}
Future<void> kr_start() async {
// 🔧 文件日志:记录 kr_start() 调用 + 时间戳
final clickTime = DateTime.now();
await KRFileLogger.log('[黑屏调试] [DEBUG-START] kr_start() 方法被调用 - $clickTime');
await KRFileLogger.log('[kr_start] ⏱️ 用户点击启动 VPN - $clickTime');
// 🔧 P0-3: 防止重复启动,避免备份被覆盖
if (kr_status.value is SingboxStarted) {
KRLogUtil.kr_w('⚠️ VPN 已在运行,请勿重复启动', tag: 'SingBox');
await KRFileLogger.log('[kr_start] VPN 已在运行,返回');
await KRFileLogger.log('[黑屏调试] [DEBUG-START] VPN 已在运行,直接返回');
return;
}
// 🔧 文件日志:等待 Lock + 时间戳
final beforeLock = DateTime.now();
await KRFileLogger.log('[黑屏调试] [DEBUG-START] 准备等待 Lock - $beforeLock');
await KRFileLogger.log('[kr_start] ⏱️ 开始等待 Lock... - $beforeLock');
await KRFileLogger.log('[黑屏调试] [DEBUG-START] 已打印等待 Lock 日志 - ${DateTime.now()}');
// 🔧 VPN 开关防卡顿:使用 Mutex 确保 kr_start() 和 kr_stop() 不会并发执行
// 快速连续点击 VPN 开关时,此 Lock 会序列化所有操作,防止系统过载
return _startStopLock.synchronized(() async {
final afterLock = DateTime.now();
final lockWaitMs = afterLock.difference(beforeLock).inMilliseconds;
// 🔍 诊断Lock 等待时间检查
if (lockWaitMs > 1000) {
await KRFileLogger.log('[kr_start] 🚨 【关键诊断】Lock 等待超过 1000ms等待 ${lockWaitMs}ms');
await KRFileLogger.log('[kr_start] 🚨 这说明前一个操作kr_stop还没释放 Lock可能被后台任务阻塞');
await KRFileLogger.log('[kr_start] 🚨 可能原因:后台 DNS/代理恢复或者后台订阅任务被阻塞');
} else {
await KRFileLogger.log('[kr_start] ✅ Lock 已获取(等待 ${lockWaitMs}ms正常');
}
await KRFileLogger.log('[kr_start] ✅ Lock 已获取(等待 ${lockWaitMs}ms- $afterLock');
try {
final beforeInternal = DateTime.now();
await _kr_startInternal();
final afterInternal = DateTime.now();
final internalMs = afterInternal.difference(beforeInternal).inMilliseconds;
await KRFileLogger.log('[kr_start] ✅ _kr_startInternal() 完成(耗时 ${internalMs}ms- $afterInternal');
} catch (e) {
await KRFileLogger.log('[kr_start] ❌ _kr_startInternal() 异常: $e - ${DateTime.now()}');
rethrow;
}
});
}
/// 实际的启动逻辑(被 Lock 保护)
Future<void> _kr_startInternal() async {
// ✅ DNS 由 libcore.dll 自动管理
// 再次检查状态Lock 内部二次检查)
if (kr_status.value is SingboxStarted) {
KRLogUtil.kr_i('✅ VPN 已在运行Lock 内检查)', tag: 'SingBox');
return;
}
// 不再手动设置状态libcore 会通过 status stream 自动发送状态更新
try {
// 🔧 修复3: 添加登录状态检查 - 只有已登录用户才能连接VPN
if (!KRAppRunData().kr_isLogin.value) {
KRLogUtil.kr_e('❌ 未登录用户禁止启动VPN连接', tag: 'SingBox');
throw Exception('用户未登录无法启动VPN服务');
}
// ✅ 改进:应用启动时已由 manifest 要求管理员权限UAC 提升)
// 用户同意 UAC 提示后,应用以管理员身份运行,可以直接使用 TUN 模式
// 无需在运行时再做权限检查
if (Platform.isWindows && kr_connectionType.value == KRConnectionType.global) {
KRLogUtil.kr_i('✅ 应用已以管理员身份运行可以使用全局代理模式TUN', tag: 'SingBox');
}
// 🪟 Windows 预检查:避免 command server 固定端口 8964 被占用导致启动失败
// ⚠️ 强制编译标记 - v2.0-lazy-load
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
// 🔧 强制重新生成配置文件(确保最新的路由规则生效)
if (kr_outbounds.isNotEmpty) {
KRLogUtil.kr_i('🔄 启动前强制重新生成配置文件...', tag: 'SingBox');
kr_saveOutbounds(kr_outbounds);
// 等待配置文件写入完成
await Future.delayed(const Duration(milliseconds: 100));
}
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
// ✅ DNS 由 sing-box 内部代理机制处理,不需要手动备份/恢复
// 参考 Hiddify 实现sing-box 有 local-dns-port、remote-dns-address 等 DNS 配置
// 🔑 先尝试停止旧实例,避免 command.sock 冲突
// 注意如果没有旧实例libcore 会报错 "command.sock: no such file",这是正常的
try {
await KRFileLogger.log('[_kr_startInternal] 开始停止旧实例 - ${DateTime.now()}');
await kr_singBox.stop().run();
await Future.delayed(const Duration(milliseconds: 500));
KRLogUtil.kr_i('✅ 已清理旧实例', tag: 'SingBox');
await KRFileLogger.log('[_kr_startInternal] 旧实例已停止 - ${DateTime.now()}');
} catch (e) {
// 预期行为:没有旧实例时会报错,可以忽略
KRLogUtil.kr_i(' 没有运行中的旧实例(正常)', tag: 'SingBox');
await KRFileLogger.log('[_kr_startInternal] 旧实例不存在(正常)- ${DateTime.now()}');
}
// 🔑 关键步骤:在 start 之前必须调用 changeOptions 初始化 HiddifyOptions
// 否则 libcore 的 StartService 会因为 HiddifyOptions == nil 而 panic
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ▶️ 步骤1: 准备 changeOptions...');
KRLogUtil.kr_i('📡 初始化 HiddifyOptions...', tag: 'SingBox');
await KRFileLogger.log('[_kr_startInternal] 开始 changeOptions - ${DateTime.now()}');
await _kr_preflightWindowsCommandServerPort();
final configMap = _getConfigOption();
final oOption = SingboxConfigOption.fromJson(configMap);
// ✅ 调试日志:确认 setSystemProxy 值正确传递给 libcore.dll
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] 📦 set-system-proxy: ${configMap["set-system-proxy"]}');
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] 📦 enable-tun: ${configMap["enable-tun"]}');
KRLogUtil.kr_i('📡 HiddifyOptions 关键配置:', tag: 'SingBox');
KRLogUtil.kr_i(' - set-system-proxy: ${configMap["set-system-proxy"]}', tag: 'SingBox');
KRLogUtil.kr_i(' - enable-tun: ${configMap["enable-tun"]}', tag: 'SingBox');
KRLogUtil.kr_i(' - mixed-port: ${configMap["mixed-port"]}', tag: 'SingBox');
await KRFileLogger.log('[_kr_startInternal] HiddifyOptions: set-system-proxy=${configMap["set-system-proxy"]}, enable-tun=${configMap["enable-tun"]}, mixed-port=${configMap["mixed-port"]}');
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⏳ 步骤2: 调用 changeOptions (libcore.dll)...');
final changeOptionsStartTime = DateTime.now();
final changeResult = await kr_singBox.changeOptions(oOption).run();
final changeOptionsDuration = DateTime.now().difference(changeOptionsStartTime).inMilliseconds;
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ✅ 步骤2完成: changeOptions 耗时 ${changeOptionsDuration}ms');
await KRFileLogger.log('[_kr_startInternal] changeOptions 完成 - ${DateTime.now()}');
changeResult.match(
(error) {
KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ❌ changeOptions 失败: $error');
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
throw Exception('初始化 HiddifyOptions 失败: $error');
},
(_) {
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
},
);
// 检查配置文件是否存在
final configFile = File(_cutPath);
if (await configFile.exists()) {
final configContent = await configFile.readAsString();
KRLogUtil.kr_i('📄 配置文件内容长度: ${configContent.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}', tag: 'SingBox');
} else {
KRLogUtil.kr_w('⚠️ 配置文件不存在: $_cutPath', tag: 'SingBox');
}
// 🔧 修复: 在启动前重新订阅状态流,确保能收到状态更新
_kr_subscribeToStatus();
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⏳ 步骤3: 调用 start (libcore.dll)...');
final startStartTime = DateTime.now();
await KRFileLogger.log('[_kr_startInternal] 🔴 开始 kr_singBox.start() - $startStartTime');
await kr_singBox.start(_cutPath, kr_configName, false).map(
(r) {
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
},
).mapLeft((err) {
KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ❌ start 失败: $err');
KRLogUtil.kr_e('❌ SingBox 启动失败: $err', tag: 'SingBox');
// 不需要手动设置状态libcore 会通过 status stream 自动发送 Stopped 事件
throw err;
}).run();
final startEndTime = DateTime.now();
final startDurationMs = startEndTime.difference(startStartTime).inMilliseconds;
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ✅ 步骤3完成: start 耗时 ${startDurationMs}ms');
await KRFileLogger.log('[_kr_startInternal] ✅ kr_singBox.start() 完成(耗时: ${startDurationMs}ms- $startEndTime');
// 🔍 关键诊断:检查 8964 端口是否真的准备好了!
await KRFileLogger.log('[_kr_startInternal] 🔍 检查 sing-box 的 8964 端口是否真的准备好...');
bool port8964Ready = false;
int connectionAttempts = 0;
final portCheckStartTime = DateTime.now();
// 立即尝试连接,看 sing-box 是否真的启动了
try {
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 500),
onTimeout: () => throw Exception('8964 端口连接超时'),
);
socket.destroy();
port8964Ready = true;
connectionAttempts = 0;
await KRFileLogger.log('[_kr_startInternal] ✅ 8964 端口已准备好(立即连接成功)');
} catch (e) {
// 立即失败,说明 sing-box 还没准备好
connectionAttempts = 1;
final portCheckDuration = DateTime.now().difference(portCheckStartTime).inMilliseconds;
await KRFileLogger.log('[_kr_startInternal] ❌ 【关键发现】8964 端口未准备好!(连接失败:$e');
await KRFileLogger.log('[_kr_startInternal] 🚨 这说明 start() 返回成功,但 sing-box 还在初始化中!');
await KRFileLogger.log('[_kr_startInternal] 🚨 后台任务在 1000ms 后会尝试订阅,极可能失败(这是 UI 卡顿的根本原因!)');
}
// ✅ 系统代理由 Go 后端 (libcore.dll) 自动处理
// 原因:通过 changeOptions() 传递 setSystemProxy: truelibcore 会自动设置系统代理
// 参考 Hiddify 实现Dart 层不再手动调用注册表或 WinINet API
await KRFileLogger.log('[_kr_startInternal] 系统代理由 libcore 自动管理 - ${DateTime.now()}');
// 🍎 macOS 专用:额外设置 SOCKS5 系统代理(让 Telegram 等应用自动走代理)
// 原因libcore 只设置 HTTP/HTTPS 代理,但 Telegram 只检测 SOCKS5 代理
// 参考 Clash同时设置 HTTP + SOCKS5 系统代理
if (Platform.isMacOS && kr_connectionType.value != KRConnectionType.global) {
await _kr_setMacOSSocks5Proxy(true);
}
// 🔴 【方案 A】启动 sing-box 进程健康监测
// 目的:检测 sing-box 是否在启动后 1-2 秒内崩溃
// 背景:日志显示 start() 返回成功,但 1.4 秒后 8964 端口无法连接
// 说明 sing-box 进程启动后崩溃了,导致后续 UI 卡顿
unawaited(_monitorSingBoxProcess());
// ⚠️ 关键修复:在启动成功后立即订阅统计流
// 原因:
// 1. 统计流需要主动订阅才能接收数据
// 2. UI 只是读取 kr_stats.value不会触发订阅
// 3. command.sock 在 start() 成功后会立即创建
KRLogUtil.kr_i('✅ SingBox 核心已启动,开始初始化 command client', tag: 'SingBox');
// 🔑 在后台延迟订阅统计流和分组流,避免阻塞 UI
await KRFileLogger.log('[_kr_startInternal] 🟢 关键路径完成,返回给调用者 - ${DateTime.now()}');
Future.delayed(const Duration(milliseconds: 1000), () async {
final backgroundStartTime = DateTime.now();
// 【关键】检查停止标志,如果用户在延迟期间点击了停止,就立即返回
// 这避免了竞态条件:后台任务在 stop() 过程中仍然在执行订阅
if (_stopRequested) {
await KRFileLogger.log('[后台任务] ⏭️ 检测到停止标志,后台任务立即返回(避免竞态条件) - $backgroundStartTime');
return;
}
try {
await KRFileLogger.log('[后台任务] 🔴 启动 1000ms 延迟后的后台任务 - $backgroundStartTime');
// 订阅统计流
try {
// 🔍 诊断:检查 8964 端口状态
final subscribeCheckStartTime = DateTime.now();
bool port8964ConnectOk = false;
try {
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 200),
onTimeout: () => throw Exception('端口连接超时'),
);
socket.destroy();
port8964ConnectOk = true;
await KRFileLogger.log('[后台任务] 📊 准备订阅前检查8964 端口✅可以连接');
} catch (portE) {
await KRFileLogger.log('[后台任务] 📊 准备订阅前检查8964 端口❌无法连接($portE- 这说明 sing-box 还没准备好!');
}
// 🔧 关键修复:如果端口检查失败,立即返回,避免 FFI 调用阻塞 UI 3+ 秒
if (!port8964ConnectOk) {
await KRFileLogger.log('[后台任务] ⛔ 8964 端口不可用,跳过订阅操作(避免 FFI 超时阻塞 UI- ${DateTime.now()}');
KRLogUtil.kr_w('⚠️ sing-box command 端口不可用,跳过订阅', tag: 'SingBox');
return;
}
// 【关键】在执行订阅前再次检查停止标志
if (_stopRequested) {
await KRFileLogger.log('[后台任务] ⏭️ 停止标志已设置,取消统计订阅 - ${DateTime.now()}');
return;
}
await KRFileLogger.log('[后台任务] 🟡 开始订阅统计数据流 - ${DateTime.now()}');
KRLogUtil.kr_i('📊 开始订阅统计数据流...', tag: 'SingBox');
await _kr_subscribeToStats(); // 🔧 UI阻塞修复使用await确保让出控制权
KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox');
await KRFileLogger.log('[后台任务] ✅ 统计数据流订阅成功 - ${DateTime.now()}');
} catch (e) {
KRLogUtil.kr_w('⚠️ 统计数据流订阅失败(稍后重试): $e', tag: 'SingBox');
await KRFileLogger.log('[后台任务] ❌ 统计数据流订阅失败: $e - ${DateTime.now()}');
await KRFileLogger.log('[后台任务] 🚨 这是导致后续操作失败的关键问题!后台任务被阻塞,导致 Lock 被占用!');
// 如果第一次失败,再等待一段时间重试
Future.delayed(const Duration(milliseconds: 2000), () async {
// 🔧 重试前检查端口可用性,避免 FFI 阻塞
try {
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 200),
onTimeout: () => throw Exception('端口连接超时'),
);
socket.destroy();
} catch (_) {
KRLogUtil.kr_w('⚠️ 重试订阅统计流时端口不可用,跳过', tag: 'SingBox');
return;
}
try {
await _kr_subscribeToStats(); // 🔧 UI阻塞修复使用await
KRLogUtil.kr_i('✅ 统计数据流重试订阅成功', tag: 'SingBox');
} catch (e2) {
KRLogUtil.kr_e('❌ 统计数据流重试订阅失败: $e2', tag: 'SingBox');
}
});
}
// 🔧 关键修复:订阅分组数据流
try {
// 【关键】在执行订阅前再次检查停止标志
if (_stopRequested) {
await KRFileLogger.log('[后台任务] ⏭️ 停止标志已设置,取消分组订阅 - ${DateTime.now()}');
return;
}
await KRFileLogger.log('[后台任务] 🟡 开始订阅分组数据流 - ${DateTime.now()}');
KRLogUtil.kr_i('📋 开始订阅分组数据流...', tag: 'SingBox');
await _kr_subscribeToGroups(); // 🔧 UI阻塞修复使用await确保让出控制权
KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox');
await KRFileLogger.log('[后台任务] ✅ 分组数据流订阅成功 - ${DateTime.now()}');
} catch (e) {
KRLogUtil.kr_w('⚠️ 分组数据流订阅失败(稍后重试): $e', tag: 'SingBox');
await KRFileLogger.log('[后台任务] ❌ 分组数据流订阅失败: $e - ${DateTime.now()}');
// 如果第一次失败,再等待一段时间重试
Future.delayed(const Duration(milliseconds: 2000), () async {
// 🔧 重试前检查端口可用性,避免 FFI 阻塞
try {
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 200),
onTimeout: () => throw Exception('端口连接超时'),
);
socket.destroy();
} catch (_) {
KRLogUtil.kr_w('⚠️ 重试订阅分组流时端口不可用,跳过', tag: 'SingBox');
return;
}
try {
await _kr_subscribeToGroups(); // 🔧 UI阻塞修复使用await
KRLogUtil.kr_i('✅ 分组数据流重试订阅成功', tag: 'SingBox');
} catch (e2) {
KRLogUtil.kr_e('❌ 分组数据流重试订阅失败: $e2', tag: 'SingBox');
}
});
}
// 🔧 关键修复:恢复用户选择的节点
try {
// 【关键】在执行节点恢复前再次检查停止标志
if (_stopRequested) {
await KRFileLogger.log('[后台任务] ⏭️ 停止标志已设置,取消节点恢复 - ${DateTime.now()}');
return;
}
final nodeRestoreStartTime = DateTime.now();
await KRFileLogger.log('[后台任务] 🟡 开始恢复用户选择的节点 - $nodeRestoreStartTime');
final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
if (selectedNode != null && selectedNode.isNotEmpty && selectedNode != 'auto') {
KRLogUtil.kr_i('🎯 恢复用户选择的节点: $selectedNode', tag: 'SingBox');
await KRFileLogger.log('[后台任务] 🟡 找到保存的节点: $selectedNode - ${DateTime.now()}');
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 启动后恢复节点选择: $selectedNode');
}
// 🔧 关键修复:等待活动组准备就绪,而不是固定延迟
// 这是解决"连接后 selected=auto"问题的关键
KRLogUtil.kr_i('⏳ 等待活动组准备就绪...', tag: 'SingBox');
final groupWaitStartTime = DateTime.now();
await KRFileLogger.log('[后台任务] ⏳ 开始等待活动组准备就绪 - $groupWaitStartTime');
int waitCount = 0;
const maxWaitCount = 25; // 最多等待 5 秒 (25 * 200ms)
while (kr_activeGroups.isEmpty && waitCount < maxWaitCount) {
await Future.delayed(const Duration(milliseconds: 200));
waitCount++;
if (waitCount % 5 == 0) {
KRLogUtil.kr_d('⏳ 等待活动组... ($waitCount/$maxWaitCount)', tag: 'SingBox');
await KRFileLogger.log('[后台任务] ⏳ 等待活动组进行中... (${waitCount}/$maxWaitCount, 已耗时 ${DateTime.now().difference(groupWaitStartTime).inMilliseconds}ms) - ${DateTime.now()}');
}
}
if (kr_activeGroups.isEmpty) {
KRLogUtil.kr_w('⚠️ 等待活动组超时,仍尝试恢复节点', tag: 'SingBox');
final groupWaitDuration = DateTime.now().difference(groupWaitStartTime).inMilliseconds;
await KRFileLogger.log('[后台任务] ⚠️ 等待活动组超时 (${groupWaitDuration}ms) - ${DateTime.now()}');
} else {
KRLogUtil.kr_i('✅ 活动组已就绪,数量: ${kr_activeGroups.length}', tag: 'SingBox');
final groupWaitDuration = DateTime.now().difference(groupWaitStartTime).inMilliseconds;
await KRFileLogger.log('[后台任务] ✅ 活动组已就绪,数量: ${kr_activeGroups.length} (耗时 ${groupWaitDuration}ms) - ${DateTime.now()}');
}
// 🔧 关键修复:使用 await 等待节点切换完成
try {
final switchStartTime = DateTime.now();
await KRFileLogger.log('[后台任务] 🟡 开始恢复节点切换: $selectedNode - $switchStartTime');
await kr_selectOutbound(selectedNode);
KRLogUtil.kr_i('✅ 节点已切换到用户选择: $selectedNode', tag: 'SingBox');
final switchDuration = DateTime.now().difference(switchStartTime).inMilliseconds;
await KRFileLogger.log('[后台任务] ✅ 节点切换成功: $selectedNode (耗时 ${switchDuration}ms) - ${DateTime.now()}');
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 节点切换成功: $selectedNode');
}
} catch (e) {
KRLogUtil.kr_e('❌ 节点切换失败: $e', tag: 'SingBox');
await KRFileLogger.log('[后台任务] ❌ 节点切换失败: $e - ${DateTime.now()}');
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 节点切换失败: $e');
}
}
final nodeRestoreDuration = DateTime.now().difference(nodeRestoreStartTime).inMilliseconds;
await KRFileLogger.log('[后台任务] ✅ 节点恢复流程完成(总耗时 ${nodeRestoreDuration}ms- ${DateTime.now()}');
} else {
KRLogUtil.kr_i(' 没有保存的节点选择或为auto使用默认配置', tag: 'SingBox');
await KRFileLogger.log('[后台任务] 没有保存的节点选择或为auto - ${DateTime.now()}');
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 没有保存的节点选择,使用默认');
}
}
} catch (e) {
KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'SingBox');
await KRFileLogger.log('[后台任务] ❌ 恢复节点选择失败: $e - ${DateTime.now()}');
if (kDebugMode) {
KRFileLogger.log('[黑屏调试] 🔵 恢复节点选择失败: $e');
}
}
final backgroundDuration = DateTime.now().difference(backgroundStartTime).inMilliseconds;
await KRFileLogger.log('[后台任务] 🟢 所有后台任务完成(总耗时 ${backgroundDuration}ms- ${DateTime.now()}');
} catch (e) {
KRLogUtil.kr_e('💥 后台任务异常: $e', tag: 'SingBox');
await KRFileLogger.log('[后台任务] 💥 后台任务异常: $e - ${DateTime.now()}');
}
});
} catch (e, stackTrace) {
KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox');
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox');
// 不需要手动设置状态libcore 会自动处理
rethrow;
}
}
/// 停止服务
Future<void> kr_stop() async {
// 🔧 文件日志:记录 kr_stop() 调用 + 时间戳
final clickTime = DateTime.now();
await KRFileLogger.log('[kr_stop] ⏱️ 用户点击关闭 VPN - $clickTime');
// 🔧 文件日志:等待 Lock + 时间戳
final beforeLock = DateTime.now();
await KRFileLogger.log('[kr_stop] ⏱️ 开始等待 Lock... - $beforeLock');
// ❌ 关键修复:添加 Lock 保护,防止 kr_start() 和 kr_stop() 并发执行
// 快速连续点击开关时,此 Lock 会序列化所有操作,防止 UI 堵塞
return _startStopLock.synchronized(() async {
final afterLock = DateTime.now();
final lockWaitMs = afterLock.difference(beforeLock).inMilliseconds;
await KRFileLogger.log('[kr_stop] ✅ Lock 已获取(等待 ${lockWaitMs}ms- $afterLock');
// 【关键】设置停止标志,通知后台任务停止执行
// 这会让正在执行或即将执行的后台任务立即返回,避免竞态条件
_stopRequested = true;
await KRFileLogger.log('[kr_stop] 🔴 设置停止标志,通知后台任务立即停止执行 - ${DateTime.now()}');
try {
final beforeInternal = DateTime.now();
await _kr_stopInternal();
final afterInternal = DateTime.now();
final internalMs = afterInternal.difference(beforeInternal).inMilliseconds;
await KRFileLogger.log('[kr_stop] ✅ _kr_stopInternal() 完成(耗时 ${internalMs}ms- $afterInternal');
} catch (e) {
await KRFileLogger.log('[kr_stop] ❌ _kr_stopInternal() 异常: $e - ${DateTime.now()}');
rethrow;
} finally {
// 在返回之前重置标志,为下一次启动做准备
_stopRequested = false;
await KRFileLogger.log('[kr_stop] 🟢 停止标志已重置,为下一次启动做准备 - ${DateTime.now()}');
}
});
}
/// 实际的停止逻辑(被 Lock 保护)
Future<void> _kr_stopInternal() async {
final stopInternalStartTime = DateTime.now();
await KRFileLogger.log('[_kr_stopInternal] 🔴 _kr_stopInternal 方法开始执行 - $stopInternalStartTime');
// ✅ DNS 由 libcore.dll 自动管理
try {
KRLogUtil.kr_i('🛑 停止 SingBox 服务...', tag: 'SingBox');
// 取消节点选择监控定时器
_nodeSelectionTimer?.cancel();
_nodeSelectionTimer = null;
KRLogUtil.kr_i('✅ 节点选择监控已停止', tag: 'SingBox');
await KRFileLogger.log('[_kr_stopInternal] ✅ 节点选择监控已停止 - ${DateTime.now()}');
await Future.delayed(const Duration(milliseconds: 100));
// 添加超时保护,防止 stop() 调用阻塞
// 🔧 超时时间 10 秒,确保 libcore 完成清理
try {
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⏳ 步骤: 调用 stop (libcore.dll)...');
final stopStartTime = DateTime.now();
await KRFileLogger.log('[_kr_stopInternal] 开始 kr_singBox.stop() - ${DateTime.now()}');
await kr_singBox.stop().run().timeout(
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
onTimeout: () {
KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⚠️ stop 超时(10秒)');
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
return const Left('timeout');
},
);
final stopDuration = DateTime.now().difference(stopStartTime).inMilliseconds;
await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ✅ stop 完成: 耗时 ${stopDuration}ms');
await KRFileLogger.log('[_kr_stopInternal] kr_singBox.stop() 完成 - ${DateTime.now()}');
} catch (e) {
KRLogUtil.kr_w('⚠️ 停止操作失败(可能已经停止): $e', tag: 'SingBox');
await KRFileLogger.log('[_kr_stopInternal] kr_singBox.stop() 异常: $e - ${DateTime.now()}');
// 继续执行清理操作
}
// ✅ DNS 和系统代理都由 Go 后端 (libcore.dll) 自动管理
// 参考 Hiddify 实现sing-box 内部有完整的 DNS 代理机制,不需要手动恢复系统 DNS
await KRFileLogger.log('[_kr_stopInternal] DNS 由 libcore 自动管理 - ${DateTime.now()}');
// 🍎 macOS 专用:清除 SOCKS5 系统代理
if (Platform.isMacOS) {
await _kr_setMacOSSocks5Proxy(false);
}
// 🔧 P0-1: 明确清理 groups 和 stats 订阅
final subscriptionCleanStartTime = DateTime.now();
await KRFileLogger.log('[_kr_stopInternal] 🔴 开始清理订阅 - $subscriptionCleanStartTime');
KRLogUtil.kr_i('🧹 清理 command client 订阅...', tag: 'SingBox');
// 清理 groups 订阅
try {
await KRFileLogger.log('[_kr_stopInternal] 🟡 清理Groups订阅中 - ${DateTime.now()}');
_subscriptionMap[_SubscriptionType.groups]?.cancel();
_subscriptionMap.remove(_SubscriptionType.groups);
KRLogUtil.kr_i('✅ Groups 订阅已取消', tag: 'SingBox');
await KRFileLogger.log('[_kr_stopInternal] ✅ Groups 订阅已取消 - ${DateTime.now()}');
} catch (e) {
KRLogUtil.kr_w('⚠️ Groups 订阅取消失败: $e', tag: 'SingBox');
await KRFileLogger.log('[_kr_stopInternal] ❌ Groups 订阅取消失败: $e - ${DateTime.now()}');
}
// 清理 stats 订阅
try {
await KRFileLogger.log('[_kr_stopInternal] 🟡 清理Stats订阅中 - ${DateTime.now()}');
_subscriptionMap[_SubscriptionType.stats]?.cancel();
_subscriptionMap.remove(_SubscriptionType.stats);
KRLogUtil.kr_i('✅ Stats 订阅已取消', tag: 'SingBox');
await KRFileLogger.log('[_kr_stopInternal] ✅ Stats 订阅已取消 - ${DateTime.now()}');
} catch (e) {
KRLogUtil.kr_w('⚠️ Stats 订阅取消失败: $e', tag: 'SingBox');
await KRFileLogger.log('[_kr_stopInternal] ❌ Stats 订阅取消失败: $e - ${DateTime.now()}');
}
// 从列表中清除非 Status 项(向后兼容)
await KRFileLogger.log('[_kr_stopInternal] 🟡 清理其他订阅中 - ${DateTime.now()}');
final subscriptionsToCancel = _kr_subscriptions.where((sub) {
final hashStr = sub.hashCode.toString();
return !hashStr.contains('Status'); // 不取消状态订阅
}).toList();
for (var subscription in subscriptionsToCancel) {
_kr_subscriptions.remove(subscription);
}
final subscriptionCleanDuration = DateTime.now().difference(subscriptionCleanStartTime).inMilliseconds;
KRLogUtil.kr_i('✅ 订阅清理完成,剩余订阅数: ${_kr_subscriptions.length}', tag: 'SingBox');
await KRFileLogger.log('[_kr_stopInternal] ✅ 订阅清理完成(耗时: ${subscriptionCleanDuration}ms剩余订阅数: ${_kr_subscriptions.length} - ${DateTime.now()}');
// 不手动设置状态,由 libcore 通过 status stream 自动发送 Stopped 事件
KRLogUtil.kr_i('✅ SingBox 停止请求已发送', tag: 'SingBox');
} catch (e, stackTrace) {
KRLogUtil.kr_e('停止服务时出错: $e');
KRLogUtil.kr_e('错误堆栈: $stackTrace');
// ✅ DNS 由 libcore 自动管理,无需手动恢复
// 不手动设置状态,信任 libcore 的状态管理
rethrow;
} finally {
// ✅ 关键修复:系统代理恢复改为异步后台执行,不在 finally 块中 await
// 这样 kr_stop() 可以立即返回,不被注册表操作阻塞
final finallyStartTime = DateTime.now();
await KRFileLogger.log('[_kr_stopInternal] 🔴 finally 块开始执行 - $finallyStartTime');
// ✅ 系统代理恢复由 Go 后端 (libcore.dll) 自动处理
// 原因libcore.stop() 会自动恢复系统代理设置
// 参考 Hiddify 实现Dart 层不再手动调用注册表或 WinINet API
await KRFileLogger.log('[_kr_stopInternal] 系统代理恢复由 libcore 自动管理 - ${DateTime.now()}');
// 最后的诊断日志
final stopInternalEndTime = DateTime.now();
await KRFileLogger.log('[_kr_stopInternal] 🟢 _kr_stopInternal 方法即将退出 - $stopInternalEndTime');
}
}
/// 🪟 WindowsStart 之前检查 old command server 端口占用情况
///
/// ✅ 极限修复:完全跳过 Windows 端口检查!
/// 原因:
/// 1. netstat、powershell、taskkill 都是系统命令,每个 0.5-1 秒
/// 2. 连续调用导致 2-3 秒的累计卡顿
/// 3. 造成 icon 停止转动、整个框架卡死
/// 4. libcore 已经有完整的端口冲突处理逻辑
///
/// 相信 libcore 的自动重试机制,比强制同步检查更可靠!
Future<void> _kr_preflightWindowsCommandServerPort() async {
if (!Platform.isWindows) return;
KRLogUtil.kr_i('⏭️ 跳过 Windows 端口预检查,相信 libcore 的自动处理机制', tag: 'SingBox');
// 完全不做检查,直接返回
// libcore 会自动处理所有的端口冲突情况
}
// ✅ 系统代理设置/恢复已移交给 Go 后端 (libcore.dll) 自动处理
// 参考 Hiddify 实现:通过 changeOptions() 传递 setSystemProxy: true
// libcore 在 start() 时自动设置代理,在 stop() 时自动恢复
// 优势:
// ✅ 无黑窗问题Go 后端内部处理,不执行外部命令)
// ✅ 更快速(无进程启动开销)
// ✅ 更可靠(由 sing-box 核心统一管理)
/// Windows: cleanup fixed command server port on logout.
/// Returns a user-facing warning message if another process owns the port.
Future<String?> kr_cleanupWindowsCommandServerPortOnLogout() async {
if (!Platform.isWindows) return null;
try {
final pidInUse = await _kr_getListeningPidByPort(_krWindowsCommandServerPort);
if (pidInUse == null) return null;
final processName = await _kr_getProcessNameByPid(pidInUse);
final currentExeName = p.basename(Platform.resolvedExecutable).toLowerCase();
final normalizedProcessName = processName?.toLowerCase();
if (normalizedProcessName != null && normalizedProcessName == currentExeName) {
KRLogUtil.kr_w(
'logout cleanup: detected port $_krWindowsCommandServerPort in use by self (pid=$pidInUse), killing',
tag: 'SingBox',
);
final killed = await _kr_killProcess(pidInUse);
if (!killed) {
KRLogUtil.kr_w(
'logout cleanup: failed to kill process (pid=$pidInUse)',
tag: 'SingBox',
);
}
return null;
}
final displayName = processName ?? 'unknown';
return '当前有其他进程占用$_krWindowsCommandServerPort端口,进程名: $displayNamePid: $pidInUse,请手动关闭';
} catch (e) {
KRLogUtil.kr_w('logout cleanup: port check failed: $e', tag: 'SingBox');
return null;
}
}
/// Windows: get the listening PID for a port (LISTEN only).
Future<int?> _kr_getListeningPidByPort(int port) async {
if (!Platform.isWindows) return null;
// Use numeric output to avoid reverse DNS lookups, which can stall when DNS is unstable.
final result = await KRWindowsProcessUtil.runHidden('netstat', ['-ano', '-p', 'tcp', '-n']);
if (result.exitCode != 0) {
KRLogUtil.kr_w('⚠️ netstat 执行失败: ${result.stderr}', tag: 'SingBox');
return null;
}
final output = result.stdout.toString();
final lines = output.split(RegExp(r'\r?\n'));
for (final rawLine in lines) {
final line = rawLine.trim();
if (line.isEmpty) continue;
if (!line.startsWith('TCP')) continue;
final parts = line.split(RegExp(r'\s+'));
// 预期格式: TCP <local> <foreign> <state> <pid>
if (parts.length < 4) continue;
final local = parts.length >= 2 ? parts[1] : '';
final foreign = parts.length >= 3 ? parts[2] : '';
final pidStr = parts.isNotEmpty ? parts.last : '';
// 🔧 P2-1: 改进端口检测精度 - 使用更精确的正则匹配
// 监听行 foreign 一般为 0.0.0.0:0 或 [::]:0避免依赖 LISTENING 文本是否本地化)
final isListeningRow = RegExp(r'(0\.0\.0\.0|::|.*?):\s*0\s*$').hasMatch(foreign);
if (!isListeningRow) continue;
// 精确匹配端口号(不使用字符串 endsWith避免误匹配
// 例如: 127.0.0.1:51213 或 [::1]:51213 或 *:51213
final portPattern = RegExp(r':\s*$port\s*$');
if (!portPattern.hasMatch(local)) continue;
final pidValue = int.tryParse(pidStr.trim());
if (pidValue != null && pidValue > 0) {
KRLogUtil.kr_d(
'🔍 端口 $port 被进程 $pidValue 占用 (local: $local, foreign: $foreign)',
tag: 'SingBox',
);
return pidValue;
}
}
KRLogUtil.kr_d('✅ 端口 $port 未被占用', tag: 'SingBox');
return null;
}
Future<String?> _kr_getProcessNameByPid(int pidValue) async {
if (!Platform.isWindows) return null;
try {
final result = await KRWindowsProcessUtil.runHidden('tasklist', [
'/FI',
'PID eq $pidValue',
'/FO',
'CSV',
'/NH',
]);
if (result.exitCode != 0) {
KRLogUtil.kr_w('⚠️ tasklist 执行失败: ${result.stderr}', tag: 'SingBox');
return null;
}
final stdout = result.stdout.toString().trim();
if (stdout.isEmpty) return null;
if (stdout.startsWith('INFO:')) return null;
// CSV 第一列为 Image Name例如: "BearVPN.exe","1234",...
final firstLine = stdout.split(RegExp(r'\r?\n')).first.trim();
final match = RegExp(r'^"([^"]+)"').firstMatch(firstLine);
return match?.group(1);
} catch (e) {
// 🔧 P2-8: 读/写竞争条件 - 捕获异常,防止进程查询导致的崩溃
KRLogUtil.kr_w('⚠️ 进程信息查询异常: $e', tag: 'SingBox');
return null;
}
}
Future<bool> _kr_killProcess(int pidValue) async {
if (!Platform.isWindows) return false;
final result = await KRWindowsProcessUtil.runHidden('taskkill', [
'/PID',
pidValue.toString(),
'/T',
'/F',
]);
if (result.exitCode == 0) return true;
KRLogUtil.kr_w(
'⚠️ taskkill 失败: exitCode=${result.exitCode}, stderr=${result.stderr}, stdout=${result.stdout}',
tag: 'SingBox',
);
return false;
}
Future<bool> _kr_waitPortFree(int port, {required Duration timeout}) async {
final deadline = DateTime.now().add(timeout);
while (DateTime.now().isBefore(deadline)) {
final pidInUse = await _kr_getListeningPidByPort(port);
if (pidInUse == null) return true;
await Future.delayed(const Duration(milliseconds: 200));
}
return false;
}
///
void kr_updateAdBlockEnabled(bool bl) async {
final oOption = _getConfigOption();
oOption["block-ads"] = bl;
final op = SingboxConfigOption.fromJson(oOption);
await kr_singBox.changeOptions(op)
..map((r) {}).mapLeft((err) {
KRLogUtil.kr_e('更新广告拦截失败: $err');
}).run();
if (kr_status.value == SingboxStarted()) {
await kr_restart();
}
kr_blockAds.value = bl;
}
Future<void> kr_restart() async {
KRLogUtil.kr_i('🔄 重启 SingBox...', tag: 'SingBox');
// 🔧 P0-2: 改进异常处理 - stop 失败也要尝试 start
// 🔧 P1-1: 优化网络抖动 - 监听状态变化而非固定延迟
// ✅ DNS/代理 由 libcore.dll 自动管理
try {
// 1. 先停止
try {
await kr_stop();
} catch (stopError) {
KRLogUtil.kr_e('❌ 停止失败: $stopError,仍尝试启动', tag: 'SingBox');
// 不 rethrow继续尝试启动
}
// 🔧 P1-1: 2. 等待完全停止 - 监听状态而非固定延迟(减少网络中断时间)
try {
// 如果已经是停止状态,直接继续
if (kr_status.value is SingboxStopped) {
KRLogUtil.kr_i('✅ SingBox 已是停止状态,立即启动', tag: 'SingBox');
} else {
// 等待状态变为停止最多等待2秒比原来的盲等500ms更聪明
final completer = Completer<void>();
late final Worker worker;
worker = ever(kr_status, (status) {
if (status is SingboxStopped) {
if (!completer.isCompleted) {
completer.complete();
worker.dispose();
KRLogUtil.kr_i('✅ 检测到 SingBox 已停止,立即启动新实例', tag: 'SingBox');
}
}
});
await completer.future.timeout(
const Duration(seconds: 2),
onTimeout: () {
KRLogUtil.kr_w('⏱️ 等待停止状态超时(2s),使用备用延迟', tag: 'SingBox');
worker.dispose();
},
);
}
} catch (stateError) {
KRLogUtil.kr_w('⚠️ 状态监听异常: $stateError,使用备用延迟', tag: 'SingBox');
// 备用:使用较短的延迟(300ms代替原来的500ms)
await Future.delayed(const Duration(milliseconds: 300));
}
// 🔧 P1-1: 3. 重新启动
try {
await kr_start();
KRLogUtil.kr_i('✅ SingBox 重启完成', tag: 'SingBox');
} catch (startError) {
KRLogUtil.kr_e('❌ 重启失败: $startError', tag: 'SingBox');
rethrow; // 只有启动失败才抛异常
}
} catch (e) {
KRLogUtil.kr_e('❌ 重启异常: $e', tag: 'SingBox');
rethrow;
}
}
//// 设置出站模式
Future<void> kr_updateConnectionType(KRConnectionType newType) async {
final previousType = kr_connectionType.value; // 保存原模式用于失败时回滚
try {
KRLogUtil.kr_i('🔄 开始更新连接类型...', tag: 'SingBox');
KRLogUtil.kr_i('📊 当前类型: ${kr_connectionType.value}', tag: 'SingBox');
KRLogUtil.kr_i('📊 新类型: $newType', tag: 'SingBox');
if (kr_connectionType.value == newType) {
KRLogUtil.kr_i('⚠️ 连接类型相同,无需更新', tag: 'SingBox');
return;
}
// ✅ 改进:直接尝试切换(不做权限检查避免黑屏)
// 关键改进:保存原模式,失败时回滚以防止自动重连机制无限循环
KRLogUtil.kr_i('📝 尝试切换模式到: $newType(原模式: $previousType', tag: 'SingBox');
kr_connectionType.value = newType;
final oOption = _getConfigOption();
var mode = "";
switch (newType) {
case KRConnectionType.global:
mode = "other";
KRLogUtil.kr_i('🌍 切换到全局代理模式', tag: 'SingBox');
break;
case KRConnectionType.rule:
mode = KRCountryUtil.kr_getCurrentCountryCode();
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
break;
// case KRConnectionType.direct:
// mode = "direct";
// break;
}
oOption["region"] = mode;
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
final op = SingboxConfigOption.fromJson(oOption);
KRLogUtil.kr_i('📄 配置选项: ${oOption.toString()}', tag: 'SingBox');
await kr_singBox.changeOptions(op)
..map((r) {
KRLogUtil.kr_i('✅ 连接类型更新成功', tag: 'SingBox');
}).mapLeft((err) {
KRLogUtil.kr_e('❌ 更新连接类型失败: $err', tag: 'SingBox');
throw err;
}).run();
if (kr_status.value == SingboxStarted()) {
KRLogUtil.kr_i('🔄 VPN已启动准备重启以应用新配置...', tag: 'SingBox');
try {
// 重启前添加超时保护10秒避免卡顿
await kr_restart().timeout(
const Duration(seconds: 10),
onTimeout: () {
KRLogUtil.kr_w('⏱️ VPN重启超时(10s),可能缺少权限', tag: 'SingBox');
throw TimeoutException('VPN重启超时请检查是否需要管理员权限');
},
);
KRLogUtil.kr_i('✅ VPN重启完成', tag: 'SingBox');
} catch (restartError) {
KRLogUtil.kr_e('❌ VPN重启失败: $restartError', tag: 'SingBox');
// 重启失败时回滚模式改变,避免触发自动重连机制无限循环
KRLogUtil.kr_w('🔙 重启失败,回滚模式到: $previousType', tag: 'SingBox');
kr_connectionType.value = previousType;
// ❌ 关键修复:不要 rethrow异常已经完整处理已回滚 + Toast 提示由监听器负责)
// rethrow;
}
} else {
KRLogUtil.kr_i(' VPN未启动配置已更新', tag: 'SingBox');
}
} catch (e, stackTrace) {
KRLogUtil.kr_e('💥 更新连接类型异常: $e', tag: 'SingBox');
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox');
// 最终保险:异常时确保回滚模式
if (kr_connectionType.value != previousType) {
KRLogUtil.kr_w('🔙 异常捕获,回滚模式到: $previousType', tag: 'SingBox');
kr_connectionType.value = previousType;
}
// ❌ 关键修复:不要 rethrow
// 异常已经通过 Toast 由监听器提示,模式已回滚
// 继续 rethrow 会导致对话框无法关闭,让应用看起来"卡"了
// rethrow;
}
}
/// 更新国家设置
Future<void> kr_updateCountry(KRCountry kr_country) async {
// 如果国家相同,直接返回
if (kr_country.kr_code == KRCountryUtil.kr_getCurrentCountryCode()) {
return;
}
try {
// 更新工具类中的当前国家
await KRCountryUtil.kr_setCurrentCountry(kr_country);
// 更新配置选项
final oOption = _getConfigOption();
oOption["region"] = kr_country.kr_code;
final op = SingboxConfigOption.fromJson(oOption);
await kr_singBox.changeOptions(op)
..map((r) {}).mapLeft((err) {
KRLogUtil.kr_e('更新国家设置失败: $err');
}).run();
// 如果服务正在运行,重启服务
if (kr_status.value == SingboxStarted()) {
await kr_restart();
}
} catch (err) {
KRLogUtil.kr_e('更新国家失败: $err');
rethrow;
}
}
Stream<SingboxStatus> kr_watchStatus() {
return kr_singBox.watchStatus();
}
Stream<List<SingboxOutboundGroup>> kr_watchGroups() {
return kr_singBox.watchGroups();
}
// 节点选择监控定时器
Timer? _nodeSelectionTimer;
Future<void> kr_selectOutbound(String tag) async {
// ========== 关键路径:必需的同步操作 ==========
// 🔧 P2新: 检查 VPN 是否在运行
if (kr_status.value is! SingboxStarted) {
KRLogUtil.kr_w('⚠️ VPN 未运行,无法选择节点: $tag', tag: 'SingBox');
throw Exception('VPN 未启动,无法选择节点。请先启动 VPN');
}
KRLogUtil.kr_i('🎯 开始选择出站节点: $tag', tag: 'SingBox');
// 🔧 关键修复:使用 await 确保保存完成
try {
await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: tag);
if (kDebugMode) {
KRLogUtil.kr_d('✅ 节点选择已保存: $tag', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox');
}
// 🔧 关键修复:使用 await 确保 command client 初始化完成
try {
await _kr_ensureCommandClientInitialized();
if (kDebugMode) {
KRLogUtil.kr_d('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
}
// 🔧 关键修复:使用正确的 group tag
// libcore 生成的selector组的tag是"proxy"而不是"select"
final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
// ⚡ 关键优化:核心 API 调用
await _kr_selectOutboundWithRetry(selectorGroupTag, tag, maxAttempts: 3, initialDelay: 50);
if (kDebugMode) {
KRLogUtil.kr_d('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
}
// ========== 关键路径结束!以下操作都在后台进行 ==========
// ⚡ 后台任务:验证 + 刷新 + 监控(不影响返回)
unawaited(_kr_backgroundNodeVerificationAndRefresh(tag));
} catch (e) {
KRLogUtil.kr_e('❌ 节点选择失败: $e', tag: 'SingBox');
rethrow; // 抛出异常,让调用者知道失败了
}
}
/// ⚡ 后台验证和刷新(不影响 UI 响应)
/// 包含:验证节点选择 + TUN 模式刷新 + 启动监控定时器
Future<void> _kr_backgroundNodeVerificationAndRefresh(String tag) async {
try {
// 第一步:等待一点时间让活动组更新
await Future.delayed(const Duration(milliseconds: 100));
// 第二步:后台验证节点选择
await _krVerifyNodeSelectionInBackground(tag);
// 第三步TUN 模式刷新连接
if (Platform.isWindows && kr_connectionType.value == KRConnectionType.global) {
await _kr_tunModeRefreshInBackground(tag);
}
// 第四步:启动节点选择监控(防止被 auto 覆盖)
_kr_startNodeSelectionMonitoring(tag);
} catch (e) {
KRLogUtil.kr_e('❌ 后台任务异常(不影响节点切换): $e', tag: 'SingBox');
}
}
/// 后台验证节点选择(异步,失败只记日志)
Future<void> _krVerifyNodeSelectionInBackground(String tag) async {
try {
final selectGroup = kr_activeGroups.firstWhere(
(group) => group.tag == 'select',
orElse: () => throw Exception('未找到 "select" 选择器组'),
);
if (selectGroup.selected != tag) {
KRLogUtil.kr_w('⚠️ 节点验证失败: 期望 $tag, 实际 ${selectGroup.selected}', tag: 'SingBox');
} else if (kDebugMode) {
KRLogUtil.kr_d('✅ 节点验证成功: $tag', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_e('❌ 节点验证异常: $e', tag: 'SingBox');
}
}
/// TUN 模式连接刷新(后台执行)
Future<void> _kr_tunModeRefreshInBackground(String tag) async {
try {
if (kDebugMode) {
KRLogUtil.kr_d('🔄 TUN 模式:后台刷新连接...', tag: 'SingBox');
}
final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select')
? 'select'
: 'proxy';
// 第一步urlTest 创建新连接
final testResult = await kr_singBox.urlTest(selectorGroupTag).run().timeout(
const Duration(seconds: 3),
onTimeout: () {
if (kDebugMode) {
KRLogUtil.kr_d('⚠️ urlTest 超时', tag: 'SingBox');
}
return const Left('timeout');
},
);
testResult.match(
(error) {
if (kDebugMode) {
KRLogUtil.kr_d('⚠️ urlTest 失败: $error', tag: 'SingBox');
}
},
(_) {
if (kDebugMode) {
KRLogUtil.kr_d('✅ urlTest 完成', tag: 'SingBox');
}
},
);
// 第二步HTTP 刷新(最有效)
await _kr_forceHttpConnectionRefresh(tag);
// 第三步:等待连接稳定
await Future.delayed(const Duration(milliseconds: 500));
if (kDebugMode) {
KRLogUtil.kr_d('✅ TUN 模式连接刷新完成', tag: 'SingBox');
}
} catch (e) {
KRLogUtil.kr_e('❌ TUN 模式刷新异常: $e(不影响节点切换)', tag: 'SingBox');
}
}
/// 启动节点选择监控定时器(防止 urltest 自动覆盖)
void _kr_startNodeSelectionMonitoring(String tag) {
try {
_nodeSelectionTimer?.cancel();
_nodeSelectionTimer = null;
if (tag != 'auto') {
final currentSelectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select')
? 'select'
: 'proxy';
final selectedTag = tag;
if (kDebugMode) {
KRLogUtil.kr_d('🔁 启动节点选择监控: $selectedTag', tag: 'SingBox');
}
_nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) {
try {
kr_singBox.selectOutbound(currentSelectorGroupTag, selectedTag).run().then((result) {
result.match(
(error) {
if (kDebugMode) {
KRLogUtil.kr_d('🔁 定时器重选失败: $error', tag: 'SingBox');
}
},
(_) {
if (kDebugMode) {
KRLogUtil.kr_d('🔁 定时器重选成功', tag: 'SingBox');
}
},
);
}).catchError((error) {
KRLogUtil.kr_w('🔁 定时器异常: $error', tag: 'SingBox');
});
} catch (e) {
KRLogUtil.kr_e('💥 定时器回调异常: $e', tag: 'SingBox');
timer.cancel();
_nodeSelectionTimer = null;
}
});
}
} catch (timerError) {
KRLogUtil.kr_e('❌ 启动定时器异常: $timerError', tag: 'SingBox');
_nodeSelectionTimer?.cancel();
_nodeSelectionTimer = null;
}
}
/// 配合文件地址
Directory get directory =>
Directory(p.join(kr_configDics.workingDir.path, "configs"));
File _file(String fileName) {
return File(p.join(directory.path, "$fileName.json"));
}
File _tempFile(String fileName) => _file("$fileName.tmp");
/// 启动节点选择监控定时器,防止用户手动选择被自动策略覆盖
void kr_startNodeSelectionMonitor(String tag) {
kr_resetNodeSelectionMonitor();
if (tag != 'auto') {
KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox');
_nodeSelectionTimer =
Timer.periodic(const Duration(seconds: 20), (timer) {
// 每 20 秒重新选择一次,确保用户选择不被覆盖
// 使用 then/catchError 避免异常导致 UI 阻塞
kr_singBox.selectOutbound("select", tag).run().then((result) {
result.match(
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
);
}).catchError((error) {
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
});
KRLogUtil.kr_d('🔁 重新确认节点选择: $tag', tag: 'SingBox');
});
}
}
/// 重置并停止节点选择监控定时器
void kr_resetNodeSelectionMonitor() {
_nodeSelectionTimer?.cancel();
_nodeSelectionTimer = null;
KRLogUtil.kr_i('✅ 节点选择监控已停止', tag: 'SingBox');
}
// File tempFile(String fileName) => file("$fileName.tmp");
/// ✅ 临时切换模式以完全断开浏览器连接(推荐方案)
/// 原理:
/// TUN 模式:浏览器 → 操作系统 → sing-box → 节点(浏览器无法关闭连接)❌
/// 系统代理:浏览器 → 代理软件 → 节点(代理可强制关闭连接)✅
/// 流程:
/// 1. TUN → 系统代理(这时代理会主动关闭所有旧连接)
/// 2. 发送 HTTP 请求(刷新代理缓存)
/// 3. 系统代理 → TUN恢复原状态
/// 优点100% 彻底断开无残留连接UX 无感知(<100ms
Future<void> _kr_refreshConnectionByModeSwitch(String nodeTag) async {
if (!Platform.isWindows) {
// 非 Windows 系统,使用原有的 HTTP 刷新方案
await _kr_forceHttpConnectionRefresh(nodeTag);
return;
}
try {
KRLogUtil.kr_i('🔄 [高级] 临时切换到系统代理以断开所有连接...', tag: 'SingBox');
final originalMode = kr_connectionType.value;
// 第一步:临时切换到系统代理模式
KRLogUtil.kr_i(' ① 临时切换TUN → 系统代理模式', tag: 'SingBox');
await kr_updateConnectionType(KRConnectionType.rule);
await Future.delayed(const Duration(milliseconds: 100)); // 等待模式切换生效
// 第二步:发送 HTTP 请求强制刷新(此时用系统代理)
KRLogUtil.kr_i(' ② 发送 HTTP 请求(通过系统代理刷新缓存)', tag: 'SingBox');
await _kr_forceHttpConnectionRefresh(nodeTag);
// 第三步切换回原模式TUN
KRLogUtil.kr_i(' ③ 恢复:系统代理 → TUN 模式', tag: 'SingBox');
await kr_updateConnectionType(originalMode);
await Future.delayed(const Duration(milliseconds: 100)); // 等待模式切换生效
KRLogUtil.kr_i('✅ [高级] 连接完全刷新完成,浏览器将使用新节点的新连接', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('⚠️ 模式切换刷新异常: $e(节点切换已生效)', tag: 'SingBox');
}
}
/// ✅ 激进方案:完全重启 VPN100% 清理所有连接)
/// 原理:完全关闭 VPN 再重新启动,所有连接都会断开并重建
/// 优点:最彻底,无任何残留连接
/// 缺点:用户会短暂看到"已断开"状态,可能有 DNS 泄漏短暂风险
Future<void> _kr_restartVpnForNodeSwitch(String nodeTag) async {
try {
KRLogUtil.kr_i('🔄 [激进] 重启 VPN 以彻底清理所有连接...', tag: 'SingBox');
// 第一步:关闭 VPN断开所有连接
KRLogUtil.kr_i(' ① 停止 VPN', tag: 'SingBox');
await kr_stop();
await Future.delayed(const Duration(milliseconds: 300)); // 等待VPN完全停止
// 第二步:重新启动 VPN建立新连接到新节点
KRLogUtil.kr_i(' ② 重新启动 VPN', tag: 'SingBox');
await kr_start();
KRLogUtil.kr_i('✅ [激进] VPN 重启完成,所有连接已刷新', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ VPN 重启失败: $e(请手动重新启动)', tag: 'SingBox');
// 尝试恢复
try {
await kr_start();
} catch (e2) {
KRLogUtil.kr_e('❌ VPN 恢复启动失败: $e2', tag: 'SingBox');
}
}
}
/// ✅ TUN模式连接强制刷新发送真实 HTTP 请求
/// 原理:实际的 HTTP 请求会强制建立新的 TCP 连接,让浏览器感知到网络路由变化
/// 效果:比仅做 urlTest 测试更有效,因为这是真实的数据传输
Future<void> _kr_forceHttpConnectionRefresh(String nodeTag) async {
try {
// 使用多个测试 URL确保至少有一个能成功
final testUrls = [
'http://connectivitycheck.gstatic.com/generate_204', // Google 连接检查
'http://www.google.com/generate_204',
'http://ipv4.icanhazip.com/', // IP 检查,会返回当前外网 IP
];
KRLogUtil.kr_i('🔗 使用 HTTP 请求强制刷新连接(节点: $nodeTag', tag: 'SingBox');
for (int i = 0; i < testUrls.length; i++) {
final httpClient = HttpClient();
httpClient.connectionTimeout = const Duration(seconds: 2);
try {
final url = testUrls[i];
KRLogUtil.kr_i(' 尝试 URL[$i]: $url', tag: 'SingBox');
// 发送 HTTP 请求(不使用 Dio直接使用 HttpClient 避免代理)
final request = await httpClient.getUrl(Uri.parse(url));
request.headers.set('Connection', 'close'); // 明确关闭连接
request.headers.set('User-Agent', 'BearVPN/1.0'); // 标识为 BearVPN 请求
final response = await request.close().timeout(
const Duration(seconds: 2),
);
// 消费响应体
final _ = await response.transform(utf8.decoder).join();
KRLogUtil.kr_i('✅ HTTP 刷新成功 ($url),状态码: ${response.statusCode}', tag: 'SingBox');
return; // 成功就返回,不需要尝试其他 URL
} catch (e) {
KRLogUtil.kr_w('⚠️ HTTP 刷新失败 (${testUrls[i]}): $e', tag: 'SingBox');
// 继续尝试下一个 URL
if (i < testUrls.length - 1) {
await Future.delayed(const Duration(milliseconds: 100));
}
} finally {
// ✅ 关键修复:确保在任何情况下都关闭连接,避免资源泄漏
httpClient.close();
}
}
KRLogUtil.kr_w('⚠️ 所有 HTTP 刷新 URL 都失败,但节点切换仍然生效', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ 连接刷新异常: $e', tag: 'SingBox');
}
}
Future<void> kr_urlTest(String groupTag) async {
KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox');
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
// 打印所有活动组信息
for (int i = 0; i < kr_activeGroups.length; i++) {
final group = kr_activeGroups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
}
}
try {
KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox');
final result = await kr_singBox.urlTest(groupTag).run();
KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox');
// 等待一段时间让 SingBox 完成测试
await Future.delayed(const Duration(seconds: 2));
// 再次检查活动组状态
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
for (int i = 0; i < kr_activeGroups.length; i++) {
final group = kr_activeGroups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
}
}
} catch (e) {
KRLogUtil.kr_e('❌ URL 测试失败: $groupTag, 错误: $e', tag: 'SingBox');
KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox');
}
}
/// 🪟 Windows: 检查当前进程是否有管理员权限TUN 模式需要)
/// 返回 true 表示有管理员权限false 表示无权限
/// ✅ 公开方法:检查 Windows 管理员权限
/// 返回 true: 有管理员权限,可以使用 TUN 模式
/// 返回 false: 无管理员权限,只能使用规则/系统代理模式
Future<bool> kr_checkWindowsAdminPrivilege() async {
return await _kr_checkWindowsAdminPrivilege();
}
/// 私有方法:内部实现(带缓存)
/// ✅ 改进:使用 FFI 直接调用 IsUserAnAdmin() Windows API
/// ⭐ 优化:
/// - 不启动新进程,无黑屏风险
/// - 直接内存操作速度快1-5ms vs 600ms
/// - 零资源开销
/// - Windows 官方 API完全可靠
Future<bool> _kr_checkWindowsAdminPrivilege() async {
if (!Platform.isWindows) return true;
// 检查缓存是否有效
if (_cachedAdminPrivilege != null && _cachedAdminPrivilegeTime != null) {
final elapsed = DateTime.now().difference(_cachedAdminPrivilegeTime!);
if (elapsed < _adminPrivilegeCacheDuration) {
KRLogUtil.kr_i('📦 使用缓存的管理员权限状态: ${_cachedAdminPrivilege}', tag: 'SingBox');
return _cachedAdminPrivilege!;
}
}
try {
// ✅ 方案 B使用 FFI 直接调用 IsUserAnAdmin() Windows API
// 比 Process.run('net session') 快 300 倍,且无黑屏风险
KRLogUtil.kr_i('🔍 使用 FFI 检查管理员权限(无黑屏)...', tag: 'SingBox');
final hasPrivilege = _isUserAnAdminFFI();
// 缓存结果
_cachedAdminPrivilege = hasPrivilege;
_cachedAdminPrivilegeTime = DateTime.now();
if (hasPrivilege) {
KRLogUtil.kr_i('✅ 检测到管理员权限', tag: 'SingBox');
} else {
KRLogUtil.kr_w('⚠️ 未检测到管理员权限TUN 模式需要管理员权限', tag: 'SingBox');
}
return hasPrivilege;
} catch (e) {
KRLogUtil.kr_e('❌ 检查管理员权限失败: $e', tag: 'SingBox');
// 检查失败时返回 false让用户重新以管理员身份运行
return false;
}
}
/// 【方案 A】sing-box 进程健康监测
/// 用于检测 sing-box 是否在启动后不久崩溃
///
/// 背景:诊断日志显示
/// - start() 立即返回成功8964 端口可连接 ✅
/// - 1.4 秒后8964 端口突然无法连接 ❌
/// - 说明 sing-box 进程在启动后崩溃了
///
/// 监测策略:
/// 1. 启动成功后,立即开始监测
/// 2. 每隔 500ms 检查一次 8964 端口
/// 3. 如果连续 3 次无法连接,说明进程崩溃了
/// 4. 但如果 _stopRequested 为 true说明用户要求停止不要恢复
Future<void> _monitorSingBoxProcess() async {
const checkInterval = Duration(milliseconds: 500);
const maxAttempts = 10; // 监测 5 秒10 次 * 500ms
int failureCount = 0;
int checkCount = 0;
try {
await KRFileLogger.log('[进程监测] 🟢 开始 sing-box 进程健康监测(每 500ms 检查一次,共 10 次)');
for (int i = 0; i < maxAttempts; i++) {
checkCount++;
await Future.delayed(checkInterval);
// 【关键】检查停止标志:如果用户要求停止,立即返回
if (_stopRequested) {
await KRFileLogger.log('[进程监测] ⏭️ 检测到停止标志,监测任务立即返回');
return;
}
try {
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 200),
onTimeout: () => throw Exception('端口连接超时'),
);
socket.destroy();
// 连接成功,重置失败计数
failureCount = 0;
await KRFileLogger.log('[进程监测] ✅ 检查 #$checkCount: 8964 端口可连接(进程正常运行)');
} catch (e) {
failureCount++;
await KRFileLogger.log('[进程监测] ❌ 检查 #$checkCount: 8964 端口无法连接(失败 #$failureCount');
// 如果连续 3 次失败,说明进程崩溃了
if (failureCount >= 3) {
// 【关键】但首先检查停止标志!
if (_stopRequested) {
await KRFileLogger.log('[进程监测] ⏭️ 连接失败,但用户已要求停止,不触发恢复');
return;
}
await KRFileLogger.log('[进程监测] 🚨 【严重】连续 3 次无法连接sing-box 进程已崩溃!');
await KRFileLogger.log('[进程监测] 🚨 时间:${DateTime.now()}');
await KRFileLogger.log('[进程监测] 🚨 这可能是导致后续 UI 卡顿的根本原因!');
KRLogUtil.kr_e('【严重】sing-box 进程在启动后崩溃了!端口无法连接', tag: 'ProcessMonitor');
// 【可选】触发恢复机制:尝试重启 sing-box
// await _triggerSingBoxRecovery();
return; // 停止监测
}
}
}
// 监测完成
await KRFileLogger.log('[进程监测] ✅ 监测完成sing-box 进程在启动后 5 秒内保持稳定运行');
KRLogUtil.kr_i('✅ sing-box 进程健康检查通过', tag: 'ProcessMonitor');
} catch (e) {
await KRFileLogger.log('[进程监测] ❌ 监测异常: $e');
KRLogUtil.kr_e('进程监测异常: $e', tag: 'ProcessMonitor');
}
}
/// 【方案 A】sing-box 进程恢复机制
/// 当检测到进程崩溃时,尝试恢复
///
/// 改进:
/// 1. 先尝试优雅关闭graceful shutdown
/// 2. 如果进程仍活着,强制杀死以立即释放端口
/// 3. 轮询确认端口已释放后再重启
Future<void> _triggerSingBoxRecovery() async {
try {
await KRFileLogger.log('[进程恢复] 🟡 检测到进程崩溃,尝试恢复...');
KRLogUtil.kr_w('⚠️ sing-box 进程崩溃,尝试恢复...', tag: 'ProcessMonitor');
// 1⃣ 先尝试优雅停止
try {
await kr_singBox.stop().run();
await KRFileLogger.log('[进程恢复] ✅ 已优雅停止旧进程');
} catch (e) {
await KRFileLogger.log('[进程恢复] ⚠️ 优雅停止失败(可能已经停止): $e');
}
// 2⃣ 等待进程优雅关闭
await Future.delayed(const Duration(milliseconds: 300));
// 3⃣ 检查进程是否还活着(检查 8964 端口)
bool processStillAlive = false;
try {
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 200),
onTimeout: () => throw Exception('端口连接超时'),
);
socket.destroy();
processStillAlive = true;
await KRFileLogger.log('[进程恢复] ⚠️ 8964 端口仍在被占用,进程未完全关闭');
} catch (e) {
processStillAlive = false;
await KRFileLogger.log('[进程恢复] ✅ 8964 端口已释放,进程已完全关闭');
}
// 4⃣ 如果进程还活着强制杀死taskkill - Windows only
bool killSucceeded = true;
if (processStillAlive) {
await KRFileLogger.log('[进程恢复] 🔴 进程仍活着,执行强制杀死...');
try {
// 【Windows 专用】使用 taskkill 强制杀死所有 sing-box 进程
// ✅ 使用 KRWindowsProcessUtil.runHidden 避免黑窗
if (Platform.isWindows) {
final result = await KRWindowsProcessUtil.runHidden(
'taskkill',
['/IM', 'sing-box.exe', '/F'],
);
// 【关键】检查返回码!
if (result.exitCode == 0) {
await KRFileLogger.log('[进程恢复] ✅ taskkill 成功(返回码: 0');
killSucceeded = true;
} else {
await KRFileLogger.log('[进程恢复] ⚠️ taskkill 返回码: ${result.exitCode}(可能失败)');
// 返回码 128 = 进程未找到(可能已自动退出)
// 返回码 1 = 没有权限
killSucceeded = (result.exitCode == 128); // 128 = 进程已不存在(正常)
}
} else {
// 【非 Windows 平台】跳过 taskkill仅依赖优雅关闭
await KRFileLogger.log('[进程恢复] 非 Windows 平台,跳过 taskkill依赖优雅关闭');
killSucceeded = true; // 假设优雅关闭足够
}
} catch (e) {
await KRFileLogger.log('[进程恢复] ⚠️ taskkill 执行异常: $e');
killSucceeded = false;
}
// 5⃣ 等待进程资源完全释放
// 如果 kill 成功,等待较短的时间;如果失败,等待较长的时间
final waitDuration = killSucceeded
? const Duration(milliseconds: 500)
: const Duration(milliseconds: 1000);
await Future.delayed(waitDuration);
}
// 6⃣ 轮询等待端口完全释放(避免 TIME_WAIT 状态)
await KRFileLogger.log('[进程恢复] 🔍 轮询验证 8964 端口是否完全释放...');
int releaseRetries = 0;
const maxReleaseRetries = 20; // 最多等待 10 秒
bool portFullyReleased = false;
while (releaseRetries < maxReleaseRetries) {
try {
// 尝试连接,如果失败说明端口已释放
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 100),
onTimeout: () => throw Exception('端口连接超时'),
);
socket.destroy();
// 仍然能连接,继续等待
releaseRetries++;
final elapsedMs = releaseRetries * 500;
await KRFileLogger.log('[进程恢复] ⏳ 端口仍被占用,等待中... (#$releaseRetries, 已等待 ${elapsedMs}ms)');
// 如果等待超过 5 秒且端口仍被占用,说明有其他进程占用端口(异常)
if (elapsedMs > 5000 && releaseRetries % 4 == 0) {
await KRFileLogger.log('[进程恢复] ⚠️ 【警告】端口占用超过 5 秒,可能有其他进程占用!');
}
await Future.delayed(const Duration(milliseconds: 500));
} catch (e) {
// 无法连接,端口已释放
portFullyReleased = true;
final elapsedMs = releaseRetries * 500;
await KRFileLogger.log('[进程恢复] ✅ 确认 8964 端口已完全释放(耗时 ${elapsedMs}ms');
break;
}
}
if (!portFullyReleased) {
await KRFileLogger.log('[进程恢复] ⚠️ 等待端口释放超时(已等待 ${maxReleaseRetries * 500}ms继续尝试重启...');
}
// 7⃣ 重新启动 sing-box带重试
await KRFileLogger.log('[进程恢复] 🟡 准备重新启动 sing-box...');
int startRetries = 0;
const maxStartRetries = 3;
bool startSucceeded = false;
while (startRetries < maxStartRetries) {
try {
await kr_singBox.start(_cutPath, kr_configName, false).map(
(r) {
KRLogUtil.kr_i('✅ sing-box 恢复启动成功', tag: 'ProcessMonitor');
return r;
},
).mapLeft((err) {
KRLogUtil.kr_e('❌ sing-box 恢复启动失败: $err', tag: 'ProcessMonitor');
throw err;
}).run();
startSucceeded = true;
await KRFileLogger.log('[进程恢复] ✅ sing-box 进程已恢复启动(第 ${startRetries + 1} 次尝试)');
break;
} catch (e) {
startRetries++;
await KRFileLogger.log('[进程恢复] ❌ sing-box 启动失败(第 $startRetries 次,错误:$e');
if (startRetries < maxStartRetries) {
// 启动失败,增加延迟后重试
final delayMs = 1000 + (startRetries * 1000); // 1s, 2s, 3s 递增延迟
await KRFileLogger.log('[进程恢复] ⏳ 将在 ${delayMs}ms 后进行第 ${startRetries + 1} 次尝试...');
await Future.delayed(Duration(milliseconds: delayMs));
}
}
}
if (!startSucceeded) {
await KRFileLogger.log('[进程恢复] ❌ sing-box 恢复启动失败3 次尝试都失败)');
throw Exception('sing-box 恢复启动失败');
}
// 8⃣ 启动后等待 sing-box 真正准备好
await Future.delayed(const Duration(milliseconds: 500));
// 验证 8964 端口真的可以连接
int portCheckRetries = 0;
while (portCheckRetries < 5) {
try {
final socket = await Socket.connect('127.0.0.1', 8964).timeout(
const Duration(milliseconds: 200),
onTimeout: () => throw Exception('端口连接超时'),
);
socket.destroy();
await KRFileLogger.log('[进程恢复] ✅ 8964 端口已准备好sing-box 完全就绪');
break;
} catch (e) {
portCheckRetries++;
if (portCheckRetries < 5) {
await Future.delayed(const Duration(milliseconds: 200));
}
}
}
// 9⃣ 重新开始监测
unawaited(_monitorSingBoxProcess());
} catch (e) {
await KRFileLogger.log('[进程恢复] ❌ 进程恢复失败: $e');
KRLogUtil.kr_e('进程恢复失败: $e', tag: 'ProcessMonitor');
}
}
/// FFI 实现:直接调用 Windows IsUserAnAdmin() API
/// 返回 true 表示有管理员权限false 表示无权限
///
/// ✅ 优点:
/// - 不创建新进程,无黑屏
/// - 直接 API 调用超快速1-5ms
/// - 内存操作,零 I/O 等待
/// - Windows 官方 API完全可靠
bool _isUserAnAdminFFI() {
try {
// 加载 shell32.dll包含 IsUserAnAdmin 函数)
final shell32 = DynamicLibrary.open('shell32.dll');
// 定义 FFI 绑定
// IsUserAnAdmin 返回 BOOLUint8 in Dart无参数
final isUserAnAdmin = shell32.lookupFunction<
Uint8 Function(),
int Function()
>('IsUserAnAdmin');
// 调用 API 并检查返回值
// 返回值 != 0 表示是管理员,== 0 表示不是
final result = isUserAnAdmin();
return result != 0;
} catch (e) {
KRLogUtil.kr_e('❌ FFI 权限检查异常: $e', tag: 'SingBox');
// 异常时返回 false安全起见假设无权限
return false;
}
}
// ==================== macOS SOCKS5 系统代理 ====================
// 🍎 让 Telegram 等应用自动走代理(参考 Clash 实现)
// 原因libcore 只设置 HTTP/HTTPS 代理,但 Telegram 只检测 SOCKS5 代理
/// 获取当前活动的网络服务名称Wi-Fi、Ethernet 等)
Future<String?> _kr_getMacOSActiveNetworkService() async {
if (!Platform.isMacOS) return null;
try {
// 获取默认路由的网络接口
final routeResult = await Process.run('route', ['-n', 'get', 'default']);
if (routeResult.exitCode != 0) {
KRLogUtil.kr_w('⚠️ 获取默认路由失败', tag: 'SingBox');
return 'Wi-Fi'; // 默认返回 Wi-Fi
}
final routeOutput = routeResult.stdout.toString();
final interfaceMatch = RegExp(r'interface:\s*(\S+)').firstMatch(routeOutput);
if (interfaceMatch == null) {
return 'Wi-Fi';
}
final interfaceName = interfaceMatch.group(1);
KRLogUtil.kr_i('🔍 检测到网络接口: $interfaceName', tag: 'SingBox');
// 获取网络服务列表,找到对应的服务名称
final servicesResult = await Process.run('networksetup', ['-listallhardwareports']);
if (servicesResult.exitCode != 0) {
return 'Wi-Fi';
}
final servicesOutput = servicesResult.stdout.toString();
final lines = servicesOutput.split('\n');
String? currentService;
for (final line in lines) {
if (line.startsWith('Hardware Port:')) {
currentService = line.replaceFirst('Hardware Port:', '').trim();
} else if (line.startsWith('Device:') && currentService != null) {
final device = line.replaceFirst('Device:', '').trim();
if (device == interfaceName) {
KRLogUtil.kr_i('✅ 找到网络服务: $currentService', tag: 'SingBox');
return currentService;
}
}
}
return 'Wi-Fi'; // 默认返回 Wi-Fi
} catch (e) {
KRLogUtil.kr_e('❌ 获取网络服务失败: $e', tag: 'SingBox');
return 'Wi-Fi';
}
}
/// 设置或清除 macOS SOCKS5 系统代理
/// [enable] true=设置代理, false=清除代理
Future<void> _kr_setMacOSSocks5Proxy(bool enable) async {
if (!Platform.isMacOS) return;
try {
final networkService = await _kr_getMacOSActiveNetworkService();
if (networkService == null) {
KRLogUtil.kr_w('⚠️ 无法获取网络服务名称', tag: 'SingBox');
return;
}
if (enable) {
// 设置 SOCKS5 代理(参考 Clash 实现)
KRLogUtil.kr_i('🍎 设置 macOS SOCKS5 系统代理: $networkService → 127.0.0.1:$kr_port', tag: 'SingBox');
// 设置 SOCKS 代理服务器和端口
final setResult = await Process.run('networksetup', [
'-setsocksfirewallproxy',
networkService,
'127.0.0.1',
kr_port.toString(),
]);
if (setResult.exitCode != 0) {
KRLogUtil.kr_e('❌ 设置 SOCKS5 代理失败: ${setResult.stderr}', tag: 'SingBox');
return;
}
// 启用 SOCKS 代理
final enableResult = await Process.run('networksetup', [
'-setsocksfirewallproxystate',
networkService,
'on',
]);
if (enableResult.exitCode != 0) {
KRLogUtil.kr_e('❌ 启用 SOCKS5 代理失败: ${enableResult.stderr}', tag: 'SingBox');
return;
}
KRLogUtil.kr_i('✅ macOS SOCKS5 系统代理已设置Telegram 等应用将自动走代理)', tag: 'SingBox');
} else {
// 清除 SOCKS5 代理
KRLogUtil.kr_i('🍎 清除 macOS SOCKS5 系统代理: $networkService', tag: 'SingBox');
// 禁用 SOCKS 代理
final disableResult = await Process.run('networksetup', [
'-setsocksfirewallproxystate',
networkService,
'off',
]);
if (disableResult.exitCode != 0) {
KRLogUtil.kr_w('⚠️ 禁用 SOCKS5 代理失败: ${disableResult.stderr}', tag: 'SingBox');
} else {
KRLogUtil.kr_i('✅ macOS SOCKS5 系统代理已清除', tag: 'SingBox');
}
}
} catch (e) {
KRLogUtil.kr_e('❌ macOS SOCKS5 代理操作异常: $e', tag: 'SingBox');
}
}
}
// 🔧 _KRWindowsProxySnapshot 类已删除 - 改用 WinINet API 直接操作,无需快照对象