import 'dart:async'; import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; import 'dart:ui'; import 'package:ffi/ffi.dart'; import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/plugins/service.dart'; import 'package:fl_clash/state.dart'; import 'generated/clash_ffi.dart'; import 'interface.dart'; class ClashLib extends ClashHandlerInterface with AndroidClashInterface { static ClashLib? _instance; Completer _canSendCompleter = Completer(); SendPort? sendPort; final receiverPort = ReceivePort(); ClashLib._internal() { _initService(); } @override preload() { return _canSendCompleter.future; } _initService() async { await service?.destroy(); _registerMainPort(receiverPort.sendPort); receiverPort.listen((message) { if (message is SendPort) { if (_canSendCompleter.isCompleted) { sendPort = null; _canSendCompleter = Completer(); } sendPort = message; _canSendCompleter.complete(true); } else { handleResult( ActionResult.fromJson(json.decode( message, )), ); } }); await service?.init(); } _registerMainPort(SendPort sendPort) { IsolateNameServer.removePortNameMapping(mainIsolate); IsolateNameServer.registerPortWithName(sendPort, mainIsolate); } factory ClashLib() { _instance ??= ClashLib._internal(); return _instance!; } @override Future nextHandleResult(result, completer) async { switch (result.method) { case ActionMethod.setFdMap: case ActionMethod.setProcessMap: case ActionMethod.stopTun: case ActionMethod.updateDns: completer?.complete(result.data as bool); return true; case ActionMethod.getRunTime: case ActionMethod.startTun: case ActionMethod.getAndroidVpnOptions: case ActionMethod.getCurrentProfileName: completer?.complete(result.data as String); return true; default: return false; } } @override destroy() async { await service?.destroy(); return true; } @override reStart() { _initService(); } @override Future shutdown() async { await super.shutdown(); destroy(); return true; } @override sendMessage(String message) async { await _canSendCompleter.future; sendPort?.send(message); } @override Future setFdMap(int fd) { return invoke( method: ActionMethod.setFdMap, data: json.encode(fd), ); } @override Future setProcessMap(item) { return invoke( method: ActionMethod.setProcessMap, data: item, ); } @override Future startTun(int fd) async { final res = await invoke( method: ActionMethod.startTun, data: json.encode(fd), ); if (res.isEmpty) { return null; } return DateTime.fromMillisecondsSinceEpoch(int.parse(res)); } @override Future stopTun() { return invoke( method: ActionMethod.stopTun, ); } @override Future getAndroidVpnOptions() async { final res = await invoke( method: ActionMethod.getAndroidVpnOptions, ); if (res.isEmpty) { return null; } return AndroidVpnOptions.fromJson(json.decode(res)); } @override Future updateDns(String value) { return invoke( method: ActionMethod.updateDns, data: value, ); } @override Future getRunTime() async { final runTimeString = await invoke( method: ActionMethod.getRunTime, ); if (runTimeString.isEmpty) { return null; } return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); } @override Future getCurrentProfileName() { return invoke( method: ActionMethod.getCurrentProfileName, ); } } class ClashLibHandler { static ClashLibHandler? _instance; late final ClashFFI clashFFI; late final DynamicLibrary lib; ClashLibHandler._internal() { lib = DynamicLibrary.open("libclash.so"); clashFFI = ClashFFI(lib); clashFFI.initNativeApiBridge( NativeApi.initializeApiDLData, ); } factory ClashLibHandler() { _instance ??= ClashLibHandler._internal(); return _instance!; } Future invokeAction(String actionParams) { final completer = Completer(); final receiver = ReceivePort(); receiver.listen((message) { if (!completer.isCompleted) { completer.complete(message); receiver.close(); } }); final actionParamsChar = actionParams.toNativeUtf8().cast(); clashFFI.invokeAction( actionParamsChar, receiver.sendPort.nativePort, ); malloc.free(actionParamsChar); return completer.future; } attachMessagePort(int messagePort) { clashFFI.attachMessagePort( messagePort, ); } attachInvokePort(int invokePort) { clashFFI.attachInvokePort( invokePort, ); } DateTime? startTun(int fd) { final runTimeRaw = clashFFI.startTUN(fd); final runTimeString = runTimeRaw.cast().toDartString(); clashFFI.freeCString(runTimeRaw); if (runTimeString.isEmpty) return null; return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); } stopTun() { clashFFI.stopTun(); } updateDns(String dns) { final dnsChar = dns.toNativeUtf8().cast(); clashFFI.updateDns(dnsChar); malloc.free(dnsChar); } setProcessMap(ProcessMapItem processMapItem) { final processMapItemChar = json.encode(processMapItem).toNativeUtf8().cast(); clashFFI.setProcessMap(processMapItemChar); malloc.free(processMapItemChar); } setState(CoreState state) { final stateChar = json.encode(state).toNativeUtf8().cast(); clashFFI.setState(stateChar); malloc.free(stateChar); } String getCurrentProfileName() { final currentProfileRaw = clashFFI.getCurrentProfileName(); final currentProfile = currentProfileRaw.cast().toDartString(); clashFFI.freeCString(currentProfileRaw); return currentProfile; } AndroidVpnOptions getAndroidVpnOptions() { final vpnOptionsRaw = clashFFI.getAndroidVpnOptions(); final vpnOptions = json.decode(vpnOptionsRaw.cast().toDartString()); clashFFI.freeCString(vpnOptionsRaw); return AndroidVpnOptions.fromJson(vpnOptions); } Traffic getTraffic() { final trafficRaw = clashFFI.getTraffic(); final trafficString = trafficRaw.cast().toDartString(); clashFFI.freeCString(trafficRaw); if (trafficString.isEmpty) { return Traffic(); } return Traffic.fromMap(json.decode(trafficString)); } Traffic getTotalTraffic(bool value) { final trafficRaw = clashFFI.getTotalTraffic(); final trafficString = trafficRaw.cast().toDartString(); clashFFI.freeCString(trafficRaw); if (trafficString.isEmpty) { return Traffic(); } return Traffic.fromMap(json.decode(trafficString)); } startListener() async { clashFFI.startListener(); return true; } stopListener() async { clashFFI.stopListener(); return true; } setFdMap(String id) { final idChar = id.toNativeUtf8().cast(); clashFFI.setFdMap(idChar); malloc.free(idChar); } Future quickStart( String homeDir, UpdateConfigParams updateConfigParams, CoreState state, ) { final completer = Completer(); final receiver = ReceivePort(); receiver.listen((message) { if (!completer.isCompleted) { completer.complete(message); receiver.close(); } }); final params = json.encode(updateConfigParams); final stateParams = json.encode(state); final homeChar = homeDir.toNativeUtf8().cast(); final paramsChar = params.toNativeUtf8().cast(); final stateParamsChar = stateParams.toNativeUtf8().cast(); clashFFI.quickStart( homeChar, paramsChar, stateParamsChar, receiver.sendPort.nativePort, ); malloc.free(homeChar); malloc.free(paramsChar); malloc.free(stateParamsChar); return completer.future; } DateTime? getRunTime() { final runTimeRaw = clashFFI.getRunTime(); final runTimeString = runTimeRaw.cast().toDartString(); clashFFI.freeCString(runTimeRaw); if (runTimeString.isEmpty) return null; return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString)); } } ClashLib? get clashLib => Platform.isAndroid && !globalState.isService ? ClashLib() : null;