feat: 保存配置
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
/// 🔧 防抖和限流工具类 - 用于处理快速点击导致的重复请求
|
||||
///
|
||||
/// 使用场景:
|
||||
/// 1. 防抖(Debounce):用户快速点击按钮,等用户停止点击后再执行一次
|
||||
/// - 模式切换、搜索、自动保存
|
||||
/// 2. 限流(Throttle):在给定时间内最多执行一次
|
||||
/// - 节点切换、数据刷新、VPN 启动/停止
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class KRDebounceThrottleUtil {
|
||||
// 防抖计时器缓存(防止多个防抖冲突)
|
||||
static final Map<String, Timer> _debounceTimers = {};
|
||||
|
||||
// 限流时间戳缓存
|
||||
static final Map<String, DateTime> _throttleTimestamps = {};
|
||||
|
||||
/// 🔧 防抖函数:等待 delay 时间无新请求后才执行
|
||||
///
|
||||
/// 适用于:模式切换、搜索、自动保存等
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// KRDebounceThrottleUtil.debounce(
|
||||
/// key: 'mode_switch',
|
||||
/// delay: Duration(milliseconds: 300),
|
||||
/// action: () {
|
||||
/// controller.kr_updateConnectionType(newType);
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
static void debounce({
|
||||
required String key,
|
||||
required Duration delay,
|
||||
required VoidCallback action,
|
||||
}) {
|
||||
// 如果有旧的待执行任务,取消它
|
||||
_debounceTimers[key]?.cancel();
|
||||
|
||||
// 设置新的延迟任务
|
||||
_debounceTimers[key] = Timer(delay, () {
|
||||
action();
|
||||
_debounceTimers.remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
/// 🔧 异步防抖函数(支持 Future)
|
||||
///
|
||||
/// 适用于需要等待异步操作的场景
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// await KRDebounceThrottleUtil.debounceAsync(
|
||||
/// key: 'node_switch',
|
||||
/// delay: Duration(milliseconds: 500),
|
||||
/// action: () async {
|
||||
/// await controller.kr_performNodeSwitch(tag);
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
static Future<void> debounceAsync({
|
||||
required String key,
|
||||
required Duration delay,
|
||||
required Future<void> Function() action,
|
||||
}) async {
|
||||
_debounceTimers[key]?.cancel();
|
||||
|
||||
return Future.delayed(delay).then((_) async {
|
||||
await action();
|
||||
_debounceTimers.remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
/// 🔧 限流函数:在指定时间内最多执行一次
|
||||
///
|
||||
/// 返回值:true 表示执行成功,false 表示被限流(仍在冷却中)
|
||||
///
|
||||
/// 适用于:节点切换、数据刷新、VPN 启动/停止等频繁操作
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// final canExecute = KRDebounceThrottleUtil.throttle(
|
||||
/// key: 'refresh',
|
||||
/// duration: Duration(seconds: 2),
|
||||
/// );
|
||||
///
|
||||
/// if (canExecute) {
|
||||
/// await controller.kr_refreshAll();
|
||||
/// } else {
|
||||
/// showToast('操作过于频繁,请稍后再试');
|
||||
/// }
|
||||
/// ```
|
||||
static bool throttle({
|
||||
required String key,
|
||||
required Duration duration,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
final lastExecuteTime = _throttleTimestamps[key];
|
||||
|
||||
// 如果这是第一次执行或已经过了冷却时间
|
||||
if (lastExecuteTime == null ||
|
||||
now.difference(lastExecuteTime).inMilliseconds >= duration.inMilliseconds) {
|
||||
_throttleTimestamps[key] = now;
|
||||
return true; // 允许执行
|
||||
}
|
||||
|
||||
return false; // 仍在冷却期,拒绝执行
|
||||
}
|
||||
|
||||
/// 🔧 异步限流函数
|
||||
///
|
||||
/// 返回值:true 表示执行成功,false 表示被限流
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// final success = await KRDebounceThrottleUtil.throttleAsync(
|
||||
/// key: 'node_switch',
|
||||
/// duration: Duration(milliseconds: 2000),
|
||||
/// action: () async {
|
||||
/// await controller.kr_performNodeSwitch(tag);
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
static Future<bool> throttleAsync({
|
||||
required String key,
|
||||
required Duration duration,
|
||||
required Future<void> Function() action,
|
||||
}) async {
|
||||
if (throttle(key: key, duration: duration)) {
|
||||
try {
|
||||
await action();
|
||||
return true;
|
||||
} catch (e) {
|
||||
// 如果执行失败,重置时间戳以允许重试
|
||||
_throttleTimestamps.remove(key);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 🔧 获取某个 key 的剩余冷却时间(毫秒)
|
||||
///
|
||||
/// 返回值:
|
||||
/// - 0 或负数:可以执行
|
||||
/// - 正数:还需等待的毫秒数
|
||||
static int getRemainingThrottleTime({
|
||||
required String key,
|
||||
required Duration duration,
|
||||
}) {
|
||||
final lastExecuteTime = _throttleTimestamps[key];
|
||||
if (lastExecuteTime == null) return 0;
|
||||
|
||||
final remaining = duration.inMilliseconds -
|
||||
DateTime.now().difference(lastExecuteTime).inMilliseconds;
|
||||
return remaining > 0 ? remaining : 0;
|
||||
}
|
||||
|
||||
/// 🔧 清除指定 key 的所有计时器(调试用)
|
||||
static void clear({String? key}) {
|
||||
if (key != null) {
|
||||
_debounceTimers[key]?.cancel();
|
||||
_debounceTimers.remove(key);
|
||||
_throttleTimestamps.remove(key);
|
||||
} else {
|
||||
// 清除所有
|
||||
for (var timer in _debounceTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_debounceTimers.clear();
|
||||
_throttleTimestamps.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 获取防抖和限流的统计信息(调试用)
|
||||
static Map<String, dynamic> getStats() {
|
||||
return {
|
||||
'activeDebounces': _debounceTimers.keys.toList(),
|
||||
'activeThrottles': _throttleTimestamps.keys.toList(),
|
||||
'totalActiveTimers': _debounceTimers.length + _throttleTimestamps.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 防抖辅助类 - 用于在 Controller 中创建防抖版本的方法
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// class MyController extends GetxController {
|
||||
/// late final _debouncer = KRDebouncedMethod(
|
||||
/// key: 'mode_switch',
|
||||
/// delay: Duration(milliseconds: 300),
|
||||
/// );
|
||||
///
|
||||
/// void kr_updateConnectionType(KRConnectionType type) {
|
||||
/// _debouncer.call(() async {
|
||||
/// // 实际的业务逻辑
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
class KRDebouncedMethod {
|
||||
final String key;
|
||||
final Duration delay;
|
||||
|
||||
KRDebouncedMethod({
|
||||
required this.key,
|
||||
this.delay = const Duration(milliseconds: 300),
|
||||
});
|
||||
|
||||
void call(VoidCallback action) {
|
||||
KRDebounceThrottleUtil.debounce(
|
||||
key: key,
|
||||
delay: delay,
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> callAsync(Future<void> Function() action) async {
|
||||
return KRDebounceThrottleUtil.debounceAsync(
|
||||
key: key,
|
||||
delay: delay,
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
KRDebounceThrottleUtil.clear(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 限流辅助类 - 用于在 Controller 中创建限流版本的方法
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// class MyController extends GetxController {
|
||||
/// late final _throttler = KRThrottledMethod(
|
||||
/// key: 'refresh',
|
||||
/// duration: Duration(seconds: 2),
|
||||
/// );
|
||||
///
|
||||
/// void kr_refreshAll() {
|
||||
/// if (_throttler.canExecute()) {
|
||||
/// // 执行刷新逻辑
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
class KRThrottledMethod {
|
||||
final String key;
|
||||
final Duration duration;
|
||||
|
||||
KRThrottledMethod({
|
||||
required this.key,
|
||||
required this.duration,
|
||||
});
|
||||
|
||||
bool canExecute() {
|
||||
return KRDebounceThrottleUtil.throttle(key: key, duration: duration);
|
||||
}
|
||||
|
||||
Future<bool> executeAsync(Future<void> Function() action) async {
|
||||
return KRDebounceThrottleUtil.throttleAsync(
|
||||
key: key,
|
||||
duration: duration,
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
|
||||
int getRemainingTime() {
|
||||
return KRDebounceThrottleUtil.getRemainingThrottleTime(
|
||||
key: key,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
KRDebounceThrottleUtil.clear(key: key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 文件日志工具类 - 用于诊断 UI 卡死问题
|
||||
///
|
||||
/// 将关键操作的时间戳写入 日志.log 文件,方便排查问题
|
||||
/// 使用全局开关控制是否启用日志写入
|
||||
class KRFileLogger {
|
||||
static const String _logFileName = '日志.log';
|
||||
|
||||
/// 🔧 全局日志开关 - 修改为 true 可启用文件日志写入
|
||||
static const bool _enableFileLogging = true; // ⚠️ 调试模式:开启日志分析UI阻塞 // ← 诊断完成后改回 false
|
||||
|
||||
static File? _logFile;
|
||||
|
||||
/// 初始化日志文件(应用启动时调用)
|
||||
static Future<void> initialize() async {
|
||||
if (!_enableFileLogging) return;
|
||||
|
||||
try {
|
||||
// 获取应用根目录(Windows 下通常是 exe 所在目录)
|
||||
final appDir = Directory.current;
|
||||
final logPath = p.join(appDir.path, _logFileName);
|
||||
_logFile = File(logPath);
|
||||
|
||||
// 如果日志文件已存在且大于 5MB,清空它
|
||||
if (await _logFile!.exists()) {
|
||||
final stat = await _logFile!.stat();
|
||||
if (stat.size > 5 * 1024 * 1024) { // 5MB
|
||||
await _logFile!.writeAsString('');
|
||||
await _writeRaw('=== 日志文件已清空(超过 5MB)===\n');
|
||||
}
|
||||
}
|
||||
|
||||
await _writeRaw('=== 日志系统初始化 - ${DateTime.now().toIso8601String()} ===\n');
|
||||
} catch (e) {
|
||||
// 初始化失败,静默处理,不影响应用
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入日志(带时间戳)
|
||||
static Future<void> log(String message) async {
|
||||
if (!_enableFileLogging || _logFile == null) return;
|
||||
|
||||
try {
|
||||
final timestamp = DateTime.now().toIso8601String();
|
||||
final logLine = '[$timestamp] $message\n';
|
||||
await _logFile!.writeAsString(logLine, mode: FileMode.append);
|
||||
} catch (e) {
|
||||
// 日志写入失败,静默处理,不要破坏主流程
|
||||
}
|
||||
}
|
||||
|
||||
/// 直接写入原始内容(用于特殊格式)
|
||||
static Future<void> _writeRaw(String content) async {
|
||||
if (!_enableFileLogging || _logFile == null) return;
|
||||
|
||||
try {
|
||||
await _logFile!.writeAsString(content, mode: FileMode.append);
|
||||
} catch (e) {
|
||||
// 日志写入失败,静默处理
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入分隔线(用于区分不同的操作)
|
||||
static Future<void> separator() async {
|
||||
if (!_enableFileLogging) return;
|
||||
await _writeRaw('\n---\n');
|
||||
}
|
||||
|
||||
/// 清空日志文件
|
||||
static Future<void> clear() async {
|
||||
if (!_enableFileLogging || _logFile == null) return;
|
||||
|
||||
try {
|
||||
await _logFile!.writeAsString('');
|
||||
await _writeRaw('=== 日志已清空 - ${DateTime.now().toIso8601String()} ===\n');
|
||||
} catch (e) {
|
||||
// 清空失败,静默处理
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,11 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
|
||||
const WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(800, 668),
|
||||
minimumSize: Size(400, 334),
|
||||
minimumSize: Size(800, 668),
|
||||
center: true,
|
||||
backgroundColor: Colors.white,
|
||||
skipTaskbar: false,
|
||||
title: 'Hi快VPN',
|
||||
title: 'Kaer VPN',
|
||||
titleBarStyle: TitleBarStyle.normal,
|
||||
windowButtonVisibility: true,
|
||||
);
|
||||
@@ -47,16 +47,17 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
await windowManager.setTitle('HiFastVPN');
|
||||
await windowManager.setSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(400, 334));
|
||||
await windowManager.setMinimumSize(const Size(800, 668));
|
||||
await windowManager.center();
|
||||
await windowManager.show();
|
||||
// 阻止窗口关闭
|
||||
await windowManager.setPreventClose(true);
|
||||
} else {
|
||||
await windowManager.setTitle('HiFastVPN');
|
||||
await windowManager.setTitle('Kaer VPN');
|
||||
await windowManager.setSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(400, 334));
|
||||
await windowManager.setMinimumSize(const Size(800, 668));
|
||||
await windowManager.center();
|
||||
await windowManager.show(); // macOS 也需要显式显示窗口
|
||||
}
|
||||
|
||||
// 初始化托盘
|
||||
@@ -94,7 +95,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
/// 初始化平台通道
|
||||
void _initPlatformChannel() {
|
||||
if (Platform.isMacOS) {
|
||||
const platform = MethodChannel('hifast_vpn/terminate');
|
||||
const platform = MethodChannel('kaer_vpn/terminate');
|
||||
platform.setMethodCallHandler((call) async {
|
||||
if (call.method == 'onTerminate') {
|
||||
KRLogUtil.kr_i('收到应用终止通知');
|
||||
@@ -144,13 +145,71 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
final String kr_port = KRSingBoxImp.instance.kr_port.toString();
|
||||
final String proxyText =
|
||||
'export https_proxy=http://127.0.0.1:$kr_port http_proxy=http://127.0.0.1:$kr_port all_proxy=socks5://127.0.0.1:$kr_port';
|
||||
|
||||
|
||||
await Clipboard.setData(ClipboardData(text: proxyText));
|
||||
}
|
||||
|
||||
/// 退出应用
|
||||
/// ✅ 改进:先恢复窗口(如果最小化),再显示对话框
|
||||
Future<void> _exitApp() async {
|
||||
KRLogUtil.kr_i('_exitApp: 退出应用');
|
||||
|
||||
// ✅ 关键修复:先恢复窗口(从最小化状态)
|
||||
// 这样可以确保对话框可见
|
||||
try {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setAlwaysOnTop(true);
|
||||
KRLogUtil.kr_i('✅ 窗口已恢复,准备显示对话框', tag: 'WindowManager');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复窗口失败(可能已显示): $e', tag: 'WindowManager');
|
||||
}
|
||||
|
||||
// 🔧 修复:检查 VPN 是否在运行,如果运行则弹窗提醒用户
|
||||
if (KRSingBoxImp.instance.kr_status.value is! SingboxStopped) {
|
||||
KRLogUtil.kr_w('⚠️ VPN 正在运行,询问用户是否关闭', tag: 'WindowManager');
|
||||
|
||||
// 显示确认对话框
|
||||
final shouldExit = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: Text('关闭 VPN'),
|
||||
content: Text("VPN 代理正在运行。\n\n是否现在关闭 VPN 并退出应用?\n\n(应用将等待 VPN 优雅关闭,预计 3-5 秒)"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: Text('关闭并退出', style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
barrierDismissible: false,
|
||||
) ?? false;
|
||||
|
||||
// ✅ 关键修复:对话框关闭后,恢复窗口的 AlwaysOnTop 状态
|
||||
try {
|
||||
await windowManager.setAlwaysOnTop(false);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复 AlwaysOnTop 失败: $e', tag: 'WindowManager');
|
||||
}
|
||||
|
||||
if (!shouldExit) {
|
||||
KRLogUtil.kr_i('_exitApp: 用户取消退出');
|
||||
return;
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('_exitApp: 用户确认关闭 VPN 并退出');
|
||||
} else {
|
||||
// ✅ VPN 未运行,也要恢复 AlwaysOnTop 状态
|
||||
try {
|
||||
await windowManager.setAlwaysOnTop(false);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复 AlwaysOnTop 失败: $e', tag: 'WindowManager');
|
||||
}
|
||||
}
|
||||
|
||||
await _handleTerminate();
|
||||
await windowManager.destroy();
|
||||
}
|
||||
@@ -159,9 +218,9 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
Future<void> _showWindow() async {
|
||||
KRLogUtil.kr_i('_showWindow: 开始显示窗口');
|
||||
try {
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
await windowManager.setAlwaysOnTop(true);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await windowManager.setAlwaysOnTop(false);
|
||||
@@ -180,6 +239,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
if (Platform.isWindows) {
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
await windowManager.hide();
|
||||
} else if (Platform.isMacOS) {
|
||||
await windowManager.hide();
|
||||
@@ -216,9 +276,29 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
/// 处理应用终止
|
||||
Future<void> _handleTerminate() async {
|
||||
KRLogUtil.kr_i('_handleTerminate: 处理应用终止');
|
||||
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
|
||||
await KRSingBoxImp.instance.kr_stop();
|
||||
|
||||
// 🔧 修复 BUG:正确检查 VPN 状态而不是直接比较 Rx 对象
|
||||
// 之前的代码:if (KRSingBoxImp.instance.kr_status == SingboxStatus.started())
|
||||
// 问题:kr_status 是 Rx<SingboxStatus> 对象,不能直接与 SingboxStatus.started() 比较
|
||||
// 结果:该条件总是 false,导致 kr_stop() 从不被调用,VPN 不会关闭
|
||||
if (KRSingBoxImp.instance.kr_status.value is SingboxStarted) {
|
||||
KRLogUtil.kr_i('🛑 VPN 正在运行,开始关闭...', tag: 'WindowManager');
|
||||
try {
|
||||
await KRSingBoxImp.instance.kr_stop();
|
||||
KRLogUtil.kr_i('✅ VPN 已关闭', tag: 'WindowManager');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ VPN 关闭出错: $e', tag: 'WindowManager');
|
||||
}
|
||||
} else {
|
||||
KRLogUtil.kr_i('✅ VPN 未运行,无需关闭', tag: 'WindowManager');
|
||||
}
|
||||
|
||||
// 销毁托盘
|
||||
try {
|
||||
await trayManager.destroy();
|
||||
KRLogUtil.kr_i('✅ 托盘已销毁', tag: 'WindowManager');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 销毁托盘出错: $e', tag: 'WindowManager');
|
||||
}
|
||||
await trayManager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_windows_process_util.dart';
|
||||
|
||||
/// Windows DNS 管理工具类
|
||||
///
|
||||
@@ -38,28 +39,48 @@ class KRWindowsDnsUtil {
|
||||
}
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
// 🔒 添加5秒超时保护
|
||||
return await Future.value(() async {
|
||||
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 获取主网络接口
|
||||
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (interfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
_primaryInterfaceName = interfaceName;
|
||||
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||
// 1. 获取主网络接口
|
||||
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (interfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
_primaryInterfaceName = interfaceName;
|
||||
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||
|
||||
// 2. 获取当前 DNS 服务器
|
||||
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
||||
if (dnsServers.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS');
|
||||
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
||||
} else {
|
||||
_originalDnsServers = dnsServers;
|
||||
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||
}
|
||||
// 2. 获取当前 DNS 服务器
|
||||
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
||||
|
||||
return true;
|
||||
// 🔧 P0修复1: 过滤掉 127.0.0.1 (sing-box 的本地 DNS)
|
||||
// 原因:如果备份了 127.0.0.1,关闭 VPN 后恢复为 127.0.0.1,但 sing-box 已停止,导致 DNS 无法解析
|
||||
final validDnsServers = dnsServers.where((dns) => !dns.startsWith('127.')).toList();
|
||||
|
||||
if (validDnsServers.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 当前 DNS 为空或全是本地地址,设为 DHCP 自动获取', tag: 'WindowsDNS');
|
||||
if (dnsServers.isNotEmpty) {
|
||||
KRLogUtil.kr_i(' (已过滤的本地DNS: ${dnsServers.join(", ")})', tag: 'WindowsDNS');
|
||||
}
|
||||
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
||||
} else {
|
||||
_originalDnsServers = validDnsServers;
|
||||
KRLogUtil.kr_i('✅ 已备份有效 DNS: ${validDnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||
if (dnsServers.length != validDnsServers.length) {
|
||||
KRLogUtil.kr_i(' (已过滤掉 ${dnsServers.length - validDnsServers.length} 个本地地址)', tag: 'WindowsDNS');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}()).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_w('⏱️ DNS 备份操作超时(5秒),跳过备份', tag: 'WindowsDNS');
|
||||
return false;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
@@ -80,22 +101,33 @@ class KRWindowsDnsUtil {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 检查是否有备份
|
||||
if (_primaryInterfaceName == null) {
|
||||
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS');
|
||||
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (_primaryInterfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
// 🔧 P1修复: 恢复时重新检测主接口,防止网络切换导致恢复错误接口
|
||||
final currentInterface = await _kr_getPrimaryNetworkInterface();
|
||||
if (currentInterface == null) {
|
||||
KRLogUtil.kr_e('❌ 无法检测当前网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 2. 恢复原始 DNS
|
||||
// 检查接口是否变化
|
||||
if (_primaryInterfaceName != null && _primaryInterfaceName != currentInterface) {
|
||||
KRLogUtil.kr_w('⚠️ 网络接口已变化: $_primaryInterfaceName → $currentInterface', tag: 'WindowsDNS');
|
||||
KRLogUtil.kr_w(' 执行兜底恢复以确保当前接口DNS正常', tag: 'WindowsDNS');
|
||||
_primaryInterfaceName = currentInterface; // 更新为当前接口
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 使用当前检测到的接口
|
||||
_primaryInterfaceName = currentInterface;
|
||||
KRLogUtil.kr_i('🔍 当前网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 检查是否有备份的DNS
|
||||
if (_originalDnsServers == null) {
|
||||
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 2. 恢复原始 DNS
|
||||
|
||||
if (_originalDnsServers!.isEmpty) {
|
||||
// 原本是 DHCP 自动获取
|
||||
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
|
||||
@@ -130,6 +162,15 @@ class KRWindowsDnsUtil {
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 🔧 P2优化: 测试 DNS 解析是否真正可用
|
||||
KRLogUtil.kr_i('🧪 测试 DNS 解析功能...', tag: 'WindowsDNS');
|
||||
final canResolve = await _kr_testDnsResolution();
|
||||
if (!canResolve) {
|
||||
KRLogUtil.kr_w('⚠️ DNS 解析测试失败,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
KRLogUtil.kr_i('✅ DNS 解析测试通过', tag: 'WindowsDNS');
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||
@@ -187,7 +228,7 @@ class KRWindowsDnsUtil {
|
||||
Future<String?> _kr_getPrimaryNetworkInterface() async {
|
||||
try {
|
||||
// 使用 netsh 获取接口列表
|
||||
final result = await Process.run('netsh', ['interface', 'show', 'interface']);
|
||||
final result = await KRWindowsProcessUtil.runHidden('netsh', ['interface', 'show', 'interface']);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
@@ -271,7 +312,7 @@ class KRWindowsDnsUtil {
|
||||
/// 返回:DNS 服务器列表
|
||||
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
|
||||
try {
|
||||
final result = await Process.run('netsh', [
|
||||
final result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'show',
|
||||
@@ -294,10 +335,9 @@ class KRWindowsDnsUtil {
|
||||
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
|
||||
if (ipMatch != null) {
|
||||
final ip = ipMatch.group(0)!;
|
||||
// 排除本地回环地址
|
||||
if (!ip.startsWith('127.')) {
|
||||
dnsServers.add(ip);
|
||||
}
|
||||
// 🔧 关键修复:不过滤127.0.0.1,以便正确检测DNS是否还在使用sing-box的本地DNS
|
||||
// 这样在恢复DNS时,第126行的验证才能正确检测到127.0.0.1并触发兜底恢复
|
||||
dnsServers.add(ip);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +365,7 @@ class KRWindowsDnsUtil {
|
||||
|
||||
// 1. 设置主 DNS
|
||||
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
|
||||
var result = await Process.run('netsh', [
|
||||
var result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'set',
|
||||
@@ -345,7 +385,7 @@ class KRWindowsDnsUtil {
|
||||
if (dnsServers.length > 1) {
|
||||
for (int i = 1; i < dnsServers.length; i++) {
|
||||
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
|
||||
result = await Process.run('netsh', [
|
||||
result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'add',
|
||||
@@ -384,7 +424,7 @@ class KRWindowsDnsUtil {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
|
||||
|
||||
final result = await Process.run('netsh', [
|
||||
final result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'set',
|
||||
@@ -416,7 +456,7 @@ class KRWindowsDnsUtil {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
|
||||
|
||||
final result = await Process.run('ipconfig', ['/flushdns']);
|
||||
final result = await KRWindowsProcessUtil.runHidden('ipconfig', ['/flushdns']);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
|
||||
@@ -428,6 +468,51 @@ class KRWindowsDnsUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 P2优化: 测试 DNS 解析是否真正可用
|
||||
///
|
||||
/// 通过 nslookup 测试常见域名解析
|
||||
/// 返回:true 表示 DNS 可用,false 表示 DNS 不可用
|
||||
Future<bool> _kr_testDnsResolution() async {
|
||||
try {
|
||||
// 测试多个常见域名,提高成功率
|
||||
final testDomains = ['www.baidu.com', 'www.qq.com', 'dns.alidns.com'];
|
||||
|
||||
for (var domain in testDomains) {
|
||||
try {
|
||||
// 使用 nslookup 测试 DNS 解析,设置 2 秒超时
|
||||
final result = await KRWindowsProcessUtil.runHidden(
|
||||
'nslookup',
|
||||
[domain],
|
||||
).timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () {
|
||||
return ProcessResult(0, 1, '', 'Timeout');
|
||||
},
|
||||
);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
final output = result.stdout.toString();
|
||||
// 检查输出是否包含 IP 地址(简单验证)
|
||||
if (output.contains('Address:') || output.contains('地址:')) {
|
||||
KRLogUtil.kr_i('✅ DNS 解析测试通过: $domain', tag: 'WindowsDNS');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 单个域名失败,继续测试下一个
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有域名都解析失败
|
||||
KRLogUtil.kr_w('⚠️ 所有测试域名解析均失败', tag: 'WindowsDNS');
|
||||
return false;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ DNS 解析测试异常: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除备份数据
|
||||
///
|
||||
/// 在应用退出或不需要时调用
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'kr_file_logger.dart';
|
||||
|
||||
class KRWindowsProcessUtil {
|
||||
/// 🔍 调试标志:设为 true 可以追踪所有命令执行(用于排查黑窗问题)
|
||||
static const bool _debugCommandExecution = true; // ← 开启调试
|
||||
|
||||
static Future<ProcessResult> runHidden(String executable, List<String> arguments) async {
|
||||
final timestamp = DateTime.now().toString();
|
||||
// 🔧 使用非阻塞日志(不 await),避免影响执行时序
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] 🔧 runHidden 调用: $executable ${arguments.join(" ")}');
|
||||
|
||||
if (!Platform.isWindows) {
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] ⚠️ 非 Windows,使用 Process.run(可能黑窗)');
|
||||
return Process.run(executable, arguments);
|
||||
}
|
||||
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] ✅ Windows,使用 CreateProcessW(无黑窗)');
|
||||
final result = await _runHiddenWindows(executable, arguments);
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] 📤 执行完成: exitCode=${result.exitCode}');
|
||||
return result;
|
||||
}
|
||||
|
||||
static Future<int> startHidden(String executable, List<String> arguments) async {
|
||||
if (!Platform.isWindows) {
|
||||
final process = await Process.start(executable, arguments);
|
||||
return process.pid;
|
||||
}
|
||||
return _startHiddenWindows(executable, arguments);
|
||||
}
|
||||
|
||||
static String _buildCommandLine(String executable, List<String> arguments) {
|
||||
final parts = <String>[_quoteArgument(executable)];
|
||||
for (final arg in arguments) {
|
||||
parts.add(_quoteArgument(arg));
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
static bool _shouldSearchPath(String executable) {
|
||||
if (executable.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return !(executable.contains('\\') || executable.contains('/') || executable.contains(':'));
|
||||
}
|
||||
|
||||
static String _quoteArgument(String value) {
|
||||
if (value.isEmpty) {
|
||||
return '""';
|
||||
}
|
||||
final needsQuotes = value.contains(' ') || value.contains('\t') || value.contains('"');
|
||||
if (!needsQuotes) {
|
||||
return value;
|
||||
}
|
||||
final buffer = StringBuffer('"');
|
||||
var backslashes = 0;
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
final char = value[i];
|
||||
if (char == '\\') {
|
||||
backslashes++;
|
||||
continue;
|
||||
}
|
||||
if (char == '"') {
|
||||
buffer.write('\\' * (backslashes * 2 + 1));
|
||||
buffer.write('"');
|
||||
backslashes = 0;
|
||||
continue;
|
||||
}
|
||||
if (backslashes > 0) {
|
||||
buffer.write('\\' * backslashes);
|
||||
backslashes = 0;
|
||||
}
|
||||
buffer.write(char);
|
||||
}
|
||||
if (backslashes > 0) {
|
||||
buffer.write('\\' * (backslashes * 2));
|
||||
}
|
||||
buffer.write('"');
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
static Future<ProcessResult> _runHiddenWindows(String executable, List<String> arguments) async {
|
||||
final stdoutPipe = _createPipe();
|
||||
final stderrPipe = _createPipe();
|
||||
|
||||
final startupInfo = calloc<STARTUPINFO>();
|
||||
final processInfo = calloc<PROCESS_INFORMATION>();
|
||||
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
|
||||
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
|
||||
final stdInput = _getStdInputHandle();
|
||||
|
||||
startupInfo.ref
|
||||
..cb = sizeOf<STARTUPINFO>()
|
||||
..dwFlags = STARTF_USESTDHANDLES
|
||||
..hStdInput = stdInput
|
||||
..hStdOutput = stdoutPipe.write
|
||||
..hStdError = stderrPipe.write;
|
||||
|
||||
final created = _CreateProcessW(
|
||||
applicationName,
|
||||
commandLine,
|
||||
nullptr,
|
||||
nullptr,
|
||||
TRUE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
nullptr,
|
||||
startupInfo,
|
||||
processInfo,
|
||||
);
|
||||
|
||||
calloc.free(commandLine);
|
||||
if (applicationName != nullptr) {
|
||||
calloc.free(applicationName);
|
||||
}
|
||||
|
||||
if (created == 0) {
|
||||
_closeHandle(stdoutPipe.read);
|
||||
_closeHandle(stdoutPipe.write);
|
||||
_closeHandle(stderrPipe.read);
|
||||
_closeHandle(stderrPipe.write);
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
throw Exception('CreateProcessW failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
_closeHandle(stdoutPipe.write);
|
||||
_closeHandle(stderrPipe.write);
|
||||
|
||||
final output = await _collectOutput(processInfo.ref.hProcess, stdoutPipe.read, stderrPipe.read);
|
||||
final exitCode = _getExitCode(processInfo.ref.hProcess);
|
||||
|
||||
_closeHandle(stdoutPipe.read);
|
||||
_closeHandle(stderrPipe.read);
|
||||
_closeHandle(processInfo.ref.hThread);
|
||||
_closeHandle(processInfo.ref.hProcess);
|
||||
|
||||
final pid = processInfo.ref.dwProcessId;
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
|
||||
return ProcessResult(pid, exitCode, output.stdout, output.stderr);
|
||||
}
|
||||
|
||||
static Future<int> _startHiddenWindows(String executable, List<String> arguments) async {
|
||||
final startupInfo = calloc<STARTUPINFO>();
|
||||
final processInfo = calloc<PROCESS_INFORMATION>();
|
||||
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
|
||||
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
|
||||
|
||||
startupInfo.ref.cb = sizeOf<STARTUPINFO>();
|
||||
|
||||
final created = _CreateProcessW(
|
||||
applicationName,
|
||||
commandLine,
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
nullptr,
|
||||
startupInfo,
|
||||
processInfo,
|
||||
);
|
||||
|
||||
calloc.free(commandLine);
|
||||
if (applicationName != nullptr) {
|
||||
calloc.free(applicationName);
|
||||
}
|
||||
|
||||
if (created == 0) {
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
throw Exception('CreateProcessW failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
final pid = processInfo.ref.dwProcessId;
|
||||
|
||||
_closeHandle(processInfo.ref.hThread);
|
||||
_closeHandle(processInfo.ref.hProcess);
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
static _Pipe _createPipe() {
|
||||
final readHandle = calloc<Pointer<Void>>();
|
||||
final writeHandle = calloc<Pointer<Void>>();
|
||||
final securityAttributes = calloc<SECURITY_ATTRIBUTES>();
|
||||
securityAttributes.ref
|
||||
..nLength = sizeOf<SECURITY_ATTRIBUTES>()
|
||||
..bInheritHandle = TRUE
|
||||
..lpSecurityDescriptor = nullptr;
|
||||
|
||||
final created = _CreatePipe(readHandle, writeHandle, securityAttributes, 0);
|
||||
calloc.free(securityAttributes);
|
||||
if (created == 0) {
|
||||
calloc.free(readHandle);
|
||||
calloc.free(writeHandle);
|
||||
throw Exception('CreatePipe failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
final readValue = readHandle.value;
|
||||
final writeValue = writeHandle.value;
|
||||
calloc.free(readHandle);
|
||||
calloc.free(writeHandle);
|
||||
|
||||
final infoResult = _SetHandleInformation(readValue, HANDLE_FLAG_INHERIT, 0);
|
||||
if (infoResult == 0) {
|
||||
_closeHandle(readValue);
|
||||
_closeHandle(writeValue);
|
||||
throw Exception('SetHandleInformation failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
return _Pipe(readValue, writeValue);
|
||||
}
|
||||
|
||||
static Pointer<Void> _getStdInputHandle() {
|
||||
final handle = _GetStdHandle(STD_INPUT_HANDLE);
|
||||
if (handle == INVALID_HANDLE_VALUE || handle == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return Pointer<Void>.fromAddress(handle);
|
||||
}
|
||||
|
||||
static Future<_ProcessOutput> _collectOutput(
|
||||
Pointer<Void> process,
|
||||
Pointer<Void> stdoutHandle,
|
||||
Pointer<Void> stderrHandle,
|
||||
) async {
|
||||
final stdoutBuilder = BytesBuilder();
|
||||
final stderrBuilder = BytesBuilder();
|
||||
|
||||
while (true) {
|
||||
final stdoutRead = _drainPipe(stdoutHandle, stdoutBuilder);
|
||||
final stderrRead = _drainPipe(stderrHandle, stderrBuilder);
|
||||
final waitResult = _WaitForSingleObject(process, 0);
|
||||
if (waitResult == WAIT_OBJECT_0) {
|
||||
break;
|
||||
}
|
||||
if (waitResult == WAIT_FAILED) {
|
||||
throw Exception('WaitForSingleObject failed: ${_GetLastError()}');
|
||||
}
|
||||
if (!stdoutRead && !stderrRead) {
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
} else {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
}
|
||||
|
||||
while (_drainPipe(stdoutHandle, stdoutBuilder) || _drainPipe(stderrHandle, stderrBuilder)) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
|
||||
return _ProcessOutput(
|
||||
_decodeOutput(stdoutBuilder),
|
||||
_decodeOutput(stderrBuilder),
|
||||
);
|
||||
}
|
||||
|
||||
static bool _drainPipe(Pointer<Void> handle, BytesBuilder builder) {
|
||||
final buffer = calloc<Uint8>(4096);
|
||||
final bytesRead = calloc<Uint32>();
|
||||
final available = calloc<Uint32>();
|
||||
var didRead = false;
|
||||
|
||||
while (true) {
|
||||
final peekOk = _PeekNamedPipe(handle, nullptr, 0, nullptr, available, nullptr);
|
||||
if (peekOk == 0 || available.value == 0) {
|
||||
break;
|
||||
}
|
||||
final toRead = available.value < 4096 ? available.value : 4096;
|
||||
final ok = _ReadFile(handle, buffer.cast<Void>(), toRead, bytesRead, nullptr);
|
||||
final read = ok == 0 ? 0 : bytesRead.value;
|
||||
if (read == 0) {
|
||||
break;
|
||||
}
|
||||
builder.add(buffer.asTypedList(read));
|
||||
didRead = true;
|
||||
}
|
||||
|
||||
calloc.free(buffer);
|
||||
calloc.free(bytesRead);
|
||||
calloc.free(available);
|
||||
|
||||
return didRead;
|
||||
}
|
||||
|
||||
static String _decodeOutput(BytesBuilder builder) {
|
||||
if (builder.length == 0) {
|
||||
return '';
|
||||
}
|
||||
final bytes = builder.toBytes();
|
||||
try {
|
||||
return systemEncoding.decode(bytes);
|
||||
} catch (_) {
|
||||
return utf8.decode(bytes, allowMalformed: true);
|
||||
}
|
||||
}
|
||||
|
||||
static int _getExitCode(Pointer<Void> process) {
|
||||
final exitCode = calloc<Uint32>();
|
||||
final ok = _GetExitCodeProcess(process, exitCode);
|
||||
final code = ok == 0 ? -1 : exitCode.value;
|
||||
calloc.free(exitCode);
|
||||
return code;
|
||||
}
|
||||
|
||||
static void _closeHandle(Pointer<Void> handle) {
|
||||
if (handle == nullptr) {
|
||||
return;
|
||||
}
|
||||
_CloseHandle(handle);
|
||||
}
|
||||
|
||||
// 🔧 WinINet API helpers for proxy settings
|
||||
/// 查询当前系统代理设置
|
||||
static String? queryWindowsProxyServer() {
|
||||
if (!Platform.isWindows) return null;
|
||||
|
||||
try {
|
||||
final bufferSize = calloc<Uint32>();
|
||||
bufferSize.value = sizeOf<INTERNET_PROXY_INFO>();
|
||||
|
||||
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
|
||||
|
||||
final result = _InternetQueryOptionW(nullptr, INTERNET_OPTION_PROXY, proxyInfo.cast<Void>(), bufferSize);
|
||||
if (result == 0) {
|
||||
calloc.free(bufferSize);
|
||||
calloc.free(proxyInfo);
|
||||
return null;
|
||||
}
|
||||
|
||||
final proxyServer = proxyInfo.ref.lpszProxy.toDartString();
|
||||
calloc.free(bufferSize);
|
||||
calloc.free(proxyInfo);
|
||||
|
||||
return proxyServer.isEmpty ? null : proxyServer;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置系统代理
|
||||
static bool setWindowsProxyServer(String? server) {
|
||||
if (!Platform.isWindows) return false;
|
||||
|
||||
try {
|
||||
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
|
||||
|
||||
if (server != null && server.isNotEmpty) {
|
||||
// 设置代理模式
|
||||
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
|
||||
proxyInfo.ref.lpszProxy = server.toNativeUtf16();
|
||||
proxyInfo.ref.lpszProxyBypass = ''.toNativeUtf16();
|
||||
} else {
|
||||
// 禁用代理
|
||||
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_DIRECT;
|
||||
proxyInfo.ref.lpszProxy = nullptr;
|
||||
proxyInfo.ref.lpszProxyBypass = nullptr;
|
||||
}
|
||||
|
||||
final result = _InternetSetOptionW(
|
||||
nullptr,
|
||||
INTERNET_OPTION_PROXY,
|
||||
proxyInfo.cast<Void>(),
|
||||
sizeOf<INTERNET_PROXY_INFO>(),
|
||||
);
|
||||
|
||||
if (server != null && server.isNotEmpty) {
|
||||
calloc.free(proxyInfo.ref.lpszProxy);
|
||||
calloc.free(proxyInfo.ref.lpszProxyBypass);
|
||||
}
|
||||
calloc.free(proxyInfo);
|
||||
|
||||
return result != 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 禁用系统代理
|
||||
static bool disableWindowsProxy() {
|
||||
return setWindowsProxyServer(null);
|
||||
}
|
||||
}
|
||||
|
||||
class _Pipe {
|
||||
final Pointer<Void> read;
|
||||
final Pointer<Void> write;
|
||||
|
||||
_Pipe(this.read, this.write);
|
||||
}
|
||||
|
||||
class _ProcessOutput {
|
||||
final String stdout;
|
||||
final String stderr;
|
||||
|
||||
_ProcessOutput(this.stdout, this.stderr);
|
||||
}
|
||||
|
||||
const int TRUE = 1;
|
||||
const int FALSE = 0;
|
||||
|
||||
const int STARTF_USESTDHANDLES = 0x00000100;
|
||||
const int CREATE_NO_WINDOW = 0x08000000;
|
||||
const int HANDLE_FLAG_INHERIT = 0x00000001;
|
||||
const int WAIT_OBJECT_0 = 0x00000000;
|
||||
const int WAIT_FAILED = 0xFFFFFFFF;
|
||||
const int STD_INPUT_HANDLE = -10;
|
||||
const int INVALID_HANDLE_VALUE = -1;
|
||||
|
||||
final class SECURITY_ATTRIBUTES extends Struct {
|
||||
@Uint32()
|
||||
external int nLength;
|
||||
|
||||
external Pointer<Void> lpSecurityDescriptor;
|
||||
|
||||
@Int32()
|
||||
external int bInheritHandle;
|
||||
}
|
||||
|
||||
final class STARTUPINFO extends Struct {
|
||||
@Uint32()
|
||||
external int cb;
|
||||
|
||||
external Pointer<Utf16> lpReserved;
|
||||
|
||||
external Pointer<Utf16> lpDesktop;
|
||||
|
||||
external Pointer<Utf16> lpTitle;
|
||||
|
||||
@Uint32()
|
||||
external int dwX;
|
||||
|
||||
@Uint32()
|
||||
external int dwY;
|
||||
|
||||
@Uint32()
|
||||
external int dwXSize;
|
||||
|
||||
@Uint32()
|
||||
external int dwYSize;
|
||||
|
||||
@Uint32()
|
||||
external int dwXCountChars;
|
||||
|
||||
@Uint32()
|
||||
external int dwYCountChars;
|
||||
|
||||
@Uint32()
|
||||
external int dwFillAttribute;
|
||||
|
||||
@Uint32()
|
||||
external int dwFlags;
|
||||
|
||||
@Uint16()
|
||||
external int wShowWindow;
|
||||
|
||||
@Uint16()
|
||||
external int cbReserved2;
|
||||
|
||||
external Pointer<Uint8> lpReserved2;
|
||||
|
||||
external Pointer<Void> hStdInput;
|
||||
|
||||
external Pointer<Void> hStdOutput;
|
||||
|
||||
external Pointer<Void> hStdError;
|
||||
}
|
||||
|
||||
final class PROCESS_INFORMATION extends Struct {
|
||||
external Pointer<Void> hProcess;
|
||||
|
||||
external Pointer<Void> hThread;
|
||||
|
||||
@Uint32()
|
||||
external int dwProcessId;
|
||||
|
||||
@Uint32()
|
||||
external int dwThreadId;
|
||||
}
|
||||
|
||||
final DynamicLibrary _kernel32 = DynamicLibrary.open('kernel32.dll');
|
||||
|
||||
final _CreatePipe = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, Uint32),
|
||||
int Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, int)>(
|
||||
'CreatePipe',
|
||||
);
|
||||
|
||||
final _SetHandleInformation = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Uint32, Uint32),
|
||||
int Function(Pointer<Void>, int, int)>(
|
||||
'SetHandleInformation',
|
||||
);
|
||||
|
||||
final _CreateProcessW = _kernel32.lookupFunction<
|
||||
Int32 Function(
|
||||
Pointer<Utf16>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
Int32,
|
||||
Uint32,
|
||||
Pointer<Void>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<STARTUPINFO>,
|
||||
Pointer<PROCESS_INFORMATION>,
|
||||
),
|
||||
int Function(
|
||||
Pointer<Utf16>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
int,
|
||||
int,
|
||||
Pointer<Void>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<STARTUPINFO>,
|
||||
Pointer<PROCESS_INFORMATION>,
|
||||
)>(
|
||||
'CreateProcessW',
|
||||
);
|
||||
|
||||
final _PeekNamedPipe = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>),
|
||||
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>)>(
|
||||
'PeekNamedPipe',
|
||||
);
|
||||
|
||||
final _ReadFile = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Void>),
|
||||
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Void>)>(
|
||||
'ReadFile',
|
||||
);
|
||||
|
||||
final _CloseHandle = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>),
|
||||
int Function(Pointer<Void>)>(
|
||||
'CloseHandle',
|
||||
);
|
||||
|
||||
final _WaitForSingleObject = _kernel32.lookupFunction<
|
||||
Uint32 Function(Pointer<Void>, Uint32),
|
||||
int Function(Pointer<Void>, int)>(
|
||||
'WaitForSingleObject',
|
||||
);
|
||||
|
||||
final _GetStdHandle = _kernel32.lookupFunction<
|
||||
IntPtr Function(Int32),
|
||||
int Function(int)>(
|
||||
'GetStdHandle',
|
||||
);
|
||||
|
||||
final _GetExitCodeProcess = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Uint32>),
|
||||
int Function(Pointer<Void>, Pointer<Uint32>)>(
|
||||
'GetExitCodeProcess',
|
||||
);
|
||||
|
||||
final _GetLastError = _kernel32.lookupFunction<
|
||||
Uint32 Function(),
|
||||
int Function()>(
|
||||
'GetLastError',
|
||||
);
|
||||
|
||||
// 🔧 WinINet API for proxy settings - 用于替代 reg 命令,消除黑屏
|
||||
final DynamicLibrary _wininet = DynamicLibrary.open('wininet.dll');
|
||||
|
||||
const int INTERNET_OPTION_PROXY = 38;
|
||||
const int INTERNET_OPEN_TYPE_PROXY = 3;
|
||||
const int INTERNET_OPEN_TYPE_DIRECT = 1;
|
||||
|
||||
final class INTERNET_PROXY_INFO extends Struct {
|
||||
@Int32()
|
||||
external int dwAccessType;
|
||||
|
||||
external Pointer<Utf16> lpszProxy;
|
||||
|
||||
external Pointer<Utf16> lpszProxyBypass;
|
||||
}
|
||||
|
||||
/// WinINet InternetSetOption API - 用于设置系统代理
|
||||
final _InternetSetOptionW = _wininet.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Uint32),
|
||||
int Function(Pointer<Void>, int, Pointer<Void>, int)>(
|
||||
'InternetSetOptionW',
|
||||
);
|
||||
|
||||
/// WinINet InternetQueryOption API - 用于查询系统代理
|
||||
final _InternetQueryOptionW = _wininet.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Pointer<Uint32>),
|
||||
int Function(Pointer<Void>, int, Pointer<Void>, Pointer<Uint32>)>(
|
||||
'InternetQueryOptionW',
|
||||
);
|
||||
Reference in New Issue
Block a user