mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-09 18:10:29 +00:00
feat: move native communication to FFI (#1457)
This commit is contained in:
86 files changed
+4558
-886
No files matched your search
@@ -0,0 +1,64 @@
|
||||
class ApiServerState {
|
||||
const ApiServerState({
|
||||
required this.enabled,
|
||||
required this.running,
|
||||
required this.network,
|
||||
required this.address,
|
||||
required this.runningPort,
|
||||
required this.pendingApply,
|
||||
required this.lastError,
|
||||
});
|
||||
|
||||
final bool enabled;
|
||||
final bool running;
|
||||
final String network;
|
||||
final String address;
|
||||
final int runningPort;
|
||||
final bool pendingApply;
|
||||
final String lastError;
|
||||
|
||||
factory ApiServerState.fromJson(Map<String, dynamic> json) {
|
||||
return ApiServerState(
|
||||
enabled: json['enabled'] as bool? ?? false,
|
||||
running: json['running'] as bool? ?? false,
|
||||
network: json['network'] as String? ?? '',
|
||||
address: json['address'] as String? ?? '',
|
||||
runningPort: json['runningPort'] as int? ?? 0,
|
||||
pendingApply: json['pendingApply'] as bool? ?? false,
|
||||
lastError: json['lastError'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
String runningAddress() {
|
||||
if (!running) return '';
|
||||
if (network == 'unix') return address;
|
||||
final separator = address.lastIndexOf(':');
|
||||
final host = separator < 0 ? address : address.substring(0, separator);
|
||||
return '$host:$runningPort';
|
||||
}
|
||||
}
|
||||
|
||||
class ApiServerOperationResult {
|
||||
const ApiServerOperationResult({required this.state, required this.error});
|
||||
|
||||
final ApiServerState state;
|
||||
final String error;
|
||||
|
||||
factory ApiServerOperationResult.fromJson(Map<String, dynamic> json) {
|
||||
final rawState = json['state'] as Map<String, dynamic>?;
|
||||
return ApiServerOperationResult(
|
||||
state: rawState == null
|
||||
? const ApiServerState(
|
||||
enabled: false,
|
||||
running: false,
|
||||
network: '',
|
||||
address: '',
|
||||
runningPort: 0,
|
||||
pendingApply: true,
|
||||
lastError: '',
|
||||
)
|
||||
: ApiServerState.fromJson(rawState),
|
||||
error: json['error'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,24 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'api_server_state.dart';
|
||||
import 'libgopeed_interface.dart';
|
||||
import 'start_config.dart';
|
||||
import 'task_event.dart';
|
||||
|
||||
class LibgopeedChannel implements LibgopeedInterface {
|
||||
static const _channel = MethodChannel('gopeed.com/libgopeed');
|
||||
final _taskEvents = StreamController<TaskEvent>.broadcast();
|
||||
|
||||
LibgopeedChannel() {
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
if (call.method != 'taskEvent') return;
|
||||
final payload = jsonDecode(call.arguments as String) as Map<String, dynamic>;
|
||||
_taskEvents.add(TaskEvent.fromJson(payload));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> start(StartConfig cfg) async {
|
||||
@@ -18,4 +30,40 @@ class LibgopeedChannel implements LibgopeedInterface {
|
||||
Future<void> stop() async {
|
||||
return await _channel.invokeMethod('stop');
|
||||
}
|
||||
|
||||
Future<ApiServerOperationResult> _apiServerOperation(String method) async {
|
||||
final payload = await _channel.invokeMethod<String>(method) ?? '';
|
||||
return ApiServerOperationResult.fromJson(jsonDecode(payload) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> getApiServerState() => _apiServerOperation('getApiServerState');
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> startApiServer() => _apiServerOperation('startApiServer');
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> stopApiServer() => _apiServerOperation('stopApiServer');
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> restartApiServer() => _apiServerOperation('restartApiServer');
|
||||
|
||||
@override
|
||||
Future<String> invoke(String method, String path, {String query = '', String body = ''}) async {
|
||||
final result = await _channel.invokeMethod<String>('invoke', {
|
||||
'method': method,
|
||||
'path': path,
|
||||
'query': query,
|
||||
'body': body,
|
||||
});
|
||||
return result ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<TaskEvent> get taskEvents => _taskEvents.stream;
|
||||
|
||||
@override
|
||||
Future<void> subscribeTaskEvents(Set<TaskEventType> events) {
|
||||
return _channel.invokeMethod<void>('subscribeTaskEvents', {'mask': events.mask});
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,47 @@ import 'dart:ffi';
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
import '../ffi/libgopeed_bind.dart';
|
||||
import '../ffi/libgopeed_worker.dart';
|
||||
import 'api_server_state.dart';
|
||||
import 'libgopeed_interface.dart';
|
||||
import 'start_config.dart';
|
||||
import 'task_event.dart';
|
||||
|
||||
class LibgopeedFFi implements LibgopeedInterface {
|
||||
late LibgopeedBind _libgopeed;
|
||||
final _taskEvents = StreamController<TaskEvent>.broadcast();
|
||||
final _worker = LibgopeedWorker();
|
||||
late final NativeCallable<Void Function(Pointer<Char>)> _taskEventCallback;
|
||||
|
||||
LibgopeedFFi(LibgopeedBind libgopeed) {
|
||||
_libgopeed = libgopeed;
|
||||
_taskEventCallback = NativeCallable<Void Function(Pointer<Char>)>.listener(_onTaskEvent);
|
||||
}
|
||||
|
||||
void _onTaskEvent(Pointer<Char> payload) {
|
||||
try {
|
||||
final decoded = jsonDecode(payload.cast<Utf8>().toDartString()) as Map<String, dynamic>;
|
||||
_taskEvents.add(TaskEvent.fromJson(decoded));
|
||||
} finally {
|
||||
_libgopeed.FreeCString(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> start(StartConfig cfg) {
|
||||
var completer = Completer<int>();
|
||||
var result = _libgopeed.Start(jsonEncode(cfg).toNativeUtf8().cast());
|
||||
if (result.r1 != nullptr) {
|
||||
completer.completeError(Exception(result.r1.cast<Utf8>().toDartString()));
|
||||
} else {
|
||||
completer.complete(result.r0);
|
||||
final cfgPtr = jsonEncode(cfg).toNativeUtf8();
|
||||
try {
|
||||
final result = _libgopeed.Start(cfgPtr.cast());
|
||||
if (result.r1 != nullptr) {
|
||||
final message = result.r1.cast<Utf8>().toDartString();
|
||||
_libgopeed.FreeCString(result.r1);
|
||||
completer.completeError(Exception(message));
|
||||
} else {
|
||||
completer.complete(result.r0);
|
||||
}
|
||||
} finally {
|
||||
malloc.free(cfgPtr);
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
@@ -34,4 +57,53 @@ class LibgopeedFFi implements LibgopeedInterface {
|
||||
completer.complete();
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<ApiServerOperationResult> _apiServerOperation(String operation) async {
|
||||
final payload = await _worker.apiServer(operation);
|
||||
return ApiServerOperationResult.fromJson(jsonDecode(payload) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> getApiServerState() => _apiServerOperation('get');
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> startApiServer() => _apiServerOperation('start');
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> stopApiServer() => _apiServerOperation('stop');
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> restartApiServer() => _apiServerOperation('restart');
|
||||
|
||||
@override
|
||||
Future<String> invoke(String method, String path, {String query = '', String body = ''}) async {
|
||||
final methodPtr = method.toNativeUtf8();
|
||||
final pathPtr = path.toNativeUtf8();
|
||||
final queryPtr = query.toNativeUtf8();
|
||||
final bodyPtr = body.toNativeUtf8();
|
||||
Pointer<Char>? resultPtr;
|
||||
try {
|
||||
resultPtr = _libgopeed.Invoke(methodPtr.cast(), pathPtr.cast(), queryPtr.cast(), bodyPtr.cast());
|
||||
if (resultPtr == nullptr) {
|
||||
throw StateError('Gopeed Invoke returned a null response');
|
||||
}
|
||||
return resultPtr.cast<Utf8>().toDartString();
|
||||
} finally {
|
||||
malloc.free(methodPtr);
|
||||
malloc.free(pathPtr);
|
||||
malloc.free(queryPtr);
|
||||
malloc.free(bodyPtr);
|
||||
if (resultPtr != null && resultPtr != nullptr) {
|
||||
_libgopeed.FreeCString(resultPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<TaskEvent> get taskEvents => _taskEvents.stream;
|
||||
|
||||
@override
|
||||
Future<void> subscribeTaskEvents(Set<TaskEventType> events) async {
|
||||
_libgopeed.SubscribeTaskEvents(events.mask, events.isEmpty ? 0 : _taskEventCallback.nativeFunction.address);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
import 'api_server_state.dart';
|
||||
import 'start_config.dart';
|
||||
import 'task_event.dart';
|
||||
|
||||
abstract class LibgopeedInterface {
|
||||
Future<int> start(StartConfig cfg);
|
||||
|
||||
Future<void> stop();
|
||||
|
||||
Future<ApiServerOperationResult> getApiServerState();
|
||||
|
||||
Future<ApiServerOperationResult> startApiServer();
|
||||
|
||||
Future<ApiServerOperationResult> stopApiServer();
|
||||
|
||||
Future<ApiServerOperationResult> restartApiServer();
|
||||
|
||||
Future<String> invoke(String method, String path, {String query = '', String body = ''});
|
||||
|
||||
Stream<TaskEvent> get taskEvents;
|
||||
|
||||
Future<void> subscribeTaskEvents(Set<TaskEventType> events);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class WebViewRpcConfig {
|
||||
class StartConfig {
|
||||
late String network;
|
||||
late String address;
|
||||
bool apiEnable = false;
|
||||
late String storage;
|
||||
late String storageDir;
|
||||
late int refreshInterval;
|
||||
|
||||
@@ -22,6 +22,7 @@ Map<String, dynamic> _$WebViewRpcConfigToJson(WebViewRpcConfig instance) =>
|
||||
StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
|
||||
..network = json['network'] as String
|
||||
..address = json['address'] as String
|
||||
..apiEnable = json['apiEnable'] as bool
|
||||
..storage = json['storage'] as String
|
||||
..storageDir = json['storageDir'] as String
|
||||
..refreshInterval = (json['refreshInterval'] as num).toInt()
|
||||
@@ -36,6 +37,7 @@ Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
|
||||
<String, dynamic>{
|
||||
'network': instance.network,
|
||||
'address': instance.address,
|
||||
'apiEnable': instance.apiEnable,
|
||||
'storage': instance.storage,
|
||||
'storageDir': instance.storageDir,
|
||||
'refreshInterval': instance.refreshInterval,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
enum TaskEventType {
|
||||
done(1, 'task.done'),
|
||||
error(2, 'task.error');
|
||||
|
||||
const TaskEventType(this.mask, this.wireName);
|
||||
|
||||
final int mask;
|
||||
final String wireName;
|
||||
|
||||
static TaskEventType fromWireName(String value) {
|
||||
return values.firstWhere(
|
||||
(type) => type.wireName == value,
|
||||
orElse: () => throw FormatException('Unknown task event type: $value'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension TaskEventTypeSet on Set<TaskEventType> {
|
||||
int get mask => fold(0, (value, type) => value | type.mask);
|
||||
}
|
||||
|
||||
class TaskEvent {
|
||||
const TaskEvent({required this.type, required this.taskId, required this.name, this.error});
|
||||
|
||||
final TaskEventType type;
|
||||
final String taskId;
|
||||
final String name;
|
||||
final String? error;
|
||||
|
||||
factory TaskEvent.fromJson(Map<String, dynamic> json) {
|
||||
return TaskEvent(
|
||||
type: TaskEventType.fromWireName(json['type'] as String),
|
||||
taskId: json['taskId'] as String,
|
||||
name: json['name'] as String? ?? '',
|
||||
error: json['error'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user