feat: win代码同步

This commit is contained in:
2026-01-13 19:26:03 -08:00
parent 9a907f6531
commit 6ef08c9e7b
12 changed files with 586 additions and 205 deletions
+11 -2
View File
@@ -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数据
+84 -4
View File
@@ -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()) {
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();
}
}
+361 -187
View File
@@ -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(
@@ -60,162 +61,197 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
TaskEither<String, Unit> 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<Utf8>()
.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<String, Unit> 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<Utf8>()
.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<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 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<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);
},
),
);
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<String, Unit> 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<Utf8>()
.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<String, Unit> stop() {
return TaskEither(
() => CombineWorker().execute(
() {
final err = _box.stop().cast<Utf8>().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<String, Unit> 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<Utf8>()
.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<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 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<String, Unit> urlTest(String groupTag) {
return TaskEither(
() => CombineWorker().execute(
() {
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().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 = <String>[];
@@ -409,14 +445,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override
TaskEither<String, Unit> clearLogs() {
return TaskEither(
() => CombineWorker().execute(
() {
_logBuffer.clear();
return right(unit);
},
),
);
return TaskEither(() async {
_logBuffer.clear();
return right(unit);
});
}
Future<List<String>> _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<Utf8>()
.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<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];
}
+21
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();
}
}
}