Add service restart
This commit is contained in:
@@ -90,8 +90,7 @@ Future<void> initAppServices(
|
||||
_loggy.debug("initializing app services");
|
||||
await Future.wait(
|
||||
[
|
||||
read(connectivityServiceProvider).init(),
|
||||
read(notificationServiceProvider).init(),
|
||||
read(singboxServiceProvider).init(),
|
||||
],
|
||||
);
|
||||
_loggy.debug('initialized app services');
|
||||
|
||||
@@ -58,6 +58,5 @@ CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
|
||||
ref.watch(singboxServiceProvider),
|
||||
ref.watch(filesEditorServiceProvider),
|
||||
ref.watch(clashApiProvider),
|
||||
ref.watch(connectivityServiceProvider),
|
||||
() => ref.read(configOptionsProvider),
|
||||
);
|
||||
|
||||
@@ -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<CoreServiceFailure, Unit> changeConfig(String fileName) {
|
||||
TaskEither<CoreServiceFailure, Unit> 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<CoreServiceFailure, Unit> start() {
|
||||
TaskEither<CoreServiceFailure, Unit> stop() {
|
||||
return exceptionHandler(
|
||||
() => singbox.start().mapLeft(CoreServiceFailure.start).run(),
|
||||
() => singbox.stop().mapLeft(CoreServiceFailure.other).run(),
|
||||
CoreServiceFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<CoreServiceFailure, Unit> stop() {
|
||||
TaskEither<CoreServiceFailure, Unit> 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<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus() {
|
||||
return singbox.watchStatus().map((event) {
|
||||
return singbox.watchStats().map((event) {
|
||||
final json = jsonDecode(event);
|
||||
return CoreStatus.fromJson(json as Map<String, dynamic>);
|
||||
}).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
|
||||
Stream<ConnectionStatus> watchConnectionStatus() =>
|
||||
connectivity.watchConnectionStatus();
|
||||
singbox.watchConnectionStatus();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<CoreServiceFailure, Unit> changeConfig(String fileName);
|
||||
|
||||
TaskEither<CoreServiceFailure, Unit> start();
|
||||
TaskEither<CoreServiceFailure, Unit> start(String fileName);
|
||||
|
||||
TaskEither<CoreServiceFailure, Unit> stop();
|
||||
|
||||
TaskEither<CoreServiceFailure, Unit> restart(String fileName);
|
||||
|
||||
Stream<Either<CoreServiceFailure, List<OutboundGroup>>> watchOutbounds();
|
||||
|
||||
TaskEither<CoreServiceFailure, Unit> selectOutbound(
|
||||
@@ -28,6 +29,8 @@ abstract interface class SingboxFacade {
|
||||
|
||||
TaskEither<CoreServiceFailure, Unit> urlTest(String groupTag);
|
||||
|
||||
Stream<ConnectionStatus> watchConnectionStatus();
|
||||
|
||||
Stream<Either<CoreServiceFailure, CoreStatus>> watchCoreStatus();
|
||||
|
||||
Stream<Either<CoreServiceFailure, String>> watchLogs();
|
||||
|
||||
@@ -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<void> toggleConnection() async {
|
||||
if (state case AsyncError()) {
|
||||
@@ -42,13 +41,16 @@ class ConnectivityController extends _$ConnectivityController with AppLogger {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reconnect() async {
|
||||
if (state case AsyncData(:final value)) {
|
||||
if (value case Connected()) {
|
||||
loggy.debug("reconnecting");
|
||||
await _disconnect();
|
||||
await _connect();
|
||||
Future<void> 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<void> _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<void> _disconnect() async {
|
||||
await _connectivity.disconnect().mapLeft((l) {
|
||||
await _core.stop().mapLeft((l) {
|
||||
loggy.warning("error disconnecting: $l");
|
||||
state = AsyncError(l, StackTrace.current);
|
||||
}).run();
|
||||
|
||||
@@ -875,21 +875,23 @@ class SingboxNativeLibrary {
|
||||
ffi.Pointer<ffi.Char> baseDir,
|
||||
ffi.Pointer<ffi.Char> workingDir,
|
||||
ffi.Pointer<ffi.Char> tempDir,
|
||||
int statusPort,
|
||||
) {
|
||||
return _setup(
|
||||
baseDir,
|
||||
workingDir,
|
||||
tempDir,
|
||||
statusPort,
|
||||
);
|
||||
}
|
||||
|
||||
late final _setupPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
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<
|
||||
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> path,
|
||||
@@ -920,28 +922,20 @@ class SingboxNativeLibrary {
|
||||
late final _changeConfigOptions = _changeConfigOptionsPtr
|
||||
.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,
|
||||
) {
|
||||
return _create(
|
||||
return _start(
|
||||
configPath,
|
||||
);
|
||||
}
|
||||
|
||||
late final _createPtr = _lookup<
|
||||
late final _startPtr = _lookup<
|
||||
ffi.NativeFunction<
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('create');
|
||||
late final _create = _createPtr
|
||||
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('start');
|
||||
late final _start = _startPtr
|
||||
.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() {
|
||||
return _stop();
|
||||
}
|
||||
@@ -950,6 +944,20 @@ class SingboxNativeLibrary {
|
||||
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>('stop');
|
||||
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(
|
||||
int command,
|
||||
int port,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export 'connectivity_service.dart';
|
||||
export 'desktop_connectivity_service.dart';
|
||||
export 'mobile_connectivity_service.dart';
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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> _connectionStatus;
|
||||
late final ReceivePort _connectionStatusReceiver;
|
||||
Stream<String>? _statusStream;
|
||||
Stream<String>? _groupsStream;
|
||||
|
||||
@@ -39,21 +46,37 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
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
|
||||
TaskEither<String, Unit> 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<String, Unit> create(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() {
|
||||
TaskEither<String, Unit> start(String configPath) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box.start().cast<Utf8>().toDartString();
|
||||
final err = _box
|
||||
.start(configPath.toNativeUtf8().cast())
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
@@ -146,7 +154,28 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
}
|
||||
|
||||
@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!;
|
||||
final receiver = ReceivePort('status receiver');
|
||||
final statusStream = receiver.asBroadcastStream(
|
||||
|
||||
@@ -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> _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
|
||||
TaskEither<String, Unit> setup(
|
||||
@@ -48,25 +69,14 @@ class MobileSingboxService with InfraLogger implements SingboxService {
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> 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<String, Unit> start() {
|
||||
TaskEither<String, Unit> 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<String, Unit> restart(String configPath) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
loggy.debug("restarting");
|
||||
await _methodChannel.invokeMethod(
|
||||
"restart",
|
||||
{"path": configPath},
|
||||
);
|
||||
return right(unit);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> watchOutbounds() {
|
||||
const channel = EventChannel("com.hiddify.app/groups");
|
||||
@@ -99,7 +123,10 @@ class MobileSingboxService with InfraLogger implements SingboxService {
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> watchStatus() {
|
||||
Stream<ConnectionStatus> watchConnectionStatus() => _connectionStatus;
|
||||
|
||||
@override
|
||||
Stream<String> watchStats() {
|
||||
// TODO: implement watchStatus
|
||||
return const Stream.empty();
|
||||
}
|
||||
|
||||
46
lib/services/singbox/shared.dart
Normal file
46
lib/services/singbox/shared.dart
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,9 +10,13 @@ abstract interface class SingboxService {
|
||||
factory SingboxService() {
|
||||
if (Platform.isAndroid) {
|
||||
return MobileSingboxService();
|
||||
}
|
||||
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
|
||||
return FFISingboxService();
|
||||
}
|
||||
throw Exception("unsupported platform");
|
||||
}
|
||||
|
||||
Future<void> init();
|
||||
|
||||
TaskEither<String, Unit> setup(
|
||||
String baseDir,
|
||||
@@ -23,19 +28,21 @@ abstract interface class SingboxService {
|
||||
|
||||
TaskEither<String, Unit> changeConfigOptions(ConfigOptions options);
|
||||
|
||||
TaskEither<String, Unit> create(String configPath);
|
||||
|
||||
TaskEither<String, Unit> start();
|
||||
TaskEither<String, Unit> start(String configPath);
|
||||
|
||||
TaskEither<String, Unit> stop();
|
||||
|
||||
TaskEither<String, Unit> restart(String configPath);
|
||||
|
||||
Stream<String> watchOutbounds();
|
||||
|
||||
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag);
|
||||
|
||||
TaskEither<String, Unit> urlTest(String groupTag);
|
||||
|
||||
Stream<String> watchStatus();
|
||||
Stream<ConnectionStatus> watchConnectionStatus();
|
||||
|
||||
Stream<String> watchStats();
|
||||
|
||||
Stream<String> watchLogs(String path);
|
||||
}
|
||||
|
||||
2
libcore
2
libcore
Submodule libcore updated: 30cb888fa7...c887815676
Reference in New Issue
Block a user