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
@@ -12,7 +12,6 @@ import '../../api/model/switch_extension.dart';
|
||||
import '../../api/model/task.dart';
|
||||
import '../../api/model/update_check_extension_resp.dart';
|
||||
import '../../api/model/update_extension_settings.dart';
|
||||
import '../../database/database.dart';
|
||||
import '../window/app_window_appearance.dart';
|
||||
import 'capability_rpc.dart';
|
||||
import 'gopeed_capability.dart';
|
||||
@@ -147,19 +146,33 @@ void _bindGopeed(CapabilityRegistry registry) {
|
||||
|
||||
void _bindStorage(CapabilityRegistry registry) {
|
||||
registry
|
||||
..bind(StorageMethods.getCreateHistory, (_) async => Database.instance.getCreateHistory() ?? const <String>[])
|
||||
..bind(StorageMethods.getCreateHistory, (_) async {
|
||||
final config = await api.getConfig();
|
||||
return config.extra.createHistory;
|
||||
})
|
||||
..bind(StorageMethods.saveCreateHistory, (urls) async {
|
||||
final config = await api.getConfig();
|
||||
final history = List<String>.of(config.extra.createHistory);
|
||||
for (final url in urls) {
|
||||
Database.instance.saveCreateHistory(url);
|
||||
history.remove(url);
|
||||
history.insert(0, url);
|
||||
if (history.length > 64) history.removeLast();
|
||||
}
|
||||
config.extra.createHistory = history;
|
||||
await api.putConfig(config);
|
||||
return const RpcUnit();
|
||||
})
|
||||
..bind(StorageMethods.removeCreateHistory, (url) async {
|
||||
Database.instance.removeCreateHistory(url);
|
||||
final config = await api.getConfig();
|
||||
final history = List<String>.of(config.extra.createHistory)..remove(url);
|
||||
config.extra.createHistory = history;
|
||||
await api.putConfig(config);
|
||||
return const RpcUnit();
|
||||
})
|
||||
..bind(StorageMethods.clearCreateHistory, (_) async {
|
||||
Database.instance.clearCreateHistory();
|
||||
final config = await api.getConfig();
|
||||
config.extra.createHistory = const [];
|
||||
await api.putConfig(config);
|
||||
return const RpcUnit();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,9 @@
|
||||
import '../../database/database.dart';
|
||||
import '../../util/util.dart';
|
||||
|
||||
class AppInitializer {
|
||||
static Future<void>? _storageInitialization;
|
||||
static Future<void>? _databaseInitialization;
|
||||
|
||||
static Future<void> ensureStorageInitialized() {
|
||||
return _storageInitialization ??= Util.initStorageDir();
|
||||
}
|
||||
|
||||
static Future<void> ensureDatabaseInitialized() {
|
||||
return _databaseInitialization ??= _initializeDatabase();
|
||||
}
|
||||
|
||||
static Future<void> _initializeDatabase() async {
|
||||
await ensureStorageInitialized();
|
||||
await Database.instance.init();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../common/api_server_state.dart';
|
||||
import '../common/start_config.dart';
|
||||
import '../common/task_event.dart';
|
||||
|
||||
import '../libgopeed_boot.dart';
|
||||
|
||||
@@ -13,4 +15,40 @@ class LibgopeedBootBrowser implements LibgopeedBoot {
|
||||
|
||||
@override
|
||||
Future<void> stop() async {}
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> getApiServerState() async => _unsupportedApiServerResult;
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> startApiServer() async => _unsupportedApiServerResult;
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> stopApiServer() async => _unsupportedApiServerResult;
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> restartApiServer() async => _unsupportedApiServerResult;
|
||||
|
||||
@override
|
||||
Future<String> invoke(String method, String path, {String query = '', String body = ''}) {
|
||||
throw UnsupportedError('Native Gopeed invoke is unavailable on web');
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<TaskEvent> get taskEvents => const Stream.empty();
|
||||
|
||||
@override
|
||||
Future<void> subscribeTaskEvents(Set<TaskEventType> events) async {}
|
||||
}
|
||||
|
||||
const _unsupportedApiServerResult = ApiServerOperationResult(
|
||||
state: ApiServerState(
|
||||
enabled: false,
|
||||
running: false,
|
||||
network: '',
|
||||
address: '',
|
||||
runningPort: 0,
|
||||
pendingApply: false,
|
||||
lastError: '',
|
||||
),
|
||||
error: '',
|
||||
);
|
||||
@@ -6,7 +6,9 @@ import '../../util/util.dart';
|
||||
import '../common/libgopeed_channel.dart';
|
||||
import '../common/libgopeed_ffi.dart';
|
||||
import '../common/libgopeed_interface.dart';
|
||||
import '../common/api_server_state.dart';
|
||||
import '../common/start_config.dart';
|
||||
import '../common/task_event.dart';
|
||||
import '../ffi/libgopeed_bind.dart';
|
||||
import '../libgopeed_boot.dart';
|
||||
|
||||
@@ -43,6 +45,32 @@ class LibgopeedBootNative implements LibgopeedBoot {
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
await _libgopeed.subscribeTaskEvents({});
|
||||
await _libgopeed.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> getApiServerState() => _libgopeed.getApiServerState();
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> startApiServer() => _libgopeed.startApiServer();
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> stopApiServer() => _libgopeed.stopApiServer();
|
||||
|
||||
@override
|
||||
Future<ApiServerOperationResult> restartApiServer() => _libgopeed.restartApiServer();
|
||||
|
||||
@override
|
||||
Future<String> invoke(String method, String path, {String query = '', String body = ''}) {
|
||||
return _libgopeed.invoke(method, path, query: query, body: body);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<TaskEvent> get taskEvents => _libgopeed.taskEvents;
|
||||
|
||||
@override
|
||||
Future<void> subscribeTaskEvents(Set<TaskEventType> events) {
|
||||
return _libgopeed.subscribeTaskEvents(events);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,72 @@ class LibgopeedBind {
|
||||
|
||||
late final _StopPtr = _lookup<ffi.NativeFunction<ffi.Void Function()>>('Stop');
|
||||
late final _Stop = _StopPtr.asFunction<void Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> GetAPIServerState() => _GetAPIServerState();
|
||||
|
||||
late final _GetAPIServerStatePtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('GetAPIServerState');
|
||||
late final _GetAPIServerState = _GetAPIServerStatePtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> StartAPIServer() => _StartAPIServer();
|
||||
|
||||
late final _StartAPIServerPtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('StartAPIServer');
|
||||
late final _StartAPIServer = _StartAPIServerPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> StopAPIServer() => _StopAPIServer();
|
||||
|
||||
late final _StopAPIServerPtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('StopAPIServer');
|
||||
late final _StopAPIServer = _StopAPIServerPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> RestartAPIServer() => _RestartAPIServer();
|
||||
|
||||
late final _RestartAPIServerPtr = _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('RestartAPIServer');
|
||||
late final _RestartAPIServer = _RestartAPIServerPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
|
||||
|
||||
ffi.Pointer<ffi.Char> Invoke(
|
||||
ffi.Pointer<ffi.Char> method,
|
||||
ffi.Pointer<ffi.Char> path,
|
||||
ffi.Pointer<ffi.Char> query,
|
||||
ffi.Pointer<ffi.Char> body,
|
||||
) {
|
||||
return _Invoke(method, path, query, body);
|
||||
}
|
||||
|
||||
late final _InvokePtr =
|
||||
_lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
)
|
||||
>
|
||||
>('Invoke');
|
||||
late final _Invoke =
|
||||
_InvokePtr.asFunction<
|
||||
ffi.Pointer<ffi.Char> Function(
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
ffi.Pointer<ffi.Char>,
|
||||
)
|
||||
>();
|
||||
|
||||
void SubscribeTaskEvents(int mask, int callback) {
|
||||
return _SubscribeTaskEvents(mask, callback);
|
||||
}
|
||||
|
||||
late final _SubscribeTaskEventsPtr = _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Uint64, ffi.UintPtr)>>(
|
||||
'SubscribeTaskEvents',
|
||||
);
|
||||
late final _SubscribeTaskEvents = _SubscribeTaskEventsPtr.asFunction<void Function(int, int)>();
|
||||
|
||||
void FreeCString(ffi.Pointer<ffi.Char> value) {
|
||||
return _FreeCString(value);
|
||||
}
|
||||
|
||||
late final _FreeCStringPtr = _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>('FreeCString');
|
||||
late final _FreeCString = _FreeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
|
||||
}
|
||||
|
||||
final class Start_return extends ffi.Struct {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
import 'libgopeed_bind.dart';
|
||||
|
||||
class LibgopeedWorker {
|
||||
LibgopeedWorker() {
|
||||
_receivePort.listen(_handleWorkerMessage);
|
||||
_errorPort.listen(_handleWorkerError);
|
||||
_isolateReady = Isolate.spawn(_workerMain, _receivePort.sendPort, onError: _errorPort.sendPort);
|
||||
}
|
||||
|
||||
final ReceivePort _receivePort = ReceivePort();
|
||||
final ReceivePort _errorPort = ReceivePort();
|
||||
final Map<int, Completer<String>> _pending = {};
|
||||
late final Future<Isolate> _isolateReady;
|
||||
final Completer<SendPort> _sendPort = Completer<SendPort>();
|
||||
int _requestID = 0;
|
||||
|
||||
Future<String> invoke(String method, String path, String query, String body) async {
|
||||
await _isolateReady;
|
||||
final sendPort = await _sendPort.future;
|
||||
final requestID = _requestID++;
|
||||
final completer = Completer<String>();
|
||||
_pending[requestID] = completer;
|
||||
sendPort.send(['invoke', requestID, method, path, query, body]);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<String> apiServer(String operation) async {
|
||||
await _isolateReady;
|
||||
final sendPort = await _sendPort.future;
|
||||
final requestID = _requestID++;
|
||||
final completer = Completer<String>();
|
||||
_pending[requestID] = completer;
|
||||
sendPort.send(['apiServer', requestID, operation]);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void _handleWorkerMessage(dynamic message) {
|
||||
if (message is SendPort) {
|
||||
if (!_sendPort.isCompleted) _sendPort.complete(message);
|
||||
return;
|
||||
}
|
||||
final response = message as List<dynamic>;
|
||||
final requestID = response[0] as int;
|
||||
final completer = _pending.remove(requestID);
|
||||
if (completer == null) return;
|
||||
if (response[1] as bool) {
|
||||
completer.complete(response[2] as String);
|
||||
} else {
|
||||
completer.completeError(StateError(response[2] as String));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleWorkerError(dynamic message) {
|
||||
final parts = message is List<dynamic> ? message : <dynamic>[message];
|
||||
final error = StateError(parts.map((part) => part.toString()).join('\n'));
|
||||
if (!_sendPort.isCompleted) _sendPort.completeError(error);
|
||||
for (final completer in _pending.values) {
|
||||
completer.completeError(error);
|
||||
}
|
||||
_pending.clear();
|
||||
}
|
||||
|
||||
static void _workerMain(SendPort mainSendPort) {
|
||||
final requests = ReceivePort();
|
||||
final bindings = LibgopeedBind(DynamicLibrary.open(_libraryName()));
|
||||
mainSendPort.send(requests.sendPort);
|
||||
requests.listen((dynamic message) {
|
||||
final request = message as List<dynamic>;
|
||||
final type = request[0] as String;
|
||||
final requestID = request[1] as int;
|
||||
try {
|
||||
final response = switch (type) {
|
||||
'invoke' => _invoke(
|
||||
bindings,
|
||||
request[2] as String,
|
||||
request[3] as String,
|
||||
request[4] as String,
|
||||
request[5] as String,
|
||||
),
|
||||
'apiServer' => _apiServer(bindings, request[2] as String),
|
||||
_ => throw StateError('Unknown libgopeed worker request: $type'),
|
||||
};
|
||||
mainSendPort.send([requestID, true, response]);
|
||||
} catch (error) {
|
||||
mainSendPort.send([requestID, false, error.toString()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static String _apiServer(LibgopeedBind bindings, String operation) {
|
||||
final resultPtr = switch (operation) {
|
||||
'get' => bindings.GetAPIServerState(),
|
||||
'start' => bindings.StartAPIServer(),
|
||||
'stop' => bindings.StopAPIServer(),
|
||||
'restart' => bindings.RestartAPIServer(),
|
||||
_ => throw StateError('Unknown API server operation: $operation'),
|
||||
};
|
||||
if (resultPtr == nullptr) {
|
||||
throw StateError('Gopeed API server operation returned a null response');
|
||||
}
|
||||
try {
|
||||
return resultPtr.cast<Utf8>().toDartString();
|
||||
} finally {
|
||||
bindings.FreeCString(resultPtr);
|
||||
}
|
||||
}
|
||||
|
||||
static String _invoke(LibgopeedBind bindings, String method, String path, String query, String body) {
|
||||
final methodPtr = method.toNativeUtf8();
|
||||
final pathPtr = path.toNativeUtf8();
|
||||
final queryPtr = query.toNativeUtf8();
|
||||
final bodyPtr = body.toNativeUtf8();
|
||||
Pointer<Char>? resultPtr;
|
||||
try {
|
||||
resultPtr = bindings.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) {
|
||||
bindings.FreeCString(resultPtr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String _libraryName() {
|
||||
if (Platform.isWindows) return 'libgopeed.dll';
|
||||
if (Platform.isMacOS) return 'libgopeed.dylib';
|
||||
if (Platform.isLinux) return 'libgopeed.so';
|
||||
throw UnsupportedError('Desktop FFI is unavailable on ${Platform.operatingSystem}');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'common/api_server_state.dart';
|
||||
import 'common/start_config.dart';
|
||||
import 'common/task_event.dart';
|
||||
import "libgopeed_boot_stub.dart"
|
||||
if (dart.library.html) 'entry/libgopeed_boot_browser.dart'
|
||||
if (dart.library.io) 'entry/libgopeed_boot_native.dart';
|
||||
@@ -16,4 +18,18 @@ abstract class LibgopeedBoot {
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../../libgopeed_boot.dart';
|
||||
import '../../../ffi/libgopeed_worker.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../gopeed_transport.dart';
|
||||
|
||||
GopeedTransport createTransport(GopeedTransportConfig config) => NativeGopeedTransport();
|
||||
|
||||
class NativeGopeedTransport implements GopeedTransport {
|
||||
NativeGopeedTransport()
|
||||
: _externalDio = Dio(
|
||||
BaseOptions(
|
||||
contentType: Headers.jsonContentType,
|
||||
sendTimeout: const Duration(seconds: 5),
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 120),
|
||||
),
|
||||
);
|
||||
|
||||
final Dio _externalDio;
|
||||
final LibgopeedWorker? _ffiWorker = Util.isDesktop() ? LibgopeedWorker() : null;
|
||||
|
||||
@override
|
||||
Future<dynamic> request(
|
||||
String path, {
|
||||
String method = 'GET',
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
final request = _RequestParts.from(path, queryParameters);
|
||||
final requestBody = data == null ? '' : jsonEncode(data);
|
||||
final payload = _ffiWorker != null
|
||||
? await _ffiWorker.invoke(method.toUpperCase(), request.path, request.query, requestBody)
|
||||
: await LibgopeedBoot.instance.invoke(
|
||||
method.toUpperCase(),
|
||||
request.path,
|
||||
query: request.query,
|
||||
body: requestBody,
|
||||
);
|
||||
return jsonDecode(payload);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
|
||||
return _externalDio.request<String>(
|
||||
uri,
|
||||
data: data,
|
||||
options: (options ?? Options()).copyWith(responseType: ResponseType.plain),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String join(String path) => path;
|
||||
}
|
||||
|
||||
class _RequestParts {
|
||||
const _RequestParts(this.path, this.query);
|
||||
|
||||
final String path;
|
||||
final String query;
|
||||
|
||||
factory _RequestParts.from(String rawPath, Map<String, dynamic>? queryParameters) {
|
||||
var path = rawPath;
|
||||
var query = '';
|
||||
final queryIndex = rawPath.indexOf('?');
|
||||
if (queryIndex >= 0) {
|
||||
path = rawPath.substring(0, queryIndex);
|
||||
query = rawPath.substring(queryIndex + 1);
|
||||
}
|
||||
final extraQuery = _encodeQueryParameters(queryParameters);
|
||||
if (extraQuery.isNotEmpty) {
|
||||
query = query.isEmpty ? extraQuery : '$query&$extraQuery';
|
||||
}
|
||||
return _RequestParts(path, query);
|
||||
}
|
||||
|
||||
static String _encodeQueryParameters(Map<String, dynamic>? queryParameters) {
|
||||
if (queryParameters == null || queryParameters.isEmpty) return '';
|
||||
final segments = <String>[];
|
||||
queryParameters.forEach((key, value) {
|
||||
if (value == null) return;
|
||||
final values = value is Iterable ? value : [value];
|
||||
for (final item in values) {
|
||||
if (item == null) continue;
|
||||
segments.add('${Uri.encodeQueryComponent(key)}=${Uri.encodeQueryComponent(item.toString())}');
|
||||
}
|
||||
});
|
||||
return segments.join('&');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import '../gopeed_transport.dart';
|
||||
|
||||
GopeedTransport createTransport(GopeedTransportConfig config) {
|
||||
throw UnsupportedError('Gopeed transport is unavailable on this platform');
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../gopeed_transport.dart';
|
||||
|
||||
GopeedTransport createTransport(GopeedTransportConfig config) => RestGopeedTransport(config);
|
||||
|
||||
class RestGopeedTransport implements GopeedTransport {
|
||||
RestGopeedTransport(GopeedTransportConfig config)
|
||||
: _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: '${Uri.base.origin}/',
|
||||
contentType: Headers.jsonContentType,
|
||||
sendTimeout: const Duration(seconds: 5),
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 120),
|
||||
),
|
||||
) {
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
if (config.apiToken.isNotEmpty) {
|
||||
options.headers['X-Api-Token'] = config.apiToken;
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
if (error.response?.statusCode == 401) {
|
||||
config.onUnauthorized?.call();
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
@override
|
||||
Future<dynamic> request(
|
||||
String path, {
|
||||
String method = 'GET',
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
}) async {
|
||||
final response = await _dio.request<dynamic>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(method: method),
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
|
||||
options ??= Options();
|
||||
options.headers ??= {};
|
||||
options.headers!['X-Target-Uri'] = uri;
|
||||
return _dio.request<String>(
|
||||
'/api/web/proxy?t=${DateTime.now().millisecondsSinceEpoch}',
|
||||
data: data,
|
||||
options: options.copyWith(responseType: ResponseType.plain),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String join(String path) {
|
||||
final baseUrl = _dio.options.baseUrl;
|
||||
final cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
|
||||
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
|
||||
return '$cleanBaseUrl/$cleanPath';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'entry/gopeed_transport_stub.dart'
|
||||
if (dart.library.html) 'entry/gopeed_transport_web.dart'
|
||||
if (dart.library.io) 'entry/gopeed_transport_native.dart';
|
||||
|
||||
class GopeedTransportConfig {
|
||||
const GopeedTransportConfig({
|
||||
required this.network,
|
||||
required this.address,
|
||||
required this.apiToken,
|
||||
this.onUnauthorized,
|
||||
});
|
||||
|
||||
final String network;
|
||||
final String address;
|
||||
final String apiToken;
|
||||
final void Function()? onUnauthorized;
|
||||
}
|
||||
|
||||
abstract interface class GopeedTransport {
|
||||
Future<dynamic> request(String path, {String method = 'GET', dynamic data, Map<String, dynamic>? queryParameters});
|
||||
|
||||
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options});
|
||||
|
||||
String join(String path);
|
||||
}
|
||||
|
||||
GopeedTransport createGopeedTransport(GopeedTransportConfig config) => createTransport(config);
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart' show Colors;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import 'app_window_chrome.dart';
|
||||
import 'app_window_payload.dart';
|
||||
@@ -16,9 +15,8 @@ class AppWindowBootstrap {
|
||||
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
final windowState = Database.instance.getWindowState();
|
||||
final windowOptions = WindowOptions(
|
||||
size: Size(windowState?.width ?? 1024, windowState?.height ?? 768),
|
||||
size: const Size(1024, 768),
|
||||
center: true,
|
||||
skipTaskbar: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
|
||||
Reference in new issue
Block a user