Add service restart

This commit is contained in:
problematicconsumer
2023-09-10 20:25:04 +03:30
parent f303cf7e24
commit dedccdd772
18 changed files with 221 additions and 325 deletions

View File

@@ -90,8 +90,7 @@ Future<void> initAppServices(
_loggy.debug("initializing app services"); _loggy.debug("initializing app services");
await Future.wait( await Future.wait(
[ [
read(connectivityServiceProvider).init(), read(singboxServiceProvider).init(),
read(notificationServiceProvider).init(),
], ],
); );
_loggy.debug('initialized app services'); _loggy.debug('initialized app services');

View File

@@ -58,6 +58,5 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
ref.watch(singboxServiceProvider), ref.watch(singboxServiceProvider),
ref.watch(filesEditorServiceProvider), ref.watch(filesEditorServiceProvider),
ref.watch(clashApiProvider), ref.watch(clashApiProvider),
ref.watch(connectivityServiceProvider),
() => ref.read(configOptionsProvider), () => ref.read(configOptionsProvider),
); );

View File

@@ -9,7 +9,6 @@ import 'package:hiddify/domain/constants.dart';
import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/domain/singbox/singbox.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/files_editor_service.dart';
import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
@@ -19,14 +18,12 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
this.singbox, this.singbox,
this.filesEditor, this.filesEditor,
this.clash, this.clash,
this.connectivity,
this.configOptions, this.configOptions,
); );
final SingboxService singbox; final SingboxService singbox;
final FilesEditorService filesEditor; final FilesEditorService filesEditor;
final ClashApi clash; final ClashApi clash;
final ConnectivityService connectivity;
final ConfigOptions Function() configOptions; final ConfigOptions Function() configOptions;
bool _initialized = false; bool _initialized = false;
@@ -89,16 +86,14 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
} }
@override @override
TaskEither<CoreServiceFailure, Unit> changeConfig(String fileName) { TaskEither<CoreServiceFailure, Unit> start(String fileName) {
return exceptionHandler( return exceptionHandler(
() { () {
final configPath = filesEditor.configPath(fileName); final configPath = filesEditor.configPath(fileName);
loggy.debug("changing config to: $configPath");
return setup() return setup()
.andThen(() => changeConfigOptions(configOptions())) .andThen(() => changeConfigOptions(configOptions()))
.andThen( .andThen(
() => () => singbox.start(configPath).mapLeft(CoreServiceFailure.start),
singbox.create(configPath).mapLeft(CoreServiceFailure.create),
) )
.run(); .run();
}, },
@@ -107,17 +102,25 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
} }
@override @override
TaskEither<CoreServiceFailure, Unit> start() { TaskEither<CoreServiceFailure, Unit> stop() {
return exceptionHandler( return exceptionHandler(
() => singbox.start().mapLeft(CoreServiceFailure.start).run(), () => singbox.stop().mapLeft(CoreServiceFailure.other).run(),
CoreServiceFailure.unexpected, CoreServiceFailure.unexpected,
); );
} }
@override @override
TaskEither<CoreServiceFailure, Unit> stop() { TaskEither<CoreServiceFailure, Unit> restart(String fileName) {
return exceptionHandler( 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, CoreServiceFailure.unexpected,
); );
} }
@@ -160,7 +163,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
@override @override
Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus() { Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus() {
return singbox.watchStatus().map((event) { return singbox.watchStats().map((event) {
final json = jsonDecode(event); final json = jsonDecode(event);
return CoreStatus.fromJson(json as Map<String, dynamic>); return CoreStatus.fromJson(json as Map<String, dynamic>);
}).handleExceptions( }).handleExceptions(
@@ -239,29 +242,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
); );
} }
@override
TaskEither<CoreServiceFailure, Unit> connect() {
return exceptionHandler(
() async {
await connectivity.connect();
return right(unit);
},
CoreServiceFailure.unexpected,
);
}
@override
TaskEither<CoreServiceFailure, Unit> disconnect() {
return exceptionHandler(
() async {
await connectivity.disconnect();
return right(unit);
},
CoreServiceFailure.unexpected,
);
}
@override @override
Stream<ConnectionStatus> watchConnectionStatus() => Stream<ConnectionStatus> watchConnectionStatus() =>
connectivity.watchConnectionStatus(); singbox.watchConnectionStatus();
} }

View File

@@ -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<CoreServiceFailure, Unit> connect();
TaskEither<CoreServiceFailure, Unit> disconnect();
Stream<ConnectionStatus> watchConnectionStatus();
}

View File

@@ -1,6 +1,4 @@
import 'package:hiddify/domain/clash/clash.dart'; import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/domain/singbox/singbox.dart';
abstract interface class CoreFacade abstract interface class CoreFacade implements SingboxFacade, ClashFacade {}
implements SingboxFacade, ClashFacade, ConnectionFacade {}

View File

@@ -1,4 +1,5 @@
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/core_service_failure.dart'; import 'package:hiddify/domain/core_service_failure.dart';
import 'package:hiddify/domain/singbox/config_options.dart'; import 'package:hiddify/domain/singbox/config_options.dart';
import 'package:hiddify/domain/singbox/core_status.dart'; import 'package:hiddify/domain/singbox/core_status.dart';
@@ -13,12 +14,12 @@ abstract interface class SingboxFacade {
ConfigOptions options, ConfigOptions options,
); );
TaskEither<CoreServiceFailure, Unit> changeConfig(String fileName); TaskEither<CoreServiceFailure, Unit> start(String fileName);
TaskEither<CoreServiceFailure, Unit> start();
TaskEither<CoreServiceFailure, Unit> stop(); TaskEither<CoreServiceFailure, Unit> stop();
TaskEither<CoreServiceFailure, Unit> restart(String fileName);
Stream<Either<CoreServiceFailure, List<OutboundGroup>>> watchOutbounds(); Stream<Either<CoreServiceFailure, List<OutboundGroup>>> watchOutbounds();
TaskEither<CoreServiceFailure, Unit> selectOutbound( TaskEither<CoreServiceFailure, Unit> selectOutbound(
@@ -28,6 +29,8 @@ abstract interface class SingboxFacade {
TaskEither<CoreServiceFailure, Unit> urlTest(String groupTag); TaskEither<CoreServiceFailure, Unit> urlTest(String groupTag);
Stream<ConnectionStatus> watchConnectionStatus();
Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus(); Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus();
Stream<Either<CoreServiceFailure, String>> watchLogs(); Stream<Either<CoreServiceFailure, String>> watchLogs();

View File

@@ -17,15 +17,14 @@ class ConnectivityController extends _$ConnectivityController with AppLogger {
if (previous == null) return; if (previous == null) return;
final shouldReconnect = previous != next; final shouldReconnect = previous != next;
if (shouldReconnect) { if (shouldReconnect) {
loggy.debug("active profile modified, reconnect"); await reconnect(next?.id);
await reconnect();
} }
}, },
); );
return _connectivity.watchConnectionStatus(); return _core.watchConnectionStatus();
} }
CoreFacade get _connectivity => ref.watch(coreFacadeProvider); CoreFacade get _core => ref.watch(coreFacadeProvider);
Future<void> toggleConnection() async { Future<void> toggleConnection() async {
if (state case AsyncError()) { if (state case AsyncError()) {
@@ -42,13 +41,16 @@ class ConnectivityController extends _$ConnectivityController with AppLogger {
} }
} }
Future<void> reconnect() async { Future<void> reconnect(String? profileId) async {
if (state case AsyncData(:final value)) { if (state case AsyncData(:final value) when value == const Connected()) {
if (value case Connected()) { if (profileId == null) {
loggy.debug("reconnecting"); return _disconnect();
await _disconnect();
await _connect();
} }
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<void> _connect() async { Future<void> _connect() async {
final activeProfile = await ref.read(activeProfileProvider.future); final activeProfile = await ref.read(activeProfileProvider.future);
await _connectivity await _core.start(activeProfile!.id).mapLeft((l) {
.changeConfig(activeProfile!.id)
.andThen(_connectivity.connect)
.mapLeft((l) {
loggy.warning("error connecting: $l"); loggy.warning("error connecting: $l");
state = AsyncError(l, StackTrace.current); state = AsyncError(l, StackTrace.current);
}).run(); }).run();
} }
Future<void> _disconnect() async { Future<void> _disconnect() async {
await _connectivity.disconnect().mapLeft((l) { await _core.stop().mapLeft((l) {
loggy.warning("error disconnecting: $l"); loggy.warning("error disconnecting: $l");
state = AsyncError(l, StackTrace.current); state = AsyncError(l, StackTrace.current);
}).run(); }).run();

View File

@@ -875,21 +875,23 @@ class SingboxNativeLibrary {
ffi.Pointer<ffi.Char> baseDir, ffi.Pointer<ffi.Char> baseDir,
ffi.Pointer<ffi.Char> workingDir, ffi.Pointer<ffi.Char> workingDir,
ffi.Pointer<ffi.Char> tempDir, ffi.Pointer<ffi.Char> tempDir,
int statusPort,
) { ) {
return _setup( return _setup(
baseDir, baseDir,
workingDir, workingDir,
tempDir, tempDir,
statusPort,
); );
} }
late final _setupPtr = _lookup< late final _setupPtr = _lookup<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>>('setup'); ffi.Pointer<ffi.Char>, ffi.LongLong)>>('setup');
late final _setup = _setupPtr.asFunction< late final _setup = _setupPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>)>(); ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> parse( ffi.Pointer<ffi.Char> parse(
ffi.Pointer<ffi.Char> path, ffi.Pointer<ffi.Char> path,
@@ -920,28 +922,20 @@ class SingboxNativeLibrary {
late final _changeConfigOptions = _changeConfigOptionsPtr late final _changeConfigOptions = _changeConfigOptionsPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>(); .asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> create( ffi.Pointer<ffi.Char> start(
ffi.Pointer<ffi.Char> configPath, ffi.Pointer<ffi.Char> configPath,
) { ) {
return _create( return _start(
configPath, configPath,
); );
} }
late final _createPtr = _lookup< late final _startPtr = _lookup<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('create'); ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('start');
late final _create = _createPtr late final _start = _startPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>(); .asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> start() {
return _start();
}
late final _startPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('start');
late final _start = _startPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> stop() { ffi.Pointer<ffi.Char> stop() {
return _stop(); return _stop();
} }
@@ -950,6 +944,20 @@ class SingboxNativeLibrary {
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('stop'); _lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('stop');
late final _stop = _stopPtr.asFunction<ffi.Pointer<ffi.Char> Function()>(); late final _stop = _stopPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> restart(
ffi.Pointer<ffi.Char> configPath,
) {
return _restart(
configPath,
);
}
late final _restartPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('restart');
late final _restart = _restartPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> startCommandClient( ffi.Pointer<ffi.Char> startCommandClient(
int command, int command,
int port, int port,

View File

@@ -1,3 +0,0 @@
export 'connectivity_service.dart';
export 'desktop_connectivity_service.dart';
export 'mobile_connectivity_service.dart';

View File

@@ -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<void> init();
Stream<ConnectionStatus> watchConnectionStatus();
Future<void> connect();
Future<void> disconnect();
}

View File

@@ -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> _connectionStatus;
@override
Future<void> init() async {
loggy.debug("initializing");
_connectionStatus =
BehaviorSubject.seeded(const ConnectionStatus.disconnected());
}
@override
Stream<ConnectionStatus> watchConnectionStatus() => _connectionStatus;
@override
Future<void> 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<void> 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();
}
}

View File

@@ -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> _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<void> 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<ConnectionStatus> watchConnectionStatus() => _connectionStatus;
@override
Future<void> connect() async {
loggy.debug("connecting");
await notifications.grantPermission();
await singbox.start().getOrElse((l) => throw l).run();
}
@override
Future<void> disconnect() async {
loggy.debug("disconnecting");
await singbox.stop().getOrElse((l) => throw l).run();
}
}

View File

@@ -1,6 +1,4 @@
import 'package:hiddify/services/connectivity/connectivity.dart';
import 'package:hiddify/services/files_editor_service.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/platform_settings.dart';
import 'package:hiddify/services/runtime_details_service.dart'; import 'package:hiddify/services/runtime_details_service.dart';
import 'package:hiddify/services/singbox/singbox_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'; part 'service_providers.g.dart';
@Riverpod(keepAlive: true)
NotificationService notificationService(NotificationServiceRef ref) =>
NotificationService();
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
FilesEditorService filesEditorService(FilesEditorServiceRef ref) => FilesEditorService filesEditorService(FilesEditorServiceRef ref) =>
FilesEditorService(); FilesEditorService();
@@ -23,13 +17,6 @@ RuntimeDetailsService runtimeDetailsService(RuntimeDetailsServiceRef ref) =>
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); SingboxService singboxService(SingboxServiceRef ref) => SingboxService();
@Riverpod(keepAlive: true)
ConnectivityService connectivityService(ConnectivityServiceRef ref) =>
ConnectivityService(
ref.watch(singboxServiceProvider),
ref.watch(notificationServiceProvider),
);
@riverpod @riverpod
PlatformSettings platformSettings(PlatformSettingsRef ref) => PlatformSettings platformSettings(PlatformSettingsRef ref) =>
PlatformSettings(); PlatformSettings();

View File

@@ -7,18 +7,25 @@ import 'dart:isolate';
import 'package:combine/combine.dart'; import 'package:combine/combine.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/singbox/config_options.dart'; import 'package:hiddify/domain/singbox/config_options.dart';
import 'package:hiddify/gen/singbox_generated_bindings.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/services/singbox/singbox_service.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:loggy/loggy.dart'; import 'package:loggy/loggy.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:rxdart/rxdart.dart';
final _logger = Loggy('FFISingboxService'); final _logger = Loggy('FFISingboxService');
class FFISingboxService with InfraLogger implements SingboxService { class FFISingboxService
with ServiceStatus, InfraLogger
implements SingboxService {
static final SingboxNativeLibrary _box = _gen(); static final SingboxNativeLibrary _box = _gen();
late final ValueStream<ConnectionStatus> _connectionStatus;
late final ReceivePort _connectionStatusReceiver;
Stream<String>? _statusStream; Stream<String>? _statusStream;
Stream<String>? _groupsStream; Stream<String>? _groupsStream;
@@ -39,21 +46,37 @@ class FFISingboxService with InfraLogger implements SingboxService {
return SingboxNativeLibrary(lib); return SingboxNativeLibrary(lib);
} }
@override
Future<void> init() async {
loggy.debug("initializing");
_connectionStatusReceiver = ReceivePort('service status receiver');
final source = _connectionStatusReceiver
.asBroadcastStream()
.map((event) => jsonDecode(event as String) as Map<String, dynamic>)
.map(mapEventToStatus);
_connectionStatus = ValueConnectableStream.seeded(
source,
const ConnectionStatus.disconnected(),
).autoConnect();
}
@override @override
TaskEither<String, Unit> setup( TaskEither<String, Unit> setup(
String baseDir, String baseDir,
String workingDir, String workingDir,
String tempDir, String tempDir,
) { ) {
final port = _connectionStatusReceiver.sendPort.nativePort;
return TaskEither( return TaskEither(
() => CombineWorker().execute( () => CombineWorker().execute(
() { () {
_box.setupOnce(NativeApi.initializeApiDLData);
_box.setup( _box.setup(
baseDir.toNativeUtf8().cast(), baseDir.toNativeUtf8().cast(),
workingDir.toNativeUtf8().cast(), workingDir.toNativeUtf8().cast(),
tempDir.toNativeUtf8().cast(), tempDir.toNativeUtf8().cast(),
port,
); );
_box.setupOnce(NativeApi.initializeApiDLData);
return right(unit); return right(unit);
}, },
), ),
@@ -98,29 +121,14 @@ class FFISingboxService with InfraLogger implements SingboxService {
} }
@override @override
TaskEither<String, Unit> create(String configPath) { TaskEither<String, Unit> start(String configPath) {
return TaskEither(
() => CombineWorker().execute(
() async {
final err = _box
.create(configPath.toNativeUtf8().cast())
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
TaskEither<String, Unit> start() {
return TaskEither( return TaskEither(
() => CombineWorker().execute( () => CombineWorker().execute(
() { () {
final err = _box.start().cast<Utf8>().toDartString(); final err = _box
.start(configPath.toNativeUtf8().cast())
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) { if (err.isNotEmpty) {
return left(err); return left(err);
} }
@@ -146,7 +154,28 @@ class FFISingboxService with InfraLogger implements SingboxService {
} }
@override @override
Stream<String> watchStatus() { TaskEither<String, Unit> restart(String configPath) {
return TaskEither(
() => CombineWorker().execute(
() {
final err = _box
.restart(configPath.toNativeUtf8().cast())
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err);
}
return right(unit);
},
),
);
}
@override
Stream<ConnectionStatus> watchConnectionStatus() => _connectionStatus;
@override
Stream<String> watchStats() {
if (_statusStream != null) return _statusStream!; if (_statusStream != null) return _statusStream!;
final receiver = ReceivePort('status receiver'); final receiver = ReceivePort('status receiver');
final statusStream = receiver.asBroadcastStream( final statusStream = receiver.asBroadcastStream(

View File

@@ -2,15 +2,36 @@ import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:fpdart/fpdart.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/domain/singbox/config_options.dart';
import 'package:hiddify/services/singbox/shared.dart';
import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:hiddify/services/singbox/singbox_service.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:rxdart/rxdart.dart';
class MobileSingboxService with InfraLogger implements SingboxService { class MobileSingboxService
late final MethodChannel _methodChannel = with ServiceStatus, InfraLogger
const MethodChannel("com.hiddify.app/method"); implements SingboxService {
late final EventChannel _logsChannel = late final _methodChannel = const MethodChannel("com.hiddify.app/method");
const EventChannel("com.hiddify.app/service.logs"); 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> _connectionStatus;
@override
Future<void> 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 @override
TaskEither<String, Unit> setup( TaskEither<String, Unit> setup(
@@ -48,25 +69,14 @@ class MobileSingboxService with InfraLogger implements SingboxService {
} }
@override @override
TaskEither<String, Unit> create(String configPath) { TaskEither<String, Unit> start(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<String, Unit> start() {
return TaskEither( return TaskEither(
() async { () async {
loggy.debug("starting"); loggy.debug("starting");
await _methodChannel.invokeMethod("start"); await _methodChannel.invokeMethod(
"start",
{"path": configPath},
);
return right(unit); return right(unit);
}, },
); );
@@ -83,6 +93,20 @@ class MobileSingboxService with InfraLogger implements SingboxService {
); );
} }
@override
TaskEither<String, Unit> restart(String configPath) {
return TaskEither(
() async {
loggy.debug("restarting");
await _methodChannel.invokeMethod(
"restart",
{"path": configPath},
);
return right(unit);
},
);
}
@override @override
Stream<String> watchOutbounds() { Stream<String> watchOutbounds() {
const channel = EventChannel("com.hiddify.app/groups"); const channel = EventChannel("com.hiddify.app/groups");
@@ -99,7 +123,10 @@ class MobileSingboxService with InfraLogger implements SingboxService {
} }
@override @override
Stream<String> watchStatus() { Stream<ConnectionStatus> watchConnectionStatus() => _connectionStatus;
@override
Stream<String> watchStats() {
// TODO: implement watchStatus // TODO: implement watchStatus
return const Stream.empty(); return const Stream.empty();
} }

View File

@@ -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(),
};
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/connectivity/connectivity.dart';
import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/domain/singbox/singbox.dart';
import 'package:hiddify/services/singbox/ffi_singbox_service.dart'; import 'package:hiddify/services/singbox/ffi_singbox_service.dart';
import 'package:hiddify/services/singbox/mobile_singbox_service.dart'; import 'package:hiddify/services/singbox/mobile_singbox_service.dart';
@@ -9,10 +10,14 @@ abstract interface class SingboxService {
factory SingboxService() { factory SingboxService() {
if (Platform.isAndroid) { if (Platform.isAndroid) {
return MobileSingboxService(); return MobileSingboxService();
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
return FFISingboxService();
} }
return FFISingboxService(); throw Exception("unsupported platform");
} }
Future<void> init();
TaskEither<String, Unit> setup( TaskEither<String, Unit> setup(
String baseDir, String baseDir,
String workingDir, String workingDir,
@@ -23,19 +28,21 @@ abstract interface class SingboxService {
TaskEither<String, Unit> changeConfigOptions(ConfigOptions options); TaskEither<String, Unit> changeConfigOptions(ConfigOptions options);
TaskEither<String, Unit> create(String configPath); TaskEither<String, Unit> start(String configPath);
TaskEither<String, Unit> start();
TaskEither<String, Unit> stop(); TaskEither<String, Unit> stop();
TaskEither<String, Unit> restart(String configPath);
Stream<String> watchOutbounds(); Stream<String> watchOutbounds();
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag); TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag);
TaskEither<String, Unit> urlTest(String groupTag); TaskEither<String, Unit> urlTest(String groupTag);
Stream<String> watchStatus(); Stream<ConnectionStatus> watchConnectionStatus();
Stream<String> watchStats();
Stream<String> watchLogs(String path); Stream<String> watchLogs(String path);
} }

Submodule libcore updated: 30cb888fa7...c887815676