feat: win代码同步

This commit is contained in:
speakeloudest 2026-01-13 19:26:03 -08:00
parent 9a907f6531
commit 6ef08c9e7b
12 changed files with 586 additions and 205 deletions

View File

@ -711,8 +711,17 @@ class KRSubscribeService {
kr_trialRemainingTime.value = ''; kr_trialRemainingTime.value = '';
/// ///
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) { // 使 is ==
await KRSingBoxImp.instance.kr_stop(); // kr_status Rx<SingboxStatus> ==
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数据 // UI数据

View File

@ -62,6 +62,7 @@ class KRWindowManager with WindowListener, TrayListener {
await windowManager.setMaximumSize(const Size(420, 900)); await windowManager.setMaximumSize(const Size(420, 900));
await windowManager.setResizable(true); await windowManager.setResizable(true);
await windowManager.center(); await windowManager.center();
await windowManager.show(); // macOS
} }
// //
@ -154,8 +155,66 @@ class KRWindowManager with WindowListener, TrayListener {
} }
/// 退 /// 退
///
Future<void> _exitApp() async { Future<void> _exitApp() async {
KRLogUtil.kr_i('_exitApp: 退出应用'); 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 _handleTerminate();
await windowManager.destroy(); await windowManager.destroy();
} }
@ -164,9 +223,9 @@ class KRWindowManager with WindowListener, TrayListener {
Future<void> _showWindow() async { Future<void> _showWindow() async {
KRLogUtil.kr_i('_showWindow: 开始显示窗口'); KRLogUtil.kr_i('_showWindow: 开始显示窗口');
try { try {
await windowManager.setSkipTaskbar(false);
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setSkipTaskbar(false);
await windowManager.setAlwaysOnTop(true); await windowManager.setAlwaysOnTop(true);
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await windowManager.setAlwaysOnTop(false); await windowManager.setAlwaysOnTop(false);
@ -185,6 +244,7 @@ class KRWindowManager with WindowListener, TrayListener {
@override @override
void onWindowClose() async { void onWindowClose() async {
if (Platform.isWindows) { if (Platform.isWindows) {
await windowManager.setSkipTaskbar(true);
await windowManager.hide(); await windowManager.hide();
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
await windowManager.hide(); await windowManager.hide();
@ -221,9 +281,29 @@ class KRWindowManager with WindowListener, TrayListener {
/// ///
Future<void> _handleTerminate() async { Future<void> _handleTerminate() async {
KRLogUtil.kr_i('_handleTerminate: 处理应用终止'); 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();
} }
} }

View File

@ -3,7 +3,7 @@ import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:combine/combine.dart'; import 'package:kaer_with_panels/utils/isolate_worker.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/core/model/directories.dart'; import 'package:kaer_with_panels/core/model/directories.dart';
@ -50,6 +50,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override @override
Future<void> init() async { Future<void> init() async {
loggy.debug("initializing"); loggy.debug("initializing");
_box.setupOnce(NativeApi.initializeApiDLData);
_statusReceiver = ReceivePort('service status receiver'); _statusReceiver = ReceivePort('service status receiver');
final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent); final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent);
_status = ValueConnectableStream.seeded( _status = ValueConnectableStream.seeded(
@ -60,162 +61,197 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override @override
TaskEither<String, Unit> setup( TaskEither<String, Unit> setup(
Directories directories, Directories directories,
bool debug, bool debug,
) { ) {
final port = _statusReceiver.sendPort.nativePort; final port = _statusReceiver.sendPort.nativePort;
return TaskEither( final baseDir = directories.baseDir.path;
() => CombineWorker().execute( final workingDir = directories.workingDir.path;
() { final tempDir = directories.tempDir.path;
_box.setupOnce(NativeApi.initializeApiDLData); final debugFlag = debug ? 1 : 0;
final err = _box
.setup( return TaskEither(() async {
directories.baseDir.path.toNativeUtf8().cast(), try {
directories.workingDir.path.toNativeUtf8().cast(), final startTime = DateTime.now();
directories.tempDir.path.toNativeUtf8().cast(), _logger.debug('[黑屏调试] setup() 开始调用 libcore.dll - $startTime');
port,
debug ? 1 : 0, final err = await IsolateWorker().execute(
) () => _ffiSetup(baseDir, workingDir, tempDir, port, debugFlag),
.cast<Utf8>() allowSyncFallback: false,
.toDartString(); );
if (err.isNotEmpty) {
return left(err); final endTime = DateTime.now();
} final durationMs = endTime.difference(startTime).inMilliseconds;
return right(unit); _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 @override
TaskEither<String, Unit> validateConfigByPath( TaskEither<String, Unit> validateConfigByPath(
String path, String path,
String tempPath, String tempPath,
bool debug, bool debug,
) { ) {
return TaskEither( final debugFlag = debug ? 1 : 0;
() => CombineWorker().execute( return TaskEither(() async {
() { try {
final err = _box final err = await IsolateWorker().execute(
.parse( () => _ffiValidateConfig(path, tempPath, debugFlag),
path.toNativeUtf8().cast(), allowSyncFallback: false,
tempPath.toNativeUtf8().cast(), );
debug ? 1 : 0, if (err != null && err.isNotEmpty) {
) return left(err);
.cast<Utf8>() }
.toDartString(); return right(unit);
if (err.isNotEmpty) { } catch (e) {
return left(err); return left(e.toString());
} }
return right(unit); });
},
),
);
} }
@override @override
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) { TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
return TaskEither( final json = jsonEncode(options.toJson());
() => CombineWorker().execute( return TaskEither(() async {
() { try {
final json = jsonEncode(options.toJson()); final startTime = DateTime.now();
final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString(); _logger.debug('[黑屏调试] changeOptions 开始调用 libcore.dll - $startTime');
if (err.isNotEmpty) {
return left(err); final err = await IsolateWorker().execute(
} () => _ffiChangeOptions(json),
return right(unit); 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 @override
TaskEither<String, String> generateFullConfigByPath( TaskEither<String, String> generateFullConfigByPath(
String path, String path,
) { ) {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final result = await IsolateWorker().execute(
final response = _box () => _ffiGenerateFullConfig(path),
.generateConfig( allowSyncFallback: false,
path.toNativeUtf8().cast(), );
) final ok = result.isNotEmpty && result[0] == true;
.cast<Utf8>() final payload = result.length > 1 ? result[1] as String : '';
.toDartString(); if (!ok) {
if (response.startsWith("error")) { return left(payload);
return left(response.replaceFirst("error", "")); }
} return right(payload);
return right(response); } catch (e) {
}, return left(e.toString());
), }
); });
} }
@override @override
TaskEither<String, Unit> start( TaskEither<String, Unit> start(
String configPath, String configPath,
String name, String name,
bool disableMemoryLimit, bool disableMemoryLimit,
) { ) {
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]"); loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final startTime = DateTime.now();
final err = _box _logger.debug('[黑屏调试] start() 开始调用 libcore.dll - $startTime');
.start(
configPath.toNativeUtf8().cast(), final err = await IsolateWorker().execute(
disableMemoryLimit ? 1 : 0, () => _ffiStart(configPath, disableMemoryLimit),
) allowSyncFallback: false,
.cast<Utf8>() );
.toDartString();
if (err.isNotEmpty) { final endTime = DateTime.now();
return left(err); final durationMs = endTime.difference(startTime).inMilliseconds;
} _logger.debug('[黑屏调试] start() 完成(耗时: ${durationMs}ms');
return right(unit);
}, 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 @override
TaskEither<String, Unit> stop() { TaskEither<String, Unit> stop() {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final startTime = DateTime.now();
final err = _box.stop().cast<Utf8>().toDartString(); _logger.debug('[黑屏调试] stop() 开始调用 libcore.dll - $startTime');
if (err.isNotEmpty) {
return left(err); final err = await IsolateWorker().execute(
} _ffiStop,
return right(unit); 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 @override
TaskEither<String, Unit> restart( TaskEither<String, Unit> restart(
String configPath, String configPath,
String name, String name,
bool disableMemoryLimit, bool disableMemoryLimit,
) { ) {
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]"); loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final err = await IsolateWorker().execute(
final err = _box () => _ffiRestart(configPath, disableMemoryLimit),
.restart( allowSyncFallback: false,
configPath.toNativeUtf8().cast(), );
disableMemoryLimit ? 1 : 0, if (err != null && err.isNotEmpty) {
) return left(err);
.cast<Utf8>() }
.toDartString(); return right(unit);
if (err.isNotEmpty) { } catch (e) {
return left(err); return left(e.toString());
} }
return right(unit); });
},
),
);
} }
@override @override
@ -243,7 +279,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
_serviceStatsStream = null; _serviceStatsStream = null;
}, },
).map( ).map(
(event) { (event) {
if (event case String _) { if (event case String _) {
if (event.startsWith('error:')) { if (event.startsWith('error:')) {
loggy.error("[service stats client] error received: $event"); loggy.error("[service stats client] error received: $event");
@ -283,7 +319,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
} }
}, },
).map( ).map(
(event) { (event) {
if (event case String _) { if (event case String _) {
if (event.startsWith('error:')) { if (event.startsWith('error:')) {
logger.error("error received: $event"); logger.error("error received: $event");
@ -327,7 +363,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
} }
}, },
).map( ).map(
(event) { (event) {
if (event case String _) { if (event case String _) {
if (event.startsWith('error:')) { if (event.startsWith('error:')) {
logger.error(event); logger.error(event);
@ -359,38 +395,38 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override @override
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) { TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final err = await IsolateWorker().execute(
final err = _box () => _ffiSelectOutbound(groupTag, outboundTag),
.selectOutbound( allowSyncFallback: false,
groupTag.toNativeUtf8().cast(), );
outboundTag.toNativeUtf8().cast(), if (err != null && err.isNotEmpty) {
) return left(err);
.cast<Utf8>() }
.toDartString(); return right(unit);
if (err.isNotEmpty) { } catch (e) {
return left(err); return left(e.toString());
} }
return right(unit); });
},
),
);
} }
@override @override
TaskEither<String, Unit> urlTest(String groupTag) { TaskEither<String, Unit> urlTest(String groupTag) {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final err = await IsolateWorker().execute(
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString(); () => _ffiUrlTest(groupTag),
if (err.isNotEmpty) { allowSyncFallback: false,
return left(err); );
} if (err != null && err.isNotEmpty) {
return right(unit); return left(err);
}, }
), return right(unit);
); } catch (e) {
return left(e.toString());
}
});
} }
final _logBuffer = <String>[]; final _logBuffer = <String>[];
@ -409,14 +445,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override @override
TaskEither<String, Unit> clearLogs() { TaskEither<String, Unit> clearLogs() {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( _logBuffer.clear();
() { return right(unit);
_logBuffer.clear(); });
return right(unit);
},
),
);
} }
Future<List<String>> _readLogFile(File file) async { Future<List<String>> _readLogFile(File file) async {
@ -443,23 +475,165 @@ class FFISingboxService with InfraLogger implements SingboxService {
required String previousAccessToken, required String previousAccessToken,
}) { }) {
loggy.debug("generating warp config"); loggy.debug("generating warp config");
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final result = await IsolateWorker().execute(
final response = _box () => _ffiGenerateWarpConfig(licenseKey, previousAccountId, previousAccessToken),
.generateWarpConfig( allowSyncFallback: false,
licenseKey.toNativeUtf8().cast(), );
previousAccountId.toNativeUtf8().cast(), final ok = result.isNotEmpty && result[0] == true;
previousAccessToken.toNativeUtf8().cast(), final payload = result.length > 1 ? result[1] as String : '';
) if (!ok) {
.cast<Utf8>() return left(payload);
.toDartString(); }
if (response.startsWith("error:")) { return right(warpFromJson(jsonDecode(payload)));
return left(response.replaceFirst('error:', "")); } catch (e) {
} return left(e.toString());
return right(warpFromJson(jsonDecode(response))); }
}, });
),
);
} }
} }
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<Utf8>()
.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<Utf8>()
.toDartString();
return err.isEmpty ? null : err;
}
String? _ffiChangeOptions(String optionsJson) {
final box = _ffiLoadLibrary();
final err = box.changeHiddifyOptions(optionsJson.toNativeUtf8().cast()).cast<Utf8>().toDartString();
return err.isEmpty ? null : err;
}
List<Object?> _ffiGenerateFullConfig(String path) {
final box = _ffiLoadLibrary();
final response = box
.generateConfig(
path.toNativeUtf8().cast(),
)
.cast<Utf8>()
.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<Utf8>()
.toDartString();
return err.isEmpty ? null : err;
}
String? _ffiStop() {
final box = _ffiLoadLibrary();
final err = box.stop().cast<Utf8>().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<Utf8>()
.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<Utf8>()
.toDartString();
return err.isEmpty ? null : err;
}
String? _ffiUrlTest(String groupTag) {
final box = _ffiLoadLibrary();
final err = box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
return err.isEmpty ? null : err;
}
List<Object?> _ffiGenerateWarpConfig(
String licenseKey,
String previousAccountId,
String previousAccessToken,
) {
final box = _ffiLoadLibrary();
final response = box
.generateWarpConfig(
licenseKey.toNativeUtf8().cast(),
previousAccountId.toNativeUtf8().cast(),
previousAccessToken.toNativeUtf8().cast(),
)
.cast<Utf8>()
.toDartString();
if (response.startsWith("error:")) {
return [false, response.replaceFirst("error:", "")];
}
return [true, response];
}

View File

@ -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<T> execute<T>(T Function() computation, {bool allowSyncFallback = false}) async {
try {
return await Isolate.run(computation);
} catch (e) {
if (!allowSyncFallback) {
rethrow;
}
return computation();
}
}
}

View File

@ -1,13 +1,23 @@
# Project-level configuration. # Project-level configuration.
cmake_minimum_required(VERSION 3.14) 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 # CMake
# CMP0175: add_custom_command() flutter_inappwebview_windows # CMP0175: add_custom_command() flutter_inappwebview_windows
# project()
if(POLICY CMP0175) if(POLICY CMP0175)
cmake_policy(SET CMP0175 OLD) cmake_policy(SET CMP0175 OLD)
endif() 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$<$<CONFIG:Debug>:Debug>") set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") 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. # the on-disk name of your application.
set(BINARY_NAME "HiFastVPN") 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. # Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG) if(IS_MULTICONFIG)
@ -83,6 +89,8 @@ include(flutter/generated_plugins.cmake)
foreach(plugin ${FLUTTER_PLUGIN_LIST}) foreach(plugin ${FLUTTER_PLUGIN_LIST})
if(TARGET ${plugin}_plugin) if(TARGET ${plugin}_plugin)
target_compile_options(${plugin}_plugin PRIVATE /FS) target_compile_options(${plugin}_plugin PRIVATE /FS)
# 访 flutter_inappwebview_windows
target_compile_options(${plugin}_plugin PRIVATE /Od)
endif() endif()
endforeach(plugin) endforeach(plugin)

View File

@ -13,7 +13,6 @@ add_executable(${BINARY_NAME} WIN32
"win32_window.cpp" "win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc" "Runner.rc"
"runner.exe.manifest"
) )
# Apply the standard set of build settings. This can be removed for applications # Apply the standard set of build settings. This can be removed for applications

View File

@ -54,6 +54,8 @@ END
// remains consistent on all systems. // remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico" IDI_APP_ICON ICON "resources\\app_icon.ico"
// Manifest resource for admin privileges and compatibility
IDR_MANIFEST1 RT_MANIFEST "runner.exe.manifest"
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
// //

View File

@ -1,14 +1,53 @@
#include <flutter/dart_project.h> #include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h> #include <flutter/flutter_view_controller.h>
#include <windows.h> #include <windows.h>
#include <shellscalingapi.h>
#include "flutter_window.h" #include "flutter_window.h"
#include "utils.h" #include "utils.h"
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h> #include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
// ✅ 为较旧的 Windows SDK 版本提供 DPI 感知常量定义
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 ((DPI_AWARENESS_CONTEXT)-4)
#endif
// 动态加载 SetProcessDpiAwarenessContextWindows 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, int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) { _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 中已配置 requireAdministratorWindows 会自动检查权限
// 如果权限不足,系统会弹出 UAC 对话框
// 在此处重复检查会导致额外的系统命令执行和黑窗显示
// 权限检查已移到 Dart 端延迟到应用完全加载后进行2秒后
HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"HiFastVPNMutex"); HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L"HiFastVPNMutex");
HWND handle = FindWindowA(NULL, "HiFastVPN"); HWND handle = FindWindowA(NULL, "HiFastVPN");
@ -27,11 +66,11 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
return 0; return 0;
} }
// Attach to console when present (e.g., 'flutter run') or create a // ✅ 改进:完全禁用控制台窗口
// new console when running with a debugger. // AttachConsole 在某些情况下仍会显示控制台窗口导致黑屏
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { // BearVPN 是 GUI 应用,不需要控制台输出
CreateAndAttachConsole(); // 日志通过 KRInitLogCollector 写入文件,无需控制台
} // (已注释掉所有控制台相关代码)
// Initialize COM, so that it is available for use in the library and/or // Initialize COM, so that it is available for use in the library and/or
// plugins. // plugins.

View File

@ -1,10 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<!-- 权限请求:应用以管理员权限运行,强制 UAC 提升 -->
<!-- ✅ TUN 模式默认权限方案:使用 requireAdministrator 模式
- 应用启动时自动弹出 UAC 提示(带盾牌图标)
- 用户同意后以管理员身份运行
- TUN 模式默认可用,无需额外权限检查
- 规则/系统代理模式在管理员身份下也可以运行
-->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<!-- requireAdministrator: 强制以管理员身份运行(需要 UAC 提升) -->
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings> <windowsSettings>
<!-- 启用高 DPI 感知,支持不同显示器的缩放 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings> </windowsSettings>
</application> </application>
<!-- 系统兼容性配置 -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>
<!-- Windows 10 and Windows 11 --> <!-- Windows 10 and Windows 11 -->

View File

@ -4,6 +4,7 @@
#include <io.h> #include <io.h>
#include <stdio.h> #include <stdio.h>
#include <windows.h> #include <windows.h>
#include <shlobj.h> // 用于权限检查
#include <iostream> #include <iostream>
@ -63,3 +64,24 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) {
} }
return utf8_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;
}

View File

@ -16,4 +16,8 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string);
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure. // encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments(); std::vector<std::string> GetCommandLineArguments();
// 🔧 检查应用是否以管理员权限运行
// 返回 true 如果当前进程有管理员权限
bool IsRunAsAdministrator();
#endif // RUNNER_UTILS_H_ #endif // RUNNER_UTILS_H_

View File

@ -157,7 +157,11 @@ bool Win32Window::Create(const std::wstring& title,
return false; return false;
} }
// 获取系统主题设置 // ✅ 关键修复:启用非客户端 DPI 缩放,这对 PerMonitor V1 感知模式很关键
// 确保窗口标题栏、边框、菜单栏等在高 DPI 下正确缩放
EnableFullDpiSupportIfAvailable(window);
// 获取系统主题设置
DWORD light_mode; DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode); DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,