From 6ef08c9e7b7426058d343842223c282bb8bf2658 Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Tue, 13 Jan 2026 19:26:03 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20win=E4=BB=A3=E7=A0=81=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/services/kr_subscribe_service.dart | 13 +- lib/app/utils/kr_window_manager.dart | 88 ++- lib/singbox/service/ffi_singbox_service.dart | 548 ++++++++++++------- lib/utils/isolate_worker.dart | 21 + windows/CMakeLists.txt | 18 +- windows/runner/CMakeLists.txt | 1 - windows/runner/Runner.rc | 2 + windows/runner/main.cpp | 49 +- windows/runner/runner.exe.manifest | 19 + windows/runner/utils.cpp | 22 + windows/runner/utils.h | 4 + windows/runner/win32_window.cpp | 6 +- 12 files changed, 586 insertions(+), 205 deletions(-) create mode 100644 lib/utils/isolate_worker.dart diff --git a/lib/app/services/kr_subscribe_service.dart b/lib/app/services/kr_subscribe_service.dart index 6e2f3ba..94f54ca 100755 --- a/lib/app/services/kr_subscribe_service.dart +++ b/lib/app/services/kr_subscribe_service.dart @@ -711,8 +711,17 @@ class KRSubscribeService { kr_trialRemainingTime.value = ''; /// 停止 - if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) { - await KRSingBoxImp.instance.kr_stop(); + // ✅ 关键修复:使用 is 类型检查替代 == 比较 + // 原因:kr_status 是 Rx 类型,直接 == 比较不可靠 + if (KRSingBoxImp.instance.kr_status.value is SingboxStarted) { + KRLogUtil.kr_i('🛑 清理时检测到 VPN 正在运行,停止 VPN...', tag: 'SubscribeService'); + try { + // ✅ 不 await,改为异步执行,防止阻塞清理流程 + // 由于主线程已经调用了 kr_stop(),这里不需要再次等待 + unawaited(KRSingBoxImp.instance.kr_stop()); + } catch (e) { + KRLogUtil.kr_w('⚠️ 清理时停止 VPN 失败: $e', tag: 'SubscribeService'); + } } // 更新UI数据 diff --git a/lib/app/utils/kr_window_manager.dart b/lib/app/utils/kr_window_manager.dart index cebd995..7efa7ad 100755 --- a/lib/app/utils/kr_window_manager.dart +++ b/lib/app/utils/kr_window_manager.dart @@ -62,6 +62,7 @@ class KRWindowManager with WindowListener, TrayListener { await windowManager.setMaximumSize(const Size(420, 900)); await windowManager.setResizable(true); await windowManager.center(); + await windowManager.show(); // macOS 也需要显式显示窗口 } // 初始化托盘 @@ -154,8 +155,66 @@ class KRWindowManager with WindowListener, TrayListener { } /// 退出应用 + /// ✅ 改进:先恢复窗口(如果最小化),再显示对话框 Future _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( + 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(); } @@ -164,9 +223,9 @@ class KRWindowManager with WindowListener, TrayListener { Future _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); @@ -185,6 +244,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(); @@ -221,9 +281,29 @@ class KRWindowManager with WindowListener, TrayListener { /// 处理应用终止 Future _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.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(); } } diff --git a/lib/singbox/service/ffi_singbox_service.dart b/lib/singbox/service/ffi_singbox_service.dart index 261188e..27b5fcb 100755 --- a/lib/singbox/service/ffi_singbox_service.dart +++ b/lib/singbox/service/ffi_singbox_service.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; -import 'package:combine/combine.dart'; +import 'package:kaer_with_panels/utils/isolate_worker.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; import 'package:kaer_with_panels/core/model/directories.dart'; @@ -50,6 +50,7 @@ class FFISingboxService with InfraLogger implements SingboxService { @override Future init() async { loggy.debug("initializing"); + _box.setupOnce(NativeApi.initializeApiDLData); _statusReceiver = ReceivePort('service status receiver'); final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent); _status = ValueConnectableStream.seeded( @@ -60,162 +61,197 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither setup( - Directories directories, - bool debug, - ) { + Directories directories, + bool debug, + ) { final port = _statusReceiver.sendPort.nativePort; - return TaskEither( - () => CombineWorker().execute( - () { - _box.setupOnce(NativeApi.initializeApiDLData); - final err = _box - .setup( - directories.baseDir.path.toNativeUtf8().cast(), - directories.workingDir.path.toNativeUtf8().cast(), - directories.tempDir.path.toNativeUtf8().cast(), - port, - debug ? 1 : 0, - ) - .cast() - .toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + final baseDir = directories.baseDir.path; + final workingDir = directories.workingDir.path; + final tempDir = directories.tempDir.path; + final debugFlag = debug ? 1 : 0; + + return TaskEither(() async { + try { + final startTime = DateTime.now(); + _logger.debug('[黑屏调试] setup() 开始调用 libcore.dll - $startTime'); + + final err = await IsolateWorker().execute( + () => _ffiSetup(baseDir, workingDir, tempDir, port, debugFlag), + allowSyncFallback: false, + ); + + final endTime = DateTime.now(); + final durationMs = endTime.difference(startTime).inMilliseconds; + _logger.debug('[黑屏调试] setup() 完成(耗时: ${durationMs}ms)'); + + if (err != null && err.isNotEmpty) { + _logger.error('[黑屏调试] setup() 错误: $err'); + return left(err); + } + return right(unit); + } catch (e) { + _logger.error('[黑屏调试] setup() 异常: $e'); + return left(e.toString()); + } + }); } @override TaskEither validateConfigByPath( - String path, - String tempPath, - bool debug, - ) { - return TaskEither( - () => CombineWorker().execute( - () { - final err = _box - .parse( - path.toNativeUtf8().cast(), - tempPath.toNativeUtf8().cast(), - debug ? 1 : 0, - ) - .cast() - .toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + String path, + String tempPath, + bool debug, + ) { + final debugFlag = debug ? 1 : 0; + return TaskEither(() async { + try { + final err = await IsolateWorker().execute( + () => _ffiValidateConfig(path, tempPath, debugFlag), + allowSyncFallback: false, + ); + if (err != null && err.isNotEmpty) { + return left(err); + } + return right(unit); + } catch (e) { + return left(e.toString()); + } + }); } @override TaskEither changeOptions(SingboxConfigOption options) { - return TaskEither( - () => CombineWorker().execute( - () { - final json = jsonEncode(options.toJson()); - final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast().toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + final json = jsonEncode(options.toJson()); + return TaskEither(() async { + try { + final startTime = DateTime.now(); + _logger.debug('[黑屏调试] changeOptions 开始调用 libcore.dll - $startTime'); + + final err = await IsolateWorker().execute( + () => _ffiChangeOptions(json), + allowSyncFallback: false, + ); + + final endTime = DateTime.now(); + final durationMs = endTime.difference(startTime).inMilliseconds; + _logger.debug('[黑屏调试] changeOptions 完成(耗时: ${durationMs}ms)'); + + if (err != null && err.isNotEmpty) { + _logger.error('[黑屏调试] changeOptions 错误: $err'); + return left(err); + } + return right(unit); + } catch (e) { + _logger.error('[黑屏调试] changeOptions 异常: $e'); + return left(e.toString()); + } + }); } @override TaskEither generateFullConfigByPath( - String path, - ) { - return TaskEither( - () => CombineWorker().execute( - () { - final response = _box - .generateConfig( - path.toNativeUtf8().cast(), - ) - .cast() - .toDartString(); - if (response.startsWith("error")) { - return left(response.replaceFirst("error", "")); - } - return right(response); - }, - ), - ); + String path, + ) { + return TaskEither(() async { + try { + final result = await IsolateWorker().execute( + () => _ffiGenerateFullConfig(path), + allowSyncFallback: false, + ); + final ok = result.isNotEmpty && result[0] == true; + final payload = result.length > 1 ? result[1] as String : ''; + if (!ok) { + return left(payload); + } + return right(payload); + } catch (e) { + return left(e.toString()); + } + }); } @override TaskEither start( - String configPath, - String name, - bool disableMemoryLimit, - ) { + String configPath, + String name, + bool disableMemoryLimit, + ) { loggy.debug("starting, memory limit: [${!disableMemoryLimit}]"); - return TaskEither( - () => CombineWorker().execute( - () { - final err = _box - .start( - configPath.toNativeUtf8().cast(), - disableMemoryLimit ? 1 : 0, - ) - .cast() - .toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + return TaskEither(() async { + try { + final startTime = DateTime.now(); + _logger.debug('[黑屏调试] start() 开始调用 libcore.dll - $startTime'); + + final err = await IsolateWorker().execute( + () => _ffiStart(configPath, disableMemoryLimit), + allowSyncFallback: false, + ); + + final endTime = DateTime.now(); + final durationMs = endTime.difference(startTime).inMilliseconds; + _logger.debug('[黑屏调试] start() 完成(耗时: ${durationMs}ms)'); + + if (err != null && err.isNotEmpty) { + _logger.error('[黑屏调试] start() 错误: $err'); + return left(err); + } + return right(unit); + } catch (e) { + _logger.error('[黑屏调试] start() 异常: $e'); + return left(e.toString()); + } + }); } @override TaskEither stop() { - return TaskEither( - () => CombineWorker().execute( - () { - final err = _box.stop().cast().toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + return TaskEither(() async { + try { + final startTime = DateTime.now(); + _logger.debug('[黑屏调试] stop() 开始调用 libcore.dll - $startTime'); + + final err = await IsolateWorker().execute( + _ffiStop, + allowSyncFallback: false, + ); + + final endTime = DateTime.now(); + final durationMs = endTime.difference(startTime).inMilliseconds; + _logger.debug('[黑屏调试] stop() 完成(耗时: ${durationMs}ms)'); + + if (err != null && err.isNotEmpty) { + _logger.error('[黑屏调试] stop() 错误: $err'); + return left(err); + } + return right(unit); + } catch (e) { + _logger.error('[黑屏调试] stop() 异常: $e'); + return left(e.toString()); + } + }); } @override TaskEither restart( - String configPath, - String name, - bool disableMemoryLimit, - ) { + String configPath, + String name, + bool disableMemoryLimit, + ) { loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]"); - return TaskEither( - () => CombineWorker().execute( - () { - final err = _box - .restart( - configPath.toNativeUtf8().cast(), - disableMemoryLimit ? 1 : 0, - ) - .cast() - .toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + return TaskEither(() async { + try { + final err = await IsolateWorker().execute( + () => _ffiRestart(configPath, disableMemoryLimit), + allowSyncFallback: false, + ); + if (err != null && err.isNotEmpty) { + return left(err); + } + return right(unit); + } catch (e) { + return left(e.toString()); + } + }); } @override @@ -243,7 +279,7 @@ class FFISingboxService with InfraLogger implements SingboxService { _serviceStatsStream = null; }, ).map( - (event) { + (event) { if (event case String _) { if (event.startsWith('error:')) { loggy.error("[service stats client] error received: $event"); @@ -283,7 +319,7 @@ class FFISingboxService with InfraLogger implements SingboxService { } }, ).map( - (event) { + (event) { if (event case String _) { if (event.startsWith('error:')) { logger.error("error received: $event"); @@ -327,7 +363,7 @@ class FFISingboxService with InfraLogger implements SingboxService { } }, ).map( - (event) { + (event) { if (event case String _) { if (event.startsWith('error:')) { logger.error(event); @@ -359,38 +395,38 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither selectOutbound(String groupTag, String outboundTag) { - return TaskEither( - () => CombineWorker().execute( - () { - final err = _box - .selectOutbound( - groupTag.toNativeUtf8().cast(), - outboundTag.toNativeUtf8().cast(), - ) - .cast() - .toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + return TaskEither(() async { + try { + final err = await IsolateWorker().execute( + () => _ffiSelectOutbound(groupTag, outboundTag), + allowSyncFallback: false, + ); + if (err != null && err.isNotEmpty) { + return left(err); + } + return right(unit); + } catch (e) { + return left(e.toString()); + } + }); } @override TaskEither urlTest(String groupTag) { - return TaskEither( - () => CombineWorker().execute( - () { - final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast().toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); + return TaskEither(() async { + try { + final err = await IsolateWorker().execute( + () => _ffiUrlTest(groupTag), + allowSyncFallback: false, + ); + if (err != null && err.isNotEmpty) { + return left(err); + } + return right(unit); + } catch (e) { + return left(e.toString()); + } + }); } final _logBuffer = []; @@ -409,14 +445,10 @@ class FFISingboxService with InfraLogger implements SingboxService { @override TaskEither clearLogs() { - return TaskEither( - () => CombineWorker().execute( - () { - _logBuffer.clear(); - return right(unit); - }, - ), - ); + return TaskEither(() async { + _logBuffer.clear(); + return right(unit); + }); } Future> _readLogFile(File file) async { @@ -443,23 +475,165 @@ class FFISingboxService with InfraLogger implements SingboxService { required String previousAccessToken, }) { loggy.debug("generating warp config"); - return TaskEither( - () => CombineWorker().execute( - () { - final response = _box - .generateWarpConfig( - licenseKey.toNativeUtf8().cast(), - previousAccountId.toNativeUtf8().cast(), - previousAccessToken.toNativeUtf8().cast(), - ) - .cast() - .toDartString(); - if (response.startsWith("error:")) { - return left(response.replaceFirst('error:', "")); - } - return right(warpFromJson(jsonDecode(response))); - }, - ), - ); + return TaskEither(() async { + try { + final result = await IsolateWorker().execute( + () => _ffiGenerateWarpConfig(licenseKey, previousAccountId, previousAccessToken), + allowSyncFallback: false, + ); + final ok = result.isNotEmpty && result[0] == true; + final payload = result.length > 1 ? result[1] as String : ''; + if (!ok) { + return left(payload); + } + return right(warpFromJson(jsonDecode(payload))); + } catch (e) { + return left(e.toString()); + } + }); } } + +SingboxNativeLibrary _ffiLoadLibrary() { + String fullPath = ""; + if (Platform.environment.containsKey('FLUTTER_TEST')) { + fullPath = "libcore"; + } + if (Platform.isWindows) { + fullPath = p.join(fullPath, "libcore.dll"); + } else if (Platform.isMacOS) { + fullPath = p.join(fullPath, "libcore.dylib"); + } else { + fullPath = p.join(fullPath, "libcore.so"); + } + final lib = DynamicLibrary.open(fullPath); + final box = SingboxNativeLibrary(lib); + box.setupOnce(NativeApi.initializeApiDLData); + return box; +} + +String? _ffiSetup( + String baseDir, + String workingDir, + String tempDir, + int statusPort, + int debugFlag, + ) { + final box = _ffiLoadLibrary(); + final err = box + .setup( + baseDir.toNativeUtf8().cast(), + workingDir.toNativeUtf8().cast(), + tempDir.toNativeUtf8().cast(), + statusPort, + debugFlag, + ) + .cast() + .toDartString(); + return err.isEmpty ? null : err; +} + +String? _ffiValidateConfig( + String path, + String tempPath, + int debugFlag, + ) { + final box = _ffiLoadLibrary(); + final err = box + .parse( + path.toNativeUtf8().cast(), + tempPath.toNativeUtf8().cast(), + debugFlag, + ) + .cast() + .toDartString(); + return err.isEmpty ? null : err; +} + +String? _ffiChangeOptions(String optionsJson) { + final box = _ffiLoadLibrary(); + final err = box.changeHiddifyOptions(optionsJson.toNativeUtf8().cast()).cast().toDartString(); + return err.isEmpty ? null : err; +} + +List _ffiGenerateFullConfig(String path) { + final box = _ffiLoadLibrary(); + final response = box + .generateConfig( + path.toNativeUtf8().cast(), + ) + .cast() + .toDartString(); + if (response.startsWith("error")) { + return [false, response.replaceFirst("error", "")]; + } + return [true, response]; +} + +String? _ffiStart(String configPath, bool disableMemoryLimit) { + final box = _ffiLoadLibrary(); + final err = box + .start( + configPath.toNativeUtf8().cast(), + disableMemoryLimit ? 1 : 0, + ) + .cast() + .toDartString(); + return err.isEmpty ? null : err; +} + +String? _ffiStop() { + final box = _ffiLoadLibrary(); + final err = box.stop().cast().toDartString(); + return err.isEmpty ? null : err; +} + +String? _ffiRestart(String configPath, bool disableMemoryLimit) { + final box = _ffiLoadLibrary(); + final err = box + .restart( + configPath.toNativeUtf8().cast(), + disableMemoryLimit ? 1 : 0, + ) + .cast() + .toDartString(); + return err.isEmpty ? null : err; +} + +String? _ffiSelectOutbound(String groupTag, String outboundTag) { + final box = _ffiLoadLibrary(); + final err = box + .selectOutbound( + groupTag.toNativeUtf8().cast(), + outboundTag.toNativeUtf8().cast(), + ) + .cast() + .toDartString(); + return err.isEmpty ? null : err; +} + +String? _ffiUrlTest(String groupTag) { + final box = _ffiLoadLibrary(); + final err = box.urlTest(groupTag.toNativeUtf8().cast()).cast().toDartString(); + return err.isEmpty ? null : err; +} + +List _ffiGenerateWarpConfig( + String licenseKey, + String previousAccountId, + String previousAccessToken, + ) { + final box = _ffiLoadLibrary(); + final response = box + .generateWarpConfig( + licenseKey.toNativeUtf8().cast(), + previousAccountId.toNativeUtf8().cast(), + previousAccessToken.toNativeUtf8().cast(), + ) + .cast() + .toDartString(); + if (response.startsWith("error:")) { + return [false, response.replaceFirst("error:", "")]; + } + return [true, response]; +} diff --git a/lib/utils/isolate_worker.dart b/lib/utils/isolate_worker.dart new file mode 100644 index 0000000..3e26048 --- /dev/null +++ b/lib/utils/isolate_worker.dart @@ -0,0 +1,21 @@ +import 'dart:async'; +import 'dart:isolate'; + +/// Simple worker that executes functions in a separate isolate. +/// Replacement for combine package's CombineWorker. +class IsolateWorker { + /// Execute a function in a separate isolate and return the result. + /// + /// Note: The function must be a top-level function or a static method, + /// and it cannot capture non-sendable objects from the surrounding scope. + Future execute(T Function() computation, {bool allowSyncFallback = false}) async { + try { + return await Isolate.run(computation); + } catch (e) { + if (!allowSyncFallback) { + rethrow; + } + return computation(); + } + } +} diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 73feca8..82bd7c8 100755 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,13 +1,23 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(HiFastVPN LANGUAGES CXX) + +# 禁用 CMake 开发者警告(特别是 CMP0175 关于 flutter_inappwebview_windows 的警告) +set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE) +set(CMAKE_POLICY_WARNING_CMP0175 OFF) # 设置 CMake 策略以兼容旧版本插件 # CMP0175: add_custom_command() 拒绝无效参数(用于兼容 flutter_inappwebview_windows 插件) +# 必须在 project() 之前设置,才能传播到所有子目录 if(POLICY CMP0175) cmake_policy(SET CMP0175 OLD) endif() +project(HiFastVPN LANGUAGES CXX) + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + # 设置静态链接 set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") @@ -24,10 +34,6 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FS") # the on-disk name of your application. set(BINARY_NAME "HiFastVPN") -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) @@ -83,6 +89,8 @@ include(flutter/generated_plugins.cmake) foreach(plugin ${FLUTTER_PLUGIN_LIST}) if(TARGET ${plugin}_plugin) target_compile_options(${plugin}_plugin PRIVATE /FS) + # 降低优化级别以避免访问违规错误(特别是 flutter_inappwebview_windows) + target_compile_options(${plugin}_plugin PRIVATE /Od) endif() endforeach(plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 394917c..b8ab1e6 100755 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -13,7 +13,6 @@ add_executable(${BINARY_NAME} WIN32 "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" - "runner.exe.manifest" ) # Apply the standard set of build settings. This can be removed for applications diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index f0df08f..b6a8f27 100755 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -54,6 +54,8 @@ END // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" +// Manifest resource for admin privileges and compatibility +IDR_MANIFEST1 RT_MANIFEST "runner.exe.manifest" ///////////////////////////////////////////////////////////////////////////// // diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index cb1b0d3..72a0ce8 100755 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -1,14 +1,53 @@ #include #include #include +#include #include "flutter_window.h" #include "utils.h" #include +// ✅ 为较旧的 Windows SDK 版本提供 DPI 感知常量定义 +#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 +#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 ((DPI_AWARENESS_CONTEXT)-4) +#endif + +// 动态加载 SetProcessDpiAwarenessContext(Windows 10 v1607+) +typedef BOOL(WINAPI* SetProcessDpiAwarenessContextFunc)(DPI_AWARENESS_CONTEXT); +typedef HRESULT(WINAPI* SetProcessDpiAwarenessFunc)(PROCESS_DPI_AWARENESS); + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // ✅ 关键修复:启用 PerMonitorV2 DPI 感知,确保高分辨率显示器上字体清晰 + // 这必须在创建任何窗口之前调用 + HMODULE user32 = LoadLibraryW(L"user32.dll"); + if (user32) { + auto set_dpi_aware_context = (SetProcessDpiAwarenessContextFunc)GetProcAddress( + user32, "SetProcessDpiAwarenessContext"); + if (set_dpi_aware_context) { + set_dpi_aware_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + FreeLibrary(user32); + } + + // 备用方案:如果 SetProcessDpiAwarenessContext 不可用,尝试 SetProcessDpiAwareness (Vista+) + HMODULE shcore = LoadLibraryW(L"shcore.dll"); + if (shcore && !user32) { + auto set_dpi_aware = (SetProcessDpiAwarenessFunc)GetProcAddress( + shcore, "SetProcessDpiAwareness"); + if (set_dpi_aware) { + set_dpi_aware(PROCESS_PER_MONITOR_DPI_AWARE); + } + FreeLibrary(shcore); + } + + // ✅ 删除此处权限检查: + // 原因:manifest 中已配置 requireAdministrator,Windows 会自动检查权限 + // 如果权限不足,系统会弹出 UAC 对话框 + // 在此处重复检查会导致额外的系统命令执行和黑窗显示 + // 权限检查已移到 Dart 端,延迟到应用完全加载后进行(2秒后) + HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"HiFastVPNMutex"); HWND handle = FindWindowA(NULL, "HiFastVPN"); @@ -27,11 +66,11 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, return 0; } - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } + // ✅ 改进:完全禁用控制台窗口 + // AttachConsole 在某些情况下仍会显示控制台窗口导致黑屏 + // BearVPN 是 GUI 应用,不需要控制台输出 + // 日志通过 KRInitLogCollector 写入文件,无需控制台 + // (已注释掉所有控制台相关代码) // Initialize COM, so that it is available for use in the library and/or // plugins. diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest index a42ea76..834950f 100755 --- a/windows/runner/runner.exe.manifest +++ b/windows/runner/runner.exe.manifest @@ -1,10 +1,29 @@ + + + + + + + + + + + + PerMonitorV2 + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp index b2b0873..f8eb2e5 100755 --- a/windows/runner/utils.cpp +++ b/windows/runner/utils.cpp @@ -4,6 +4,7 @@ #include #include #include +#include // 用于权限检查 #include @@ -63,3 +64,24 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } return utf8_string; } + +// 🔧 检查应用是否以管理员权限运行 +bool IsRunAsAdministrator() { + BOOL is_admin = FALSE; + HANDLE token_handle = nullptr; + + // 打开当前进程的访问令牌 + if (::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token_handle)) { + // 获取令牌中的提升信息 + TOKEN_ELEVATION elevation{}; + DWORD size = sizeof(elevation); + + if (::GetTokenInformation(token_handle, TokenElevation, &elevation, size, &size)) { + is_admin = elevation.TokenIsElevated; + } + + ::CloseHandle(token_handle); + } + + return is_admin == TRUE; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h index 3879d54..bea5ee9 100755 --- a/windows/runner/utils.h +++ b/windows/runner/utils.h @@ -16,4 +16,8 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string); // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); +// 🔧 检查应用是否以管理员权限运行 +// 返回 true 如果当前进程有管理员权限 +bool IsRunAsAdministrator(); + #endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index d57ddd4..03e55ce 100755 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -157,7 +157,11 @@ bool Win32Window::Create(const std::wstring& title, return false; } - // 获取系统主题设置 + // ✅ 关键修复:启用非客户端 DPI 缩放,这对 PerMonitor V1 感知模式很关键 + // 确保窗口标题栏、边框、菜单栏等在高 DPI 下正确缩放 + EnableFullDpiSupportIfAvailable(window); + + // 获取系统主题设置 DWORD light_mode; DWORD light_mode_size = sizeof(light_mode); LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,