diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 42343e08..5d73e6b8 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -90,8 +90,7 @@ Future initAppServices( _loggy.debug("initializing app services"); await Future.wait( [ - read(connectivityServiceProvider).init(), - read(notificationServiceProvider).init(), + read(singboxServiceProvider).init(), ], ); _loggy.debug('initialized app services'); diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index 46e27adc..d62220d1 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -58,6 +58,5 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), ref.watch(filesEditorServiceProvider), ref.watch(clashApiProvider), - ref.watch(connectivityServiceProvider), () => ref.read(configOptionsProvider), ); diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index 19a15032..238d0a6d 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -9,7 +9,6 @@ import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/services/connectivity/connectivity.dart'; import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; @@ -19,14 +18,12 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { this.singbox, this.filesEditor, this.clash, - this.connectivity, this.configOptions, ); final SingboxService singbox; final FilesEditorService filesEditor; final ClashApi clash; - final ConnectivityService connectivity; final ConfigOptions Function() configOptions; bool _initialized = false; @@ -89,16 +86,14 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { } @override - TaskEither changeConfig(String fileName) { + TaskEither start(String fileName) { return exceptionHandler( () { final configPath = filesEditor.configPath(fileName); - loggy.debug("changing config to: $configPath"); return setup() .andThen(() => changeConfigOptions(configOptions())) .andThen( - () => - singbox.create(configPath).mapLeft(CoreServiceFailure.create), + () => singbox.start(configPath).mapLeft(CoreServiceFailure.start), ) .run(); }, @@ -107,17 +102,25 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { } @override - TaskEither start() { + TaskEither stop() { return exceptionHandler( - () => singbox.start().mapLeft(CoreServiceFailure.start).run(), + () => singbox.stop().mapLeft(CoreServiceFailure.other).run(), CoreServiceFailure.unexpected, ); } @override - TaskEither stop() { + TaskEither restart(String fileName) { return exceptionHandler( - () => singbox.stop().mapLeft(CoreServiceFailure.other).run(), + () { + final configPath = filesEditor.configPath(fileName); + return changeConfigOptions(configOptions()) + .andThen( + () => + singbox.restart(configPath).mapLeft(CoreServiceFailure.start), + ) + .run(); + }, CoreServiceFailure.unexpected, ); } @@ -160,7 +163,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { @override Stream> watchCoreStatus() { - return singbox.watchStatus().map((event) { + return singbox.watchStats().map((event) { final json = jsonDecode(event); return CoreStatus.fromJson(json as Map); }).handleExceptions( @@ -239,29 +242,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { ); } - @override - TaskEither connect() { - return exceptionHandler( - () async { - await connectivity.connect(); - return right(unit); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither disconnect() { - return exceptionHandler( - () async { - await connectivity.disconnect(); - return right(unit); - }, - CoreServiceFailure.unexpected, - ); - } - @override Stream watchConnectionStatus() => - connectivity.watchConnectionStatus(); + singbox.watchConnectionStatus(); } diff --git a/lib/domain/connectivity/connection_facade.dart b/lib/domain/connectivity/connection_facade.dart deleted file mode 100644 index 87b327d5..00000000 --- a/lib/domain/connectivity/connection_facade.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connection_status.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; - -abstract interface class ConnectionFacade { - TaskEither connect(); - - TaskEither disconnect(); - - Stream watchConnectionStatus(); -} diff --git a/lib/domain/core_facade.dart b/lib/domain/core_facade.dart index 2679e5b1..a3ada4aa 100644 --- a/lib/domain/core_facade.dart +++ b/lib/domain/core_facade.dart @@ -1,6 +1,4 @@ import 'package:hiddify/domain/clash/clash.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; -abstract interface class CoreFacade - implements SingboxFacade, ClashFacade, ConnectionFacade {} +abstract interface class CoreFacade implements SingboxFacade, ClashFacade {} diff --git a/lib/domain/singbox/singbox_facade.dart b/lib/domain/singbox/singbox_facade.dart index d8714e2e..496da38d 100644 --- a/lib/domain/singbox/singbox_facade.dart +++ b/lib/domain/singbox/singbox_facade.dart @@ -1,4 +1,5 @@ import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/singbox/config_options.dart'; import 'package:hiddify/domain/singbox/core_status.dart'; @@ -13,12 +14,12 @@ abstract interface class SingboxFacade { ConfigOptions options, ); - TaskEither changeConfig(String fileName); - - TaskEither start(); + TaskEither start(String fileName); TaskEither stop(); + TaskEither restart(String fileName); + Stream>> watchOutbounds(); TaskEither selectOutbound( @@ -28,6 +29,8 @@ abstract interface class SingboxFacade { TaskEither urlTest(String groupTag); + Stream watchConnectionStatus(); + Stream> watchCoreStatus(); Stream> watchLogs(); diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/common/connectivity/connectivity_controller.dart index 3e7a4ae1..23df1282 100644 --- a/lib/features/common/connectivity/connectivity_controller.dart +++ b/lib/features/common/connectivity/connectivity_controller.dart @@ -17,15 +17,14 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { if (previous == null) return; final shouldReconnect = previous != next; if (shouldReconnect) { - loggy.debug("active profile modified, reconnect"); - await reconnect(); + await reconnect(next?.id); } }, ); - return _connectivity.watchConnectionStatus(); + return _core.watchConnectionStatus(); } - CoreFacade get _connectivity => ref.watch(coreFacadeProvider); + CoreFacade get _core => ref.watch(coreFacadeProvider); Future toggleConnection() async { if (state case AsyncError()) { @@ -42,13 +41,16 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } } - Future reconnect() async { - if (state case AsyncData(:final value)) { - if (value case Connected()) { - loggy.debug("reconnecting"); - await _disconnect(); - await _connect(); + Future reconnect(String? profileId) async { + if (state case AsyncData(:final value) when value == const Connected()) { + if (profileId == null) { + return _disconnect(); } + loggy.debug("reconnecting, profile: [$profileId]"); + await _core.restart(profileId).mapLeft((l) { + loggy.warning("error reconnecting: $l"); + state = AsyncError(l, StackTrace.current); + }).run(); } } @@ -65,17 +67,14 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { Future _connect() async { final activeProfile = await ref.read(activeProfileProvider.future); - await _connectivity - .changeConfig(activeProfile!.id) - .andThen(_connectivity.connect) - .mapLeft((l) { + await _core.start(activeProfile!.id).mapLeft((l) { loggy.warning("error connecting: $l"); state = AsyncError(l, StackTrace.current); }).run(); } Future _disconnect() async { - await _connectivity.disconnect().mapLeft((l) { + await _core.stop().mapLeft((l) { loggy.warning("error disconnecting: $l"); state = AsyncError(l, StackTrace.current); }).run(); diff --git a/lib/gen/singbox_generated_bindings.dart b/lib/gen/singbox_generated_bindings.dart index b83ee1f7..a0f8be3f 100644 --- a/lib/gen/singbox_generated_bindings.dart +++ b/lib/gen/singbox_generated_bindings.dart @@ -875,21 +875,23 @@ class SingboxNativeLibrary { ffi.Pointer baseDir, ffi.Pointer workingDir, ffi.Pointer tempDir, + int statusPort, ) { return _setup( baseDir, workingDir, tempDir, + statusPort, ); } late final _setupPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>>('setup'); + ffi.Pointer, ffi.LongLong)>>('setup'); late final _setup = _setupPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>(); + ffi.Pointer, int)>(); ffi.Pointer parse( ffi.Pointer path, @@ -920,28 +922,20 @@ class SingboxNativeLibrary { late final _changeConfigOptions = _changeConfigOptionsPtr .asFunction Function(ffi.Pointer)>(); - ffi.Pointer create( + ffi.Pointer start( ffi.Pointer configPath, ) { - return _create( + return _start( configPath, ); } - late final _createPtr = _lookup< + late final _startPtr = _lookup< ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer)>>('create'); - late final _create = _createPtr + ffi.Pointer Function(ffi.Pointer)>>('start'); + late final _start = _startPtr .asFunction Function(ffi.Pointer)>(); - ffi.Pointer start() { - return _start(); - } - - late final _startPtr = - _lookup Function()>>('start'); - late final _start = _startPtr.asFunction Function()>(); - ffi.Pointer stop() { return _stop(); } @@ -950,6 +944,20 @@ class SingboxNativeLibrary { _lookup Function()>>('stop'); late final _stop = _stopPtr.asFunction Function()>(); + ffi.Pointer restart( + ffi.Pointer configPath, + ) { + return _restart( + configPath, + ); + } + + late final _restartPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer)>>('restart'); + late final _restart = _restartPtr + .asFunction Function(ffi.Pointer)>(); + ffi.Pointer startCommandClient( int command, int port, diff --git a/lib/services/connectivity/connectivity.dart b/lib/services/connectivity/connectivity.dart deleted file mode 100644 index 9b6161d6..00000000 --- a/lib/services/connectivity/connectivity.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'connectivity_service.dart'; -export 'desktop_connectivity_service.dart'; -export 'mobile_connectivity_service.dart'; diff --git a/lib/services/connectivity/connectivity_service.dart b/lib/services/connectivity/connectivity_service.dart deleted file mode 100644 index 2eb86893..00000000 --- a/lib/services/connectivity/connectivity_service.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/services/connectivity/desktop_connectivity_service.dart'; -import 'package:hiddify/services/connectivity/mobile_connectivity_service.dart'; -import 'package:hiddify/services/notification/notification.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; - -abstract class ConnectivityService { - factory ConnectivityService( - SingboxService singboxService, - NotificationService notificationService, - ) { - if (PlatformUtils.isDesktop) { - return DesktopConnectivityService(singboxService); - } - return MobileConnectivityService(singboxService, notificationService); - } - - Future init(); - - Stream watchConnectionStatus(); - - Future connect(); - - Future disconnect(); -} diff --git a/lib/services/connectivity/desktop_connectivity_service.dart b/lib/services/connectivity/desktop_connectivity_service.dart deleted file mode 100644 index 1e0d6885..00000000 --- a/lib/services/connectivity/desktop_connectivity_service.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/services/connectivity/connectivity_service.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:rxdart/rxdart.dart'; - -class DesktopConnectivityService - with InfraLogger - implements ConnectivityService { - DesktopConnectivityService(this._singboxService); - - final SingboxService _singboxService; - - late final BehaviorSubject _connectionStatus; - - @override - Future init() async { - loggy.debug("initializing"); - _connectionStatus = - BehaviorSubject.seeded(const ConnectionStatus.disconnected()); - } - - @override - Stream watchConnectionStatus() => _connectionStatus; - - @override - Future connect() async { - loggy.debug('connecting'); - _connectionStatus.value = const ConnectionStatus.connecting(); - await _singboxService.start().match( - (err) { - _connectionStatus.value = ConnectionStatus.disconnected( - CoreConnectionFailure( - CoreServiceStartFailure(err), - ), - ); - }, - (_) => _connectionStatus.value = const ConnectionStatus.connected(), - ).run(); - } - - @override - Future disconnect() async { - loggy.debug("disconnecting"); - _connectionStatus.value = const ConnectionStatus.disconnecting(); - await _singboxService.stop().getOrElse((l) { - _connectionStatus.value = const ConnectionStatus.connected(); - throw l; - }).run(); - _connectionStatus.value = const ConnectionStatus.disconnected(); - } -} diff --git a/lib/services/connectivity/mobile_connectivity_service.dart b/lib/services/connectivity/mobile_connectivity_service.dart deleted file mode 100644 index a923216d..00000000 --- a/lib/services/connectivity/mobile_connectivity_service.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/services/connectivity/connectivity_service.dart'; -import 'package:hiddify/services/notification/notification.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:rxdart/rxdart.dart'; - -// TODO: rewrite -class MobileConnectivityService - with InfraLogger - implements ConnectivityService { - MobileConnectivityService(this.singbox, this.notifications); - - final SingboxService singbox; - final NotificationService notifications; - - late final EventChannel _statusChannel; - late final EventChannel _alertsChannel; - late final ValueStream _connectionStatus; - - static CoreServiceFailure fromServiceAlert(String key, String? message) { - return switch (key) { - "EmptyConfiguration" => InvalidConfig(message), - "StartCommandServer" || - "CreateService" => - CoreServiceCreateFailure(message), - "StartService" => CoreServiceStartFailure(message), - _ => const CoreServiceOtherFailure(), - }; - } - - static ConnectionStatus fromServiceEvent(dynamic event) { - final status = event['status'] as String; - late ConnectionStatus connectionStatus; - switch (status) { - case "Stopped": - final failure = event["failure"] as String?; - final message = event["message"] as String?; - connectionStatus = ConnectionStatus.disconnected( - switch (failure) { - null => null, - "RequestVPNPermission" => MissingVpnPermission(message), - "RequestNotificationPermission" => - MissingNotificationPermission(message), - "EmptyConfiguration" || - "StartCommandServer" || - "CreateService" || - "StartService" => - CoreConnectionFailure(fromServiceAlert(failure, message)), - _ => const UnexpectedConnectionFailure(), - }, - ); - case "Starting": - connectionStatus = const Connecting(); - case "Started": - connectionStatus = const Connected(); - case "Stopping": - connectionStatus = const Disconnecting(); - } - return connectionStatus; - } - - @override - Future init() async { - loggy.debug("initializing"); - _statusChannel = const EventChannel("com.hiddify.app/service.status"); - _alertsChannel = const EventChannel("com.hiddify.app/service.alerts"); - final status = - _statusChannel.receiveBroadcastStream().map(fromServiceEvent); - final alerts = - _alertsChannel.receiveBroadcastStream().map(fromServiceEvent); - _connectionStatus = - ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); - await _connectionStatus.first; - } - - @override - Stream watchConnectionStatus() => _connectionStatus; - - @override - Future connect() async { - loggy.debug("connecting"); - await notifications.grantPermission(); - await singbox.start().getOrElse((l) => throw l).run(); - } - - @override - Future disconnect() async { - loggy.debug("disconnecting"); - await singbox.stop().getOrElse((l) => throw l).run(); - } -} diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart index 4c6222aa..87df56f8 100644 --- a/lib/services/service_providers.dart +++ b/lib/services/service_providers.dart @@ -1,6 +1,4 @@ -import 'package:hiddify/services/connectivity/connectivity.dart'; import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/services/notification/notification.dart'; import 'package:hiddify/services/platform_settings.dart'; import 'package:hiddify/services/runtime_details_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -8,10 +6,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'service_providers.g.dart'; -@Riverpod(keepAlive: true) -NotificationService notificationService(NotificationServiceRef ref) => - NotificationService(); - @Riverpod(keepAlive: true) FilesEditorService filesEditorService(FilesEditorServiceRef ref) => FilesEditorService(); @@ -23,13 +17,6 @@ RuntimeDetailsService runtimeDetailsService(RuntimeDetailsServiceRef ref) => @Riverpod(keepAlive: true) SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); -@Riverpod(keepAlive: true) -ConnectivityService connectivityService(ConnectivityServiceRef ref) => - ConnectivityService( - ref.watch(singboxServiceProvider), - ref.watch(notificationServiceProvider), - ); - @riverpod PlatformSettings platformSettings(PlatformSettingsRef ref) => PlatformSettings(); diff --git a/lib/services/singbox/ffi_singbox_service.dart b/lib/services/singbox/ffi_singbox_service.dart index 1f56e1fe..69582c6b 100644 --- a/lib/services/singbox/ffi_singbox_service.dart +++ b/lib/services/singbox/ffi_singbox_service.dart @@ -7,18 +7,25 @@ import 'dart:isolate'; import 'package:combine/combine.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/singbox/config_options.dart'; import 'package:hiddify/gen/singbox_generated_bindings.dart'; +import 'package:hiddify/services/singbox/shared.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:loggy/loggy.dart'; import 'package:path/path.dart' as p; +import 'package:rxdart/rxdart.dart'; final _logger = Loggy('FFISingboxService'); -class FFISingboxService with InfraLogger implements SingboxService { +class FFISingboxService + with ServiceStatus, InfraLogger + implements SingboxService { static final SingboxNativeLibrary _box = _gen(); + late final ValueStream _connectionStatus; + late final ReceivePort _connectionStatusReceiver; Stream? _statusStream; Stream? _groupsStream; @@ -39,21 +46,37 @@ class FFISingboxService with InfraLogger implements SingboxService { return SingboxNativeLibrary(lib); } + @override + Future init() async { + loggy.debug("initializing"); + _connectionStatusReceiver = ReceivePort('service status receiver'); + final source = _connectionStatusReceiver + .asBroadcastStream() + .map((event) => jsonDecode(event as String) as Map) + .map(mapEventToStatus); + _connectionStatus = ValueConnectableStream.seeded( + source, + const ConnectionStatus.disconnected(), + ).autoConnect(); + } + @override TaskEither setup( String baseDir, String workingDir, String tempDir, ) { + final port = _connectionStatusReceiver.sendPort.nativePort; return TaskEither( () => CombineWorker().execute( () { + _box.setupOnce(NativeApi.initializeApiDLData); _box.setup( baseDir.toNativeUtf8().cast(), workingDir.toNativeUtf8().cast(), tempDir.toNativeUtf8().cast(), + port, ); - _box.setupOnce(NativeApi.initializeApiDLData); return right(unit); }, ), @@ -98,29 +121,14 @@ class FFISingboxService with InfraLogger implements SingboxService { } @override - TaskEither create(String configPath) { - return TaskEither( - () => CombineWorker().execute( - () async { - final err = _box - .create(configPath.toNativeUtf8().cast()) - .cast() - .toDartString(); - if (err.isNotEmpty) { - return left(err); - } - return right(unit); - }, - ), - ); - } - - @override - TaskEither start() { + TaskEither start(String configPath) { return TaskEither( () => CombineWorker().execute( () { - final err = _box.start().cast().toDartString(); + final err = _box + .start(configPath.toNativeUtf8().cast()) + .cast() + .toDartString(); if (err.isNotEmpty) { return left(err); } @@ -146,7 +154,28 @@ class FFISingboxService with InfraLogger implements SingboxService { } @override - Stream watchStatus() { + TaskEither restart(String configPath) { + return TaskEither( + () => CombineWorker().execute( + () { + final err = _box + .restart(configPath.toNativeUtf8().cast()) + .cast() + .toDartString(); + if (err.isNotEmpty) { + return left(err); + } + return right(unit); + }, + ), + ); + } + + @override + Stream watchConnectionStatus() => _connectionStatus; + + @override + Stream watchStats() { if (_statusStream != null) return _statusStream!; final receiver = ReceivePort('status receiver'); final statusStream = receiver.asBroadcastStream( diff --git a/lib/services/singbox/mobile_singbox_service.dart b/lib/services/singbox/mobile_singbox_service.dart index 4d9b4f22..23708a8d 100644 --- a/lib/services/singbox/mobile_singbox_service.dart +++ b/lib/services/singbox/mobile_singbox_service.dart @@ -2,15 +2,36 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/connectivity/connection_status.dart'; import 'package:hiddify/domain/singbox/config_options.dart'; +import 'package:hiddify/services/singbox/shared.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; +import 'package:rxdart/rxdart.dart'; -class MobileSingboxService with InfraLogger implements SingboxService { - late final MethodChannel _methodChannel = - const MethodChannel("com.hiddify.app/method"); - late final EventChannel _logsChannel = - const EventChannel("com.hiddify.app/service.logs"); +class MobileSingboxService + with ServiceStatus, InfraLogger + implements SingboxService { + late final _methodChannel = const MethodChannel("com.hiddify.app/method"); + late final _statusChannel = + const EventChannel("com.hiddify.app/service.status"); + late final _alertsChannel = + const EventChannel("com.hiddify.app/service.alerts"); + late final _logsChannel = const EventChannel("com.hiddify.app/service.logs"); + + late final ValueStream _connectionStatus; + + @override + Future init() async { + loggy.debug("initializing"); + final status = + _statusChannel.receiveBroadcastStream().map(mapEventToStatus); + final alerts = + _alertsChannel.receiveBroadcastStream().map(mapEventToStatus); + _connectionStatus = + ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); + await _connectionStatus.first; + } @override TaskEither setup( @@ -48,25 +69,14 @@ class MobileSingboxService with InfraLogger implements SingboxService { } @override - TaskEither create(String configPath) { - return TaskEither( - () async { - loggy.debug("creating service for: $configPath"); - await _methodChannel.invokeMethod( - "set_active_config_path", - {"path": configPath}, - ); - return right(unit); - }, - ); - } - - @override - TaskEither start() { + TaskEither start(String configPath) { return TaskEither( () async { loggy.debug("starting"); - await _methodChannel.invokeMethod("start"); + await _methodChannel.invokeMethod( + "start", + {"path": configPath}, + ); return right(unit); }, ); @@ -83,6 +93,20 @@ class MobileSingboxService with InfraLogger implements SingboxService { ); } + @override + TaskEither restart(String configPath) { + return TaskEither( + () async { + loggy.debug("restarting"); + await _methodChannel.invokeMethod( + "restart", + {"path": configPath}, + ); + return right(unit); + }, + ); + } + @override Stream watchOutbounds() { const channel = EventChannel("com.hiddify.app/groups"); @@ -99,7 +123,10 @@ class MobileSingboxService with InfraLogger implements SingboxService { } @override - Stream watchStatus() { + Stream watchConnectionStatus() => _connectionStatus; + + @override + Stream watchStats() { // TODO: implement watchStatus return const Stream.empty(); } diff --git a/lib/services/singbox/shared.dart b/lib/services/singbox/shared.dart new file mode 100644 index 00000000..6c34b189 --- /dev/null +++ b/lib/services/singbox/shared.dart @@ -0,0 +1,46 @@ +import 'package:hiddify/domain/connectivity/connectivity.dart'; +import 'package:hiddify/domain/core_service_failure.dart'; + +mixin ServiceStatus { + ConnectionStatus mapEventToStatus(dynamic event) { + final status = event['status'] as String; + late ConnectionStatus connectionStatus; + switch (status) { + case "Stopped": + final failure = event["alert"] as String?; + final message = event["message"] as String?; + connectionStatus = ConnectionStatus.disconnected( + switch (failure) { + null => null, + "RequestVPNPermission" => MissingVpnPermission(message), + "RequestNotificationPermission" => + MissingNotificationPermission(message), + "EmptyConfiguration" || + "StartCommandServer" || + "CreateService" || + "StartService" => + CoreConnectionFailure(fromServiceAlert(failure, message)), + _ => const UnexpectedConnectionFailure(), + }, + ); + case "Starting": + connectionStatus = const Connecting(); + case "Started": + connectionStatus = const Connected(); + case "Stopping": + connectionStatus = const Disconnecting(); + } + return connectionStatus; + } + + CoreServiceFailure fromServiceAlert(String key, String? message) { + return switch (key) { + "EmptyConfiguration" => InvalidConfig(message), + "StartCommandServer" || + "CreateService" => + CoreServiceCreateFailure(message), + "StartService" => CoreServiceStartFailure(message), + _ => const CoreServiceOtherFailure(), + }; + } +} diff --git a/lib/services/singbox/singbox_service.dart b/lib/services/singbox/singbox_service.dart index 3ea8bdd5..6b8451fb 100644 --- a/lib/services/singbox/singbox_service.dart +++ b/lib/services/singbox/singbox_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/domain/connectivity/connectivity.dart'; import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/services/singbox/ffi_singbox_service.dart'; import 'package:hiddify/services/singbox/mobile_singbox_service.dart'; @@ -9,10 +10,14 @@ abstract interface class SingboxService { factory SingboxService() { if (Platform.isAndroid) { return MobileSingboxService(); + } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + return FFISingboxService(); } - return FFISingboxService(); + throw Exception("unsupported platform"); } + Future init(); + TaskEither setup( String baseDir, String workingDir, @@ -23,19 +28,21 @@ abstract interface class SingboxService { TaskEither changeConfigOptions(ConfigOptions options); - TaskEither create(String configPath); - - TaskEither start(); + TaskEither start(String configPath); TaskEither stop(); + TaskEither restart(String configPath); + Stream watchOutbounds(); TaskEither selectOutbound(String groupTag, String outboundTag); TaskEither urlTest(String groupTag); - Stream watchStatus(); + Stream watchConnectionStatus(); + + Stream watchStats(); Stream watchLogs(String path); } diff --git a/libcore b/libcore index 30cb888f..c8878156 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 30cb888fa7216581472a706cf5ef852b1086974a +Subproject commit c8878156767c0795324b1a7dc737e026b33fa10f