Add service restart
This commit is contained in:
@@ -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,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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user