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

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

View File

@@ -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(

View File

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

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 '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<void> init();
TaskEither<String, Unit> setup(
String baseDir,
String workingDir,
@@ -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);
}