diff --git a/lib/common/conf/binary_conf.dart b/lib/common/conf/binary_conf.dart new file mode 100644 index 0000000..9d34558 --- /dev/null +++ b/lib/common/conf/binary_conf.dart @@ -0,0 +1,44 @@ +import 'dart:io'; + +import 'package:archive/archive.dart'; +import 'package:flutter/services.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; + +class BinaryModuleConf { + static const _modules = { + "aria2c": "0", + }; + + static Future extractModule(List modules, String workingDir) async { + for (var m in _modules.entries) { + if (!modules.contains(m.key)) continue; + final name = m.key; + final version = m.value; + final dir = "$workingDir\\$name"; + final versionFile = File("$dir\\version"); + if (await versionFile.exists() && + (await versionFile.readAsString()).trim() == version) { + dPrint( + "BinaryModuleConf.extractModule skip $name version == $version"); + continue; + } + // write model file + final zipBuffer = await rootBundle.load("assets/binary/$name.zip"); + final decoder = ZipDecoder().decodeBytes(zipBuffer.buffer.asUint8List()); + for (var value in decoder.files) { + final filename = value.name; + if (value.isFile) { + final data = value.content as List; + final file = File('$dir\\$filename'); + await file.create(recursive: true); + await file.writeAsBytes(data); + } else { + await Directory('$dir\\$filename').create(recursive: true); + } + } + // write version file + await versionFile.writeAsString(version); + dPrint("BinaryModuleConf.extractModule $name $dir"); + } + } +} diff --git a/lib/common/io/aria2c.dart b/lib/common/io/aria2c.dart index 81f01a0..ca9dfa3 100644 --- a/lib/common/io/aria2c.dart +++ b/lib/common/io/aria2c.dart @@ -7,157 +7,9 @@ import 'package:hive/hive.dart'; import 'package:starcitizen_doctor/api/api.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; -import 'package:starcitizen_doctor/common/rust/api/process_api.dart' - as rs_process; + import 'package:starcitizen_doctor/common/utils/log.dart'; class Aria2cManager { - static bool _isDaemonRunning = false; - // static final String _aria2cDir = "${AppConf.applicationBinaryModuleDir}\\aria2c"; - static final String _aria2cDir = "\\aria2c"; - - static Aria2c? _aria2c; - - static Aria2c getClient() { - if (_aria2c != null) return _aria2c!; - throw "not connect!"; - } - - static bool get isAvailable => _isDaemonRunning && _aria2c != null; - - static Future checkLazyLoad() async { - try { - final sessionFile = File("$_aria2cDir\\aria2.session"); - // 有下载任务则第一时间初始化 - if (await sessionFile.exists() && - (await sessionFile.readAsString()).trim().isNotEmpty) { - await launchDaemon(); - } - } catch (e) { - dPrint("Aria2cManager.checkLazyLoad Error:$e"); - } - } - - static Future launchDaemon() async { - if (_isDaemonRunning) return; - // await BinaryModuleConf.extractModule(["aria2c"]); - - /// skip for debug hot reload - if (kDebugMode) { - if ((await SystemHelper.getPID("aria2c")).isNotEmpty) { - dPrint("[Aria2cManager] debug skip for hot reload"); - return; - } - } - - final sessionFile = File("$_aria2cDir\\aria2.session"); - if (!await sessionFile.exists()) { - await sessionFile.create(recursive: true); - } - - final exePath = "$_aria2cDir\\aria2c.exe"; - final port = await getFreePort(); - final pwd = generateRandomPassword(16); - dPrint("pwd === $pwd"); - final trackerList = await Api.getTorrentTrackerList(); - dPrint("trackerList === $trackerList"); - dPrint("Aria2cManager .----- aria2c start $port------"); - - final stream = rs_process.startProcess( - executable: exePath, - arguments: [ - "-V", - "-c", - "-x 10", - "--dir=$_aria2cDir\\downloads", - "--disable-ipv6", - "--enable-rpc", - "--pause", - "--rpc-listen-port=$port", - "--rpc-secret=$pwd", - "--input-file=${sessionFile.absolute.path.trim()}", - "--save-session=${sessionFile.absolute.path.trim()}", - "--save-session-interval=60", - "--file-allocation=trunc", - "--seed-time=0", - ], - workingDirectory: _aria2cDir); - - String launchError = ""; - - stream.listen((event) { - dPrint("Aria2cManager.rs_process event === $event"); - if (event.startsWith("output:")) { - if (event.contains("IPv4 RPC: listening on TCP port")) { - _onLaunch(port, pwd, trackerList); - } - } else if (event.startsWith("error:")) { - _isDaemonRunning = false; - _aria2c = null; - launchError = event; - } else if (event.startsWith("exit:")) { - _isDaemonRunning = false; - _aria2c = null; - launchError = event; - } - }); - - while (true) { - if (_isDaemonRunning) return; - if (launchError.isNotEmpty) throw launchError; - await Future.delayed(const Duration(milliseconds: 100)); - } - } - - static Future getFreePort() async { - final serverSocket = await ServerSocket.bind("127.0.0.1", 0); - final port = serverSocket.port; - await serverSocket.close(); - return port; - } - - static String generateRandomPassword(int length) { - const String charset = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - Random random = Random(); - StringBuffer buffer = StringBuffer(); - for (int i = 0; i < length; i++) { - int randomIndex = random.nextInt(charset.length); - buffer.write(charset[randomIndex]); - } - return buffer.toString(); - } - - static int textToByte(String text) { - if (text.length == 1) { - return 0; - } - if (int.tryParse(text) != null) { - return int.parse(text); - } - if (text.endsWith("k")) { - return int.parse(text.substring(0, text.length - 1)) * 1024; - } - if (text.endsWith("m")) { - return int.parse(text.substring(0, text.length - 1)) * 1024 * 1024; - } - return 0; - } - - static Future _onLaunch( - int port, String pwd, String trackerList) async { - _isDaemonRunning = true; - _aria2c = Aria2c("ws://127.0.0.1:$port/jsonrpc", "websocket", pwd); - _aria2c!.getVersion().then((value) { - dPrint("Aria2cManager.connected! version == ${value.version}"); - }); - final box = await Hive.openBox("app_conf"); - _aria2c!.changeGlobalOption(Aria2Option() - ..maxOverallUploadLimit = - textToByte(box.get("downloader_up_limit", defaultValue: "0")) - ..maxOverallDownloadLimit = - textToByte(box.get("downloader_down_limit", defaultValue: "0")) - ..btTracker = trackerList); - } } diff --git a/lib/common/utils/provider.dart b/lib/common/utils/provider.dart new file mode 100644 index 0000000..e1c5c2c --- /dev/null +++ b/lib/common/utils/provider.dart @@ -0,0 +1,9 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:starcitizen_doctor/app.dart'; + +extension ProviderExtension on AutoDisposeNotifier { + AppGlobalModel get appGlobalModel => + ref.read(appGlobalModelProvider.notifier); + + AppGlobalState get appGlobalState => ref.read(appGlobalModelProvider); +} diff --git a/lib/provider/aria2c.dart b/lib/provider/aria2c.dart new file mode 100644 index 0000000..c4b5f8a --- /dev/null +++ b/lib/provider/aria2c.dart @@ -0,0 +1,178 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:starcitizen_doctor/common/conf/binary_conf.dart'; +import 'package:starcitizen_doctor/common/rust/api/process_api.dart' + as rs_process; +import 'dart:io'; +import 'dart:math'; +import 'package:aria2/aria2.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:starcitizen_doctor/api/api.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.dart'; + +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/common/utils/provider.dart'; + +part 'aria2c.g.dart'; + +part 'aria2c.freezed.dart'; + +@freezed +class Aria2cModelState with _$Aria2cModelState { + const factory Aria2cModelState({ + required bool isDaemonRunning, + required String aria2cDir, + Aria2c? aria2c, + }) = _Aria2cModelState; +} + +@riverpod +class Aria2cModel extends _$Aria2cModel { + @override + Aria2cModelState build() { + if (appGlobalState.applicationBinaryModuleDir == null) { + throw Exception("applicationBinaryModuleDir is null"); + } + ref.keepAlive(); + final aria2cDir = "${appGlobalState.applicationBinaryModuleDir}\\aria2c"; + // LazyLoad init + () async { + try { + final sessionFile = File("$aria2cDir\\aria2.session"); + // 有下载任务则第一时间初始化 + if (await sessionFile.exists() && + (await sessionFile.readAsString()).trim().isNotEmpty) { + dPrint("launch Aria2c daemon"); + await launchDaemon(appGlobalState.applicationBinaryModuleDir!); + } else { + dPrint("LazyLoad Aria2c daemon"); + } + } catch (e) { + dPrint("Aria2cManager.checkLazyLoad Error:$e"); + } + }(); + + return Aria2cModelState(isDaemonRunning: false, aria2cDir: aria2cDir); + } + + Future launchDaemon(String applicationBinaryModuleDir) async { + if (state.isDaemonRunning) return; + await BinaryModuleConf.extractModule( + ["aria2c"], applicationBinaryModuleDir); + + /// skip for debug hot reload + if (kDebugMode) { + if ((await SystemHelper.getPID("aria2c")).isNotEmpty) { + dPrint("[Aria2cManager] debug skip for hot reload"); + return; + } + } + + final sessionFile = File("${state.aria2cDir}\\aria2.session"); + if (!await sessionFile.exists()) { + await sessionFile.create(recursive: true); + } + + final exePath = "${state.aria2cDir}\\aria2c.exe"; + final port = await getFreePort(); + final pwd = generateRandomPassword(16); + dPrint("pwd === $pwd"); + final trackerList = await Api.getTorrentTrackerList(); + dPrint("trackerList === $trackerList"); + dPrint("Aria2cManager .----- aria2c start $port------"); + + final stream = rs_process.startProcess( + executable: exePath, + arguments: [ + "-V", + "-c", + "-x 10", + "--dir=${state.aria2cDir}\\downloads", + "--disable-ipv6", + "--enable-rpc", + "--pause", + "--rpc-listen-port=$port", + "--rpc-secret=$pwd", + "--input-file=${sessionFile.absolute.path.trim()}", + "--save-session=${sessionFile.absolute.path.trim()}", + "--save-session-interval=60", + "--file-allocation=trunc", + "--seed-time=0", + ], + workingDirectory: state.aria2cDir); + + String launchError = ""; + + stream.listen((event) { + dPrint("Aria2cManager.rs_process event === $event"); + if (event.startsWith("output:")) { + if (event.contains("IPv4 RPC: listening on TCP port")) { + _onLaunch(port, pwd, trackerList); + } + } else if (event.startsWith("error:")) { + state = state.copyWith(aria2c: null, isDaemonRunning: false); + launchError = event; + } else if (event.startsWith("exit:")) { + state = state.copyWith(aria2c: null, isDaemonRunning: false); + launchError = event; + } + }); + + while (true) { + if (state.isDaemonRunning) return; + if (launchError.isNotEmpty) throw launchError; + await Future.delayed(const Duration(milliseconds: 100)); + } + } + + Future getFreePort() async { + final serverSocket = await ServerSocket.bind("127.0.0.1", 0); + final port = serverSocket.port; + await serverSocket.close(); + return port; + } + + String generateRandomPassword(int length) { + const String charset = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + Random random = Random(); + StringBuffer buffer = StringBuffer(); + for (int i = 0; i < length; i++) { + int randomIndex = random.nextInt(charset.length); + buffer.write(charset[randomIndex]); + } + return buffer.toString(); + } + + int textToByte(String text) { + if (text.length == 1) { + return 0; + } + if (int.tryParse(text) != null) { + return int.parse(text); + } + if (text.endsWith("k")) { + return int.parse(text.substring(0, text.length - 1)) * 1024; + } + if (text.endsWith("m")) { + return int.parse(text.substring(0, text.length - 1)) * 1024 * 1024; + } + return 0; + } + + Future _onLaunch(int port, String pwd, String trackerList) async { + final aria2c = Aria2c("ws://127.0.0.1:$port/jsonrpc", "websocket", pwd); + state = state.copyWith(aria2c: aria2c, isDaemonRunning: true); + aria2c.getVersion().then((value) { + dPrint("Aria2cManager.connected! version == ${value.version}"); + }); + final box = await Hive.openBox("app_conf"); + aria2c.changeGlobalOption(Aria2Option() + ..maxOverallUploadLimit = + textToByte(box.get("downloader_up_limit", defaultValue: "0")) + ..maxOverallDownloadLimit = + textToByte(box.get("downloader_down_limit", defaultValue: "0")) + ..btTracker = trackerList); + } +} diff --git a/lib/provider/aria2c.freezed.dart b/lib/provider/aria2c.freezed.dart new file mode 100644 index 0000000..d421447 --- /dev/null +++ b/lib/provider/aria2c.freezed.dart @@ -0,0 +1,184 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'aria2c.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$Aria2cModelState { + bool get isDaemonRunning => throw _privateConstructorUsedError; + String get aria2cDir => throw _privateConstructorUsedError; + Aria2c? get aria2c => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $Aria2cModelStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $Aria2cModelStateCopyWith<$Res> { + factory $Aria2cModelStateCopyWith( + Aria2cModelState value, $Res Function(Aria2cModelState) then) = + _$Aria2cModelStateCopyWithImpl<$Res, Aria2cModelState>; + @useResult + $Res call({bool isDaemonRunning, String aria2cDir, Aria2c? aria2c}); +} + +/// @nodoc +class _$Aria2cModelStateCopyWithImpl<$Res, $Val extends Aria2cModelState> + implements $Aria2cModelStateCopyWith<$Res> { + _$Aria2cModelStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isDaemonRunning = null, + Object? aria2cDir = null, + Object? aria2c = freezed, + }) { + return _then(_value.copyWith( + isDaemonRunning: null == isDaemonRunning + ? _value.isDaemonRunning + : isDaemonRunning // ignore: cast_nullable_to_non_nullable + as bool, + aria2cDir: null == aria2cDir + ? _value.aria2cDir + : aria2cDir // ignore: cast_nullable_to_non_nullable + as String, + aria2c: freezed == aria2c + ? _value.aria2c + : aria2c // ignore: cast_nullable_to_non_nullable + as Aria2c?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$Aria2cModelStateImplCopyWith<$Res> + implements $Aria2cModelStateCopyWith<$Res> { + factory _$$Aria2cModelStateImplCopyWith(_$Aria2cModelStateImpl value, + $Res Function(_$Aria2cModelStateImpl) then) = + __$$Aria2cModelStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool isDaemonRunning, String aria2cDir, Aria2c? aria2c}); +} + +/// @nodoc +class __$$Aria2cModelStateImplCopyWithImpl<$Res> + extends _$Aria2cModelStateCopyWithImpl<$Res, _$Aria2cModelStateImpl> + implements _$$Aria2cModelStateImplCopyWith<$Res> { + __$$Aria2cModelStateImplCopyWithImpl(_$Aria2cModelStateImpl _value, + $Res Function(_$Aria2cModelStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isDaemonRunning = null, + Object? aria2cDir = null, + Object? aria2c = freezed, + }) { + return _then(_$Aria2cModelStateImpl( + isDaemonRunning: null == isDaemonRunning + ? _value.isDaemonRunning + : isDaemonRunning // ignore: cast_nullable_to_non_nullable + as bool, + aria2cDir: null == aria2cDir + ? _value.aria2cDir + : aria2cDir // ignore: cast_nullable_to_non_nullable + as String, + aria2c: freezed == aria2c + ? _value.aria2c + : aria2c // ignore: cast_nullable_to_non_nullable + as Aria2c?, + )); + } +} + +/// @nodoc + +class _$Aria2cModelStateImpl + with DiagnosticableTreeMixin + implements _Aria2cModelState { + const _$Aria2cModelStateImpl( + {required this.isDaemonRunning, required this.aria2cDir, this.aria2c}); + + @override + final bool isDaemonRunning; + @override + final String aria2cDir; + @override + final Aria2c? aria2c; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'Aria2cModelState(isDaemonRunning: $isDaemonRunning, aria2cDir: $aria2cDir, aria2c: $aria2c)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'Aria2cModelState')) + ..add(DiagnosticsProperty('isDaemonRunning', isDaemonRunning)) + ..add(DiagnosticsProperty('aria2cDir', aria2cDir)) + ..add(DiagnosticsProperty('aria2c', aria2c)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$Aria2cModelStateImpl && + (identical(other.isDaemonRunning, isDaemonRunning) || + other.isDaemonRunning == isDaemonRunning) && + (identical(other.aria2cDir, aria2cDir) || + other.aria2cDir == aria2cDir) && + (identical(other.aria2c, aria2c) || other.aria2c == aria2c)); + } + + @override + int get hashCode => + Object.hash(runtimeType, isDaemonRunning, aria2cDir, aria2c); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$Aria2cModelStateImplCopyWith<_$Aria2cModelStateImpl> get copyWith => + __$$Aria2cModelStateImplCopyWithImpl<_$Aria2cModelStateImpl>( + this, _$identity); +} + +abstract class _Aria2cModelState implements Aria2cModelState { + const factory _Aria2cModelState( + {required final bool isDaemonRunning, + required final String aria2cDir, + final Aria2c? aria2c}) = _$Aria2cModelStateImpl; + + @override + bool get isDaemonRunning; + @override + String get aria2cDir; + @override + Aria2c? get aria2c; + @override + @JsonKey(ignore: true) + _$$Aria2cModelStateImplCopyWith<_$Aria2cModelStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/provider/aria2c.g.dart b/lib/provider/aria2c.g.dart new file mode 100644 index 0000000..acfd355 --- /dev/null +++ b/lib/provider/aria2c.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'aria2c.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$aria2cModelHash() => r'45bfdd07a11bb93594b76297a995e8ba24e0e984'; + +/// See also [Aria2cModel]. +@ProviderFor(Aria2cModel) +final aria2cModelProvider = + AutoDisposeNotifierProvider.internal( + Aria2cModel.new, + name: r'aria2cModelProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$aria2cModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Aria2cModel = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/ui/splash_ui.dart b/lib/ui/splash_ui.dart index e662917..1c8443f 100644 --- a/lib/ui/splash_ui.dart +++ b/lib/ui/splash_ui.dart @@ -7,6 +7,7 @@ import 'package:starcitizen_doctor/common/conf/const_conf.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/io/aria2c.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/provider/aria2c.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; class SplashUI extends HookConsumerWidget { @@ -19,7 +20,7 @@ class SplashUI extends HookConsumerWidget { useEffect(() { final appModel = ref.read(appGlobalModelProvider.notifier); - _initApp(context, appModel, stepState); + _initApp(context, appModel, stepState, ref); return null; }, const []); @@ -58,7 +59,7 @@ class SplashUI extends HookConsumerWidget { } void _initApp(BuildContext context, AppGlobalModel appModel, - ValueNotifier stepState) async { + ValueNotifier stepState, WidgetRef ref) async { await appModel.initApp(); AnalyticsApi.touch("launch"); try { @@ -70,7 +71,7 @@ class SplashUI extends HookConsumerWidget { if (!context.mounted) return; await appModel.checkUpdate(context); stepState.value = 2; - await Aria2cManager.checkLazyLoad(); + ref.read(aria2cModelProvider); // Navigator.pushAndRemoveUntil( // context!, // BaseUIContainer(