feat: 保存配置
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user