feat: win代码同步
This commit is contained in:
parent
9a907f6531
commit
6ef08c9e7b
@ -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<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数据
|
||||
|
||||
@ -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<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();
|
||||
}
|
||||
@ -164,9 +223,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);
|
||||
@ -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<void> _handleTerminate() async {
|
||||
KRLogUtil.kr_i('_handleTerminate: 处理应用终止');
|
||||
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
|
||||
|
||||
// 🔧 修复 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<void> 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(
|
||||
@ -64,27 +65,35 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
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<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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
|
||||
@ -93,62 +102,72 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
String tempPath,
|
||||
bool debug,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.parse(
|
||||
path.toNativeUtf8().cast(),
|
||||
tempPath.toNativeUtf8().cast(),
|
||||
debug ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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<String, Unit> changeOptions(SingboxConfigOption options) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final json = jsonEncode(options.toJson());
|
||||
final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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<String, String> generateFullConfigByPath(
|
||||
String path,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
.generateConfig(
|
||||
path.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (response.startsWith("error")) {
|
||||
return left(response.replaceFirst("error", ""));
|
||||
}
|
||||
return right(response);
|
||||
},
|
||||
),
|
||||
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
|
||||
@ -158,38 +177,58 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
bool disableMemoryLimit,
|
||||
) {
|
||||
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.start(
|
||||
configPath.toNativeUtf8().cast(),
|
||||
disableMemoryLimit ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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<String, Unit> stop() {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box.stop().cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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
|
||||
@ -199,23 +238,20 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
bool disableMemoryLimit,
|
||||
) {
|
||||
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.restart(
|
||||
configPath.toNativeUtf8().cast(),
|
||||
disableMemoryLimit ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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
|
||||
@ -359,38 +395,38 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.selectOutbound(
|
||||
groupTag.toNativeUtf8().cast(),
|
||||
outboundTag.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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<String, Unit> urlTest(String groupTag) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
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 = <String>[];
|
||||
@ -409,14 +445,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> clearLogs() {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
return TaskEither(() async {
|
||||
_logBuffer.clear();
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<String>> _readLogFile(File file) async {
|
||||
@ -443,10 +475,156 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
required String previousAccessToken,
|
||||
}) {
|
||||
loggy.debug("generating warp config");
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
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<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(),
|
||||
@ -455,11 +633,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (response.startsWith("error:")) {
|
||||
return left(response.replaceFirst('error:', ""));
|
||||
}
|
||||
return right(warpFromJson(jsonDecode(response)));
|
||||
},
|
||||
),
|
||||
);
|
||||
return [false, response.replaceFirst("error:", "")];
|
||||
}
|
||||
return [true, response];
|
||||
}
|
||||
|
||||
21
lib/utils/isolate_worker.dart
Normal file
21
lib/utils/isolate_worker.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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$<$<CONFIG:Debug>: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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
|
||||
@ -1,14 +1,53 @@
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
#include <shellscalingapi.h>
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.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
|
||||
|
||||
// 动态加载 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.
|
||||
|
||||
@ -1,10 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<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">
|
||||
<windowsSettings>
|
||||
<!-- 启用高 DPI 感知,支持不同显示器的缩放 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- 系统兼容性配置 -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
#include <shlobj.h> // 用于权限检查
|
||||
|
||||
#include <iostream>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
std::vector<std::string> GetCommandLineArguments();
|
||||
|
||||
// 🔧 检查应用是否以管理员权限运行
|
||||
// 返回 true 如果当前进程有管理员权限
|
||||
bool IsRunAsAdministrator();
|
||||
|
||||
#endif // RUNNER_UTILS_H_
|
||||
|
||||
@ -157,6 +157,10 @@ 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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user