mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 17:40:28 +00:00
feat: move native communication to FFI (#1457)
This commit is contained in:
86 files changed
+4558
-886
No files matched your search
@@ -8,14 +8,15 @@ import 'package:path_provider/path_provider.dart';
|
||||
import '../../api/api.dart' as api;
|
||||
import '../../api/model/downloader_config.dart';
|
||||
import '../../core/capabilities/app_capabilities.dart';
|
||||
import '../../core/common/api_server_state.dart';
|
||||
import '../../core/common/start_config.dart';
|
||||
import '../../core/common/task_event.dart';
|
||||
import '../../core/entry/app_initializer.dart';
|
||||
import '../../core/libgopeed_boot.dart';
|
||||
import '../../database/database.dart';
|
||||
import '../../database/entity.dart';
|
||||
import '../../features/auth/application/web_auth_controller.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../../util/log_util.dart';
|
||||
import '../../util/analytics.dart';
|
||||
import '../../util/package_info.dart';
|
||||
import '../../util/util.dart';
|
||||
import '../rpc/webview_rpc_service.dart';
|
||||
@@ -31,28 +32,26 @@ final appRuntimeControllerProvider = AsyncNotifierProvider<AppRuntimeController,
|
||||
class AppRuntimeState {
|
||||
const AppRuntimeState({
|
||||
required this.startConfig,
|
||||
required this.runningPort,
|
||||
required this.apiServerState,
|
||||
required this.downloaderConfig,
|
||||
this.localBackendStarted = false,
|
||||
this.startupError,
|
||||
});
|
||||
|
||||
final StartConfig startConfig;
|
||||
final int runningPort;
|
||||
final ApiServerState apiServerState;
|
||||
final DownloaderConfig downloaderConfig;
|
||||
final bool localBackendStarted;
|
||||
final Object? startupError;
|
||||
|
||||
String runningAddress() {
|
||||
if (startConfig.network == 'unix') {
|
||||
return startConfig.address;
|
||||
}
|
||||
return '${startConfig.address.split(':').first}:$runningPort';
|
||||
}
|
||||
int get runningPort => apiServerState.runningPort;
|
||||
|
||||
String runningAddress() => apiServerState.runningAddress();
|
||||
}
|
||||
|
||||
class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
static StartConfig? _defaultStartConfig;
|
||||
Future<void> _preferenceWrites = Future<void>.value();
|
||||
|
||||
@override
|
||||
Future<AppRuntimeState> build() async {
|
||||
@@ -63,11 +62,18 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
Future<void> reloadConfig() async {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
final config = await ref.read(gopeedServiceProvider).getConfig();
|
||||
final apiState = kIsWeb ? current.apiServerState : (await LibgopeedBoot.instance.getApiServerState()).state;
|
||||
final startConfig = _copyStartConfig(current.startConfig)
|
||||
..apiEnable = config.api.enable
|
||||
..network = config.api.network
|
||||
..address = config.api.address
|
||||
..apiToken = config.api.token;
|
||||
state = AsyncValue.data(
|
||||
AppRuntimeState(
|
||||
startConfig: current.startConfig,
|
||||
runningPort: current.runningPort,
|
||||
downloaderConfig: await ref.read(gopeedServiceProvider).getConfig(),
|
||||
startConfig: startConfig,
|
||||
apiServerState: apiState,
|
||||
downloaderConfig: config,
|
||||
localBackendStarted: current.localBackendStarted,
|
||||
startupError: current.startupError,
|
||||
),
|
||||
@@ -80,7 +86,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
state = AsyncValue.data(
|
||||
AppRuntimeState(
|
||||
startConfig: current.startConfig,
|
||||
runningPort: current.runningPort,
|
||||
apiServerState: current.apiServerState,
|
||||
downloaderConfig: DownloaderConfig.fromJson(config.toJson()),
|
||||
localBackendStarted: current.localBackendStarted,
|
||||
startupError: current.startupError,
|
||||
@@ -90,7 +96,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
}
|
||||
|
||||
Future<AppRuntimeState> _init() async {
|
||||
await AppInitializer.ensureDatabaseInitialized();
|
||||
await AppInitializer.ensureStorageInitialized();
|
||||
try {
|
||||
await initPackageInfo();
|
||||
} catch (_) {}
|
||||
@@ -104,37 +110,57 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
}
|
||||
Object? startupError;
|
||||
var runningPort = 0;
|
||||
var apiServerState = const ApiServerState(
|
||||
enabled: false,
|
||||
running: false,
|
||||
network: '',
|
||||
address: '',
|
||||
runningPort: 0,
|
||||
pendingApply: false,
|
||||
lastError: '',
|
||||
);
|
||||
var localBackendStarted = false;
|
||||
try {
|
||||
// Go's rest.Start is already idempotent: when its server is running it
|
||||
// returns the existing port before rebuilding or touching the Unix
|
||||
// socket. This is also what keeps Riverpod hot reload safe.
|
||||
runningPort = await LibgopeedBoot.instance.start(startConfig);
|
||||
await LibgopeedBoot.instance.subscribeTaskEvents({TaskEventType.done, TaskEventType.error});
|
||||
if (!kIsWeb) {
|
||||
final statusResult = await LibgopeedBoot.instance.getApiServerState();
|
||||
apiServerState = statusResult.state;
|
||||
if (statusResult.error.isNotEmpty) {
|
||||
logger.w('read API server state failed: ${statusResult.error}');
|
||||
}
|
||||
}
|
||||
localBackendStarted = true;
|
||||
} catch (error, stackTrace) {
|
||||
startupError = error;
|
||||
logger.w('libgopeed init failed, falling back to external API', error, stackTrace);
|
||||
if (startConfig.network == 'unix') {
|
||||
startConfig.network = 'tcp';
|
||||
startConfig.address = '127.0.0.1:9999';
|
||||
}
|
||||
runningPort = int.tryParse(startConfig.address.split(':').last) ?? 9999;
|
||||
logger.e('libgopeed init failed', error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
api.init(
|
||||
startConfig.network,
|
||||
_runningAddress(startConfig, runningPort),
|
||||
startConfig.apiToken,
|
||||
onUnauthorized: ref.read(webAuthControllerProvider).requireLogin,
|
||||
);
|
||||
if (kIsWeb) {
|
||||
final webAuth = ref.read(webAuthControllerProvider);
|
||||
api.init('tcp', Uri.base.authority, '', onUnauthorized: webAuth.requireLogin);
|
||||
} else {
|
||||
api.init(startConfig.network, _runningAddress(startConfig, runningPort), startConfig.apiToken);
|
||||
}
|
||||
|
||||
final config = await _loadDownloaderConfig();
|
||||
if (!kIsWeb) {
|
||||
startConfig
|
||||
..apiEnable = config.api.enable
|
||||
..network = config.api.network
|
||||
..address = config.api.address
|
||||
..apiToken = config.api.token;
|
||||
}
|
||||
unawaited(_initTrackerUpdate(config));
|
||||
unawaited(_initAndroidForegroundService(appLocalizationsFor(config.extra.locale)));
|
||||
await _initAnalytics(config);
|
||||
|
||||
final result = AppRuntimeState(
|
||||
startConfig: startConfig,
|
||||
runningPort: runningPort,
|
||||
apiServerState: apiServerState,
|
||||
downloaderConfig: config,
|
||||
localBackendStarted: localBackendStarted,
|
||||
startupError: startupError,
|
||||
@@ -158,11 +184,10 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
|
||||
Future<StartConfig> _loadStartConfig() async {
|
||||
final defaultCfg = await _initDefaultStartConfig();
|
||||
final saved = Database.instance.getStartConfig();
|
||||
final cfg = StartConfig()
|
||||
..network = saved?.network ?? defaultCfg.network
|
||||
..address = saved?.address ?? defaultCfg.address
|
||||
..apiToken = saved?.apiToken ?? defaultCfg.apiToken
|
||||
..network = defaultCfg.network
|
||||
..address = defaultCfg.address
|
||||
..apiToken = defaultCfg.apiToken
|
||||
..storage = defaultCfg.storage
|
||||
..storageDir = defaultCfg.storageDir
|
||||
..refreshInterval = defaultCfg.refreshInterval;
|
||||
@@ -178,6 +203,13 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
..storageDir = Util.getStorageDir()
|
||||
..refreshInterval = 0
|
||||
..apiToken = '';
|
||||
if (kIsWeb) {
|
||||
cfg
|
||||
..network = 'tcp'
|
||||
..address = Uri.base.authority;
|
||||
_defaultStartConfig = cfg;
|
||||
return cfg;
|
||||
}
|
||||
if (!Util.supportUnixSocket()) {
|
||||
cfg
|
||||
..network = 'tcp'
|
||||
@@ -270,6 +302,20 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initAnalytics(DownloaderConfig config) async {
|
||||
if (!AnalyticsConfig.isConfigured || !config.extra.analyticsEnabled) return;
|
||||
try {
|
||||
await Analytics.instance.init(fallbackClientId: config.extra.analyticsClientId);
|
||||
if (config.extra.analyticsClientId.isEmpty && Analytics.instance.clientId.isNotEmpty) {
|
||||
config.extra.analyticsClientId = Analytics.instance.clientId;
|
||||
await ref.read(gopeedServiceProvider).putConfig(config);
|
||||
}
|
||||
unawaited(Analytics.instance.logAppOpen());
|
||||
} catch (error, stackTrace) {
|
||||
logger.w('GA4 init failed', error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTrackers(DownloaderConfig config) async {
|
||||
final btExtra = config.extra.bt;
|
||||
final trackers = <String>[];
|
||||
@@ -290,7 +336,7 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
state = AsyncValue.data(
|
||||
AppRuntimeState(
|
||||
startConfig: current.startConfig,
|
||||
runningPort: current.runningPort,
|
||||
apiServerState: current.apiServerState,
|
||||
downloaderConfig: config,
|
||||
localBackendStarted: current.localBackendStarted,
|
||||
startupError: current.startupError,
|
||||
@@ -322,11 +368,116 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
return '${config.address.split(':').first}:$runningPort';
|
||||
}
|
||||
|
||||
Future<void> saveStartConfig(StartConfig config) async {
|
||||
Database.instance.saveStartConfig(
|
||||
StartConfigEntity(network: config.network, address: config.address, apiToken: config.apiToken),
|
||||
Future<void> saveApiServerConfig(StartConfig config) async {
|
||||
if (kIsWeb) return;
|
||||
await _persistApiServerConfig(config);
|
||||
}
|
||||
|
||||
Future<void> startApiServer() async {
|
||||
if (kIsWeb) return;
|
||||
await _persistApiServerEnabled(true);
|
||||
await _applyApiServerOperation(LibgopeedBoot.instance.startApiServer());
|
||||
}
|
||||
|
||||
Future<void> stopApiServer() async {
|
||||
if (kIsWeb) return;
|
||||
await _persistApiServerEnabled(false);
|
||||
await _applyApiServerOperation(LibgopeedBoot.instance.stopApiServer());
|
||||
}
|
||||
|
||||
Future<void> restartApiServer(StartConfig config) async {
|
||||
if (kIsWeb) return;
|
||||
final desired = _copyStartConfig(config)..apiEnable = true;
|
||||
await _persistApiServerConfig(desired);
|
||||
await _applyApiServerOperation(LibgopeedBoot.instance.restartApiServer());
|
||||
}
|
||||
|
||||
Future<void> refreshApiServerState() async {
|
||||
if (kIsWeb) return;
|
||||
await _applyApiServerOperation(LibgopeedBoot.instance.getApiServerState());
|
||||
}
|
||||
|
||||
Future<void> _persistApiServerConfig(StartConfig config) async {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
final nextConfig = _copyStartConfig(config);
|
||||
final downloaderConfig = await ref.read(gopeedServiceProvider).getConfig();
|
||||
downloaderConfig.api
|
||||
..enable = nextConfig.apiEnable
|
||||
..network = nextConfig.network
|
||||
..address = nextConfig.address
|
||||
..token = nextConfig.apiToken;
|
||||
await ref.read(gopeedServiceProvider).putConfig(downloaderConfig);
|
||||
final statusResult = await LibgopeedBoot.instance.getApiServerState();
|
||||
_replaceApiRuntime(nextConfig, downloaderConfig, statusResult.state);
|
||||
if (statusResult.error.isNotEmpty) {
|
||||
throw StateError(statusResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistApiServerEnabled(bool enabled) async {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
final downloaderConfig = await ref.read(gopeedServiceProvider).getConfig();
|
||||
downloaderConfig.api.enable = enabled;
|
||||
await ref.read(gopeedServiceProvider).putConfig(downloaderConfig);
|
||||
final nextConfig = _copyStartConfig(current.startConfig)..apiEnable = enabled;
|
||||
final statusResult = await LibgopeedBoot.instance.getApiServerState();
|
||||
_replaceApiRuntime(nextConfig, downloaderConfig, statusResult.state);
|
||||
if (statusResult.error.isNotEmpty) {
|
||||
throw StateError(statusResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyApiServerOperation(Future<ApiServerOperationResult> operation) async {
|
||||
final result = await operation;
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
_replaceApiRuntime(current.startConfig, current.downloaderConfig, result.state);
|
||||
}
|
||||
if (result.error.isNotEmpty) {
|
||||
throw StateError(result.error);
|
||||
}
|
||||
}
|
||||
|
||||
void _replaceApiRuntime(StartConfig config, DownloaderConfig downloaderConfig, ApiServerState apiState) {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
state = AsyncValue.data(
|
||||
AppRuntimeState(
|
||||
startConfig: _copyStartConfig(config),
|
||||
apiServerState: apiState,
|
||||
downloaderConfig: DownloaderConfig.fromJson(downloaderConfig.toJson()),
|
||||
localBackendStarted: current.localBackendStarted,
|
||||
startupError: current.startupError,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
StartConfig _copyStartConfig(StartConfig config) {
|
||||
return StartConfig()
|
||||
..network = config.network
|
||||
..address = config.address
|
||||
..apiEnable = config.apiEnable
|
||||
..storage = config.storage
|
||||
..storageDir = config.storageDir
|
||||
..refreshInterval = config.refreshInterval
|
||||
..apiToken = config.apiToken
|
||||
..webViewRpcConfig = config.webViewRpcConfig;
|
||||
}
|
||||
|
||||
Future<void> updateExtraConfig(void Function(ExtraConfig extra) mutation) {
|
||||
final operation = _preferenceWrites.then((_) async {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
final config = await ref.read(gopeedServiceProvider).getConfig();
|
||||
mutation(config.extra);
|
||||
await ref.read(gopeedServiceProvider).putConfig(config);
|
||||
replaceDownloaderConfig(config);
|
||||
});
|
||||
_preferenceWrites = operation.then<void>((_) {}, onError: (_, _) {});
|
||||
return operation;
|
||||
}
|
||||
}
|
||||
|
||||
const allTrackerSubscribeUrls = [
|
||||
|
||||
Reference in new issue
Block a user