mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 14:23:19 +08:00
feat: 重构Native sftp&docker实例共享逻辑
This commit is contained in:
@@ -238,6 +238,11 @@ class DockerSessionManager extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void disconnectAll() {
|
||||||
|
_disconnectAll();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void _disconnect(String hostId) {
|
void _disconnect(String hostId) {
|
||||||
final session = _sessions.remove(hostId);
|
final session = _sessions.remove(hostId);
|
||||||
if (session == null) return;
|
if (session == null) return;
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ class DockerPanel extends ConsumerStatefulWidget {
|
|||||||
this.initialHostId,
|
this.initialHostId,
|
||||||
this.lockToHost = false,
|
this.lockToHost = false,
|
||||||
this.allowDisconnect = true,
|
this.allowDisconnect = true,
|
||||||
|
this.sessionManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool showHeader;
|
final bool showHeader;
|
||||||
final String? initialHostId;
|
final String? initialHostId;
|
||||||
final bool lockToHost;
|
final bool lockToHost;
|
||||||
final bool allowDisconnect;
|
final bool allowDisconnect;
|
||||||
|
final DockerSessionManager? sessionManager;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<DockerPanel> createState() => _DockerPanelState();
|
ConsumerState<DockerPanel> createState() => _DockerPanelState();
|
||||||
@@ -54,13 +56,16 @@ class _DockerPanelState extends ConsumerState<DockerPanel> {
|
|||||||
bool _connecting = false;
|
bool _connecting = false;
|
||||||
String? _autoConnectAttemptedHostId;
|
String? _autoConnectAttemptedHostId;
|
||||||
|
|
||||||
|
DockerSessionManager get _manager =>
|
||||||
|
widget.sessionManager ?? ref.read(dockerSessionManagerProvider);
|
||||||
|
|
||||||
Future<void> _refreshHosts() => runRefreshWithFeedback(
|
Future<void> _refreshHosts() => runRefreshWithFeedback(
|
||||||
context,
|
context,
|
||||||
() => ref.read(hostListProvider.notifier).refresh(throwOnError: true),
|
() => ref.read(hostListProvider.notifier).refresh(throwOnError: true),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _openServerPicker() async {
|
Future<void> _openServerPicker() async {
|
||||||
final manager = ref.read(dockerSessionManagerProvider);
|
final manager = _manager;
|
||||||
final selected = await showModalBottomSheet<ServerModel>(
|
final selected = await showModalBottomSheet<ServerModel>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -83,7 +88,7 @@ class _DockerPanelState extends ConsumerState<DockerPanel> {
|
|||||||
|
|
||||||
Future<void> _connectOrActivate(ServerModel server) async {
|
Future<void> _connectOrActivate(ServerModel server) async {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final manager = ref.read(dockerSessionManagerProvider);
|
final manager = _manager;
|
||||||
if (manager.isConnected(server.id)) {
|
if (manager.isConnected(server.id)) {
|
||||||
manager.activate(server.id);
|
manager.activate(server.id);
|
||||||
return;
|
return;
|
||||||
@@ -132,7 +137,8 @@ class _DockerPanelState extends ConsumerState<DockerPanel> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final hostsAsync = ref.watch(hostListProvider);
|
final hostsAsync = ref.watch(hostListProvider);
|
||||||
final manager = ref.watch(dockerSessionManagerProvider);
|
final DockerSessionManager manager =
|
||||||
|
widget.sessionManager ?? ref.watch(dockerSessionManagerProvider);
|
||||||
final isPlusActive = ref.watch(isPlusActiveProvider);
|
final isPlusActive = ref.watch(isPlusActiveProvider);
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
@@ -276,15 +282,18 @@ class _DockerPanelState extends ConsumerState<DockerPanel> {
|
|||||||
String? lockedHostId,
|
String? lockedHostId,
|
||||||
DockerContainer container,
|
DockerContainer container,
|
||||||
) async {
|
) async {
|
||||||
final manager = ref.read(dockerSessionManagerProvider);
|
final manager = _manager;
|
||||||
manager.getLogs(container, hostId: lockedHostId);
|
manager.getLogs(container, hostId: lockedHostId);
|
||||||
await showModalBottomSheet<void>(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
barrierColor: Colors.black.withValues(alpha: 0.34),
|
barrierColor: Colors.black.withValues(alpha: 0.34),
|
||||||
builder: (_) =>
|
builder: (_) => _DockerLogsSheet(
|
||||||
_DockerLogsSheet(hostId: lockedHostId, container: container),
|
hostId: lockedHostId,
|
||||||
|
container: container,
|
||||||
|
manager: manager,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1380,10 +1389,15 @@ class _DockerCircleAction extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DockerLogsSheet extends ConsumerStatefulWidget {
|
class _DockerLogsSheet extends ConsumerStatefulWidget {
|
||||||
const _DockerLogsSheet({required this.hostId, required this.container});
|
const _DockerLogsSheet({
|
||||||
|
required this.hostId,
|
||||||
|
required this.container,
|
||||||
|
required this.manager,
|
||||||
|
});
|
||||||
|
|
||||||
final String? hostId;
|
final String? hostId;
|
||||||
final DockerContainer container;
|
final DockerContainer container;
|
||||||
|
final DockerSessionManager manager;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_DockerLogsSheet> createState() => _DockerLogsSheetState();
|
ConsumerState<_DockerLogsSheet> createState() => _DockerLogsSheetState();
|
||||||
@@ -1398,7 +1412,7 @@ class _DockerLogsSheetState extends ConsumerState<_DockerLogsSheet> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_manager = ref.read(dockerSessionManagerProvider);
|
_manager = widget.manager;
|
||||||
_manager.addListener(_onLogsChanged);
|
_manager.addListener(_onLogsChanged);
|
||||||
_autoRefreshTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
_autoRefreshTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||||
final session = widget.hostId == null
|
final session = widget.hostId == null
|
||||||
|
|||||||
@@ -302,11 +302,32 @@ class SftpSessionManager extends ChangeNotifier {
|
|||||||
Future<void> disconnectActive() async {
|
Future<void> disconnectActive() async {
|
||||||
final hostId = _activeHostId;
|
final hostId = _activeHostId;
|
||||||
if (hostId == null) return;
|
if (hostId == null) return;
|
||||||
|
await disconnect(hostId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnect(String hostId) async {
|
||||||
final connection = _connections.remove(hostId);
|
final connection = _connections.remove(hostId);
|
||||||
final state = _states.remove(hostId);
|
final state = _states.remove(hostId);
|
||||||
await connection?.close();
|
await connection?.close();
|
||||||
state?.dispose();
|
state?.dispose();
|
||||||
_activeHostId = _states.isEmpty ? null : _states.keys.first;
|
if (_activeHostId == hostId) {
|
||||||
|
_activeHostId = _states.isEmpty ? null : _states.keys.first;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> disconnectAll() async {
|
||||||
|
final connections = _connections.values.toList(growable: false);
|
||||||
|
final states = _states.values.toList(growable: false);
|
||||||
|
_connections.clear();
|
||||||
|
_states.clear();
|
||||||
|
_activeHostId = null;
|
||||||
|
for (final state in states) {
|
||||||
|
state.dispose();
|
||||||
|
}
|
||||||
|
for (final connection in connections) {
|
||||||
|
await connection.close();
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,10 +816,10 @@ class SftpSessionManager extends ChangeNotifier {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
for (final connection in _connections.values) {
|
for (final connection in _connections.values.toList(growable: false)) {
|
||||||
unawaited(connection.close());
|
unawaited(connection.close());
|
||||||
}
|
}
|
||||||
for (final state in _states.values) {
|
for (final state in _states.values.toList(growable: false)) {
|
||||||
state.dispose();
|
state.dispose();
|
||||||
}
|
}
|
||||||
_connections.clear();
|
_connections.clear();
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class SftpPanel extends ConsumerStatefulWidget {
|
|||||||
this.lockToHost = false,
|
this.lockToHost = false,
|
||||||
this.onExecCommand,
|
this.onExecCommand,
|
||||||
this.onCommandPanelClose,
|
this.onCommandPanelClose,
|
||||||
|
this.sessionManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool showHeader;
|
final bool showHeader;
|
||||||
@@ -54,6 +55,7 @@ class SftpPanel extends ConsumerStatefulWidget {
|
|||||||
final bool lockToHost;
|
final bool lockToHost;
|
||||||
final void Function(String command)? onExecCommand;
|
final void Function(String command)? onExecCommand;
|
||||||
final VoidCallback? onCommandPanelClose;
|
final VoidCallback? onCommandPanelClose;
|
||||||
|
final SftpSessionManager? sessionManager;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<SftpPanel> createState() => _SftpPanelState();
|
ConsumerState<SftpPanel> createState() => _SftpPanelState();
|
||||||
@@ -63,13 +65,16 @@ class _SftpPanelState extends ConsumerState<SftpPanel> {
|
|||||||
bool _connecting = false;
|
bool _connecting = false;
|
||||||
String? _autoConnectAttemptedHostId;
|
String? _autoConnectAttemptedHostId;
|
||||||
|
|
||||||
|
SftpSessionManager get _manager =>
|
||||||
|
widget.sessionManager ?? ref.read(sftpSessionManagerProvider);
|
||||||
|
|
||||||
Future<void> _refresh() => runRefreshWithFeedback(
|
Future<void> _refresh() => runRefreshWithFeedback(
|
||||||
context,
|
context,
|
||||||
() => ref.read(hostListProvider.notifier).refresh(throwOnError: true),
|
() => ref.read(hostListProvider.notifier).refresh(throwOnError: true),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> _openServerPicker() async {
|
Future<void> _openServerPicker() async {
|
||||||
final manager = ref.read(sftpSessionManagerProvider);
|
final manager = _manager;
|
||||||
final selected = await showModalBottomSheet<ServerModel>(
|
final selected = await showModalBottomSheet<ServerModel>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -92,7 +97,7 @@ class _SftpPanelState extends ConsumerState<SftpPanel> {
|
|||||||
|
|
||||||
Future<void> _connectOrActivate(ServerModel server) async {
|
Future<void> _connectOrActivate(ServerModel server) async {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final manager = ref.read(sftpSessionManagerProvider);
|
final manager = _manager;
|
||||||
if (manager.isConnected(server.id)) {
|
if (manager.isConnected(server.id)) {
|
||||||
manager.activate(server.id);
|
manager.activate(server.id);
|
||||||
return;
|
return;
|
||||||
@@ -143,7 +148,8 @@ class _SftpPanelState extends ConsumerState<SftpPanel> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final hostsAsync = ref.watch(hostListProvider);
|
final hostsAsync = ref.watch(hostListProvider);
|
||||||
final manager = ref.watch(sftpSessionManagerProvider);
|
final SftpSessionManager manager =
|
||||||
|
widget.sessionManager ?? ref.watch(sftpSessionManagerProvider);
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: manager,
|
animation: manager,
|
||||||
@@ -265,7 +271,7 @@ class _SftpPanelState extends ConsumerState<SftpPanel> {
|
|||||||
final hostId = widget.initialHostId;
|
final hostId = widget.initialHostId;
|
||||||
if (hostId == null || hostId.isEmpty) return;
|
if (hostId == null || hostId.isEmpty) return;
|
||||||
if (_autoConnectAttemptedHostId == hostId) return;
|
if (_autoConnectAttemptedHostId == hostId) return;
|
||||||
final manager = ref.read(sftpSessionManagerProvider);
|
final manager = _manager;
|
||||||
if (manager.activeHostId == hostId && manager.activeSession != null) {
|
if (manager.activeHostId == hostId && manager.activeSession != null) {
|
||||||
_autoConnectAttemptedHostId = hostId;
|
_autoConnectAttemptedHostId = hostId;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -266,6 +266,12 @@ class _TerminalBottomBarState extends ConsumerState<TerminalBottomBar> {
|
|||||||
|
|
||||||
Future<void> _showSftp(BuildContext context) async {
|
Future<void> _showSftp(BuildContext context) async {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
|
final hostId = ref
|
||||||
|
.read(terminalSessionManagerProvider)
|
||||||
|
.activeSession
|
||||||
|
?.config
|
||||||
|
.hostId;
|
||||||
|
if (hostId == null) return;
|
||||||
await showModalBottomSheet<void>(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -277,11 +283,10 @@ class _TerminalBottomBarState extends ConsumerState<TerminalBottomBar> {
|
|||||||
title: l.tr('terminal.menu.sftp'),
|
title: l.tr('terminal.menu.sftp'),
|
||||||
icon: Icons.folder_outlined,
|
icon: Icons.folder_outlined,
|
||||||
child: SftpPanel(
|
child: SftpPanel(
|
||||||
initialHostId: ref
|
initialHostId: hostId,
|
||||||
.read(terminalSessionManagerProvider)
|
sessionManager: ref
|
||||||
.activeSession
|
.read(terminalSessionResourcesProvider)
|
||||||
?.config
|
.sftpForHost(hostId),
|
||||||
.hostId,
|
|
||||||
allowDisconnect: false,
|
allowDisconnect: false,
|
||||||
lockToHost: true,
|
lockToHost: true,
|
||||||
onExecCommand: (command) {
|
onExecCommand: (command) {
|
||||||
@@ -304,6 +309,12 @@ class _TerminalBottomBarState extends ConsumerState<TerminalBottomBar> {
|
|||||||
await SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
|
await SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
|
final hostId = ref
|
||||||
|
.read(terminalSessionManagerProvider)
|
||||||
|
.activeSession
|
||||||
|
?.config
|
||||||
|
.hostId;
|
||||||
|
if (hostId == null) return;
|
||||||
await showModalBottomSheet<void>(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -315,11 +326,10 @@ class _TerminalBottomBarState extends ConsumerState<TerminalBottomBar> {
|
|||||||
title: l.tr('terminal.menu.docker'),
|
title: l.tr('terminal.menu.docker'),
|
||||||
iconWidget: const DockerIcon(),
|
iconWidget: const DockerIcon(),
|
||||||
child: DockerPanel(
|
child: DockerPanel(
|
||||||
initialHostId: ref
|
initialHostId: hostId,
|
||||||
.read(terminalSessionManagerProvider)
|
sessionManager: ref
|
||||||
.activeSession
|
.read(terminalSessionResourcesProvider)
|
||||||
?.config
|
.dockerForHost(hostId),
|
||||||
.hostId,
|
|
||||||
allowDisconnect: false,
|
allowDisconnect: false,
|
||||||
lockToHost: true,
|
lockToHost: true,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,20 +10,24 @@ import 'server_status_monitor_manager.dart';
|
|||||||
import 'terminal_session.dart';
|
import 'terminal_session.dart';
|
||||||
|
|
||||||
typedef ShouldAutoStartStatusMonitor = bool Function();
|
typedef ShouldAutoStartStatusMonitor = bool Function();
|
||||||
|
typedef OnLastSessionForHostClosed = Future<void> Function(String hostId);
|
||||||
|
|
||||||
class TerminalSessionManager extends ChangeNotifier {
|
class TerminalSessionManager extends ChangeNotifier {
|
||||||
TerminalSessionManager({
|
TerminalSessionManager({
|
||||||
Uuid? uuid,
|
Uuid? uuid,
|
||||||
ServerStatusMonitorManager? statusMonitorManager,
|
ServerStatusMonitorManager? statusMonitorManager,
|
||||||
ShouldAutoStartStatusMonitor? shouldAutoStartStatusMonitor,
|
ShouldAutoStartStatusMonitor? shouldAutoStartStatusMonitor,
|
||||||
|
OnLastSessionForHostClosed? onLastSessionForHostClosed,
|
||||||
}) : _uuid = uuid ?? const Uuid(),
|
}) : _uuid = uuid ?? const Uuid(),
|
||||||
_statusMonitorManager = statusMonitorManager,
|
_statusMonitorManager = statusMonitorManager,
|
||||||
_shouldAutoStartStatusMonitor =
|
_shouldAutoStartStatusMonitor =
|
||||||
shouldAutoStartStatusMonitor ?? (() => false);
|
shouldAutoStartStatusMonitor ?? (() => false),
|
||||||
|
_onLastSessionForHostClosed = onLastSessionForHostClosed;
|
||||||
|
|
||||||
final Uuid _uuid;
|
final Uuid _uuid;
|
||||||
final ServerStatusMonitorManager? _statusMonitorManager;
|
final ServerStatusMonitorManager? _statusMonitorManager;
|
||||||
final ShouldAutoStartStatusMonitor _shouldAutoStartStatusMonitor;
|
final ShouldAutoStartStatusMonitor _shouldAutoStartStatusMonitor;
|
||||||
|
final OnLastSessionForHostClosed? _onLastSessionForHostClosed;
|
||||||
final List<TerminalSession> _sessions = [];
|
final List<TerminalSession> _sessions = [];
|
||||||
String? _activeId;
|
String? _activeId;
|
||||||
|
|
||||||
@@ -101,6 +105,7 @@ class TerminalSessionManager extends ChangeNotifier {
|
|||||||
if (_activeId == id) {
|
if (_activeId == id) {
|
||||||
_activeId = _sessions.isEmpty ? null : _sessions.first.id;
|
_activeId = _sessions.isEmpty ? null : _sessions.first.id;
|
||||||
}
|
}
|
||||||
|
await _closeDependentSessionsIfLastForHost(session.config.hostId);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,15 +113,25 @@ class TerminalSessionManager extends ChangeNotifier {
|
|||||||
final copy = List<TerminalSession>.from(_sessions);
|
final copy = List<TerminalSession>.from(_sessions);
|
||||||
_sessions.clear();
|
_sessions.clear();
|
||||||
_activeId = null;
|
_activeId = null;
|
||||||
|
final hostIds = <String>{};
|
||||||
for (final session in copy) {
|
for (final session in copy) {
|
||||||
|
hostIds.add(session.config.hostId);
|
||||||
await _detachStatusMonitor(session);
|
await _detachStatusMonitor(session);
|
||||||
await session.controller.disconnect();
|
await session.controller.disconnect();
|
||||||
session.viewController.dispose();
|
session.viewController.dispose();
|
||||||
session.scrollController.dispose();
|
session.scrollController.dispose();
|
||||||
}
|
}
|
||||||
|
for (final hostId in hostIds) {
|
||||||
|
await _onLastSessionForHostClosed?.call(hostId);
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _closeDependentSessionsIfLastForHost(String hostId) async {
|
||||||
|
if (_sessions.any((session) => session.config.hostId == hostId)) return;
|
||||||
|
await _onLastSessionForHostClosed?.call(hostId);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _connect(TerminalSession session) async {
|
Future<void> _connect(TerminalSession session) async {
|
||||||
final controller = session.controller;
|
final controller = session.controller;
|
||||||
session.status = TerminalSessionStatus.connecting;
|
session.status = TerminalSessionStatus.connecting;
|
||||||
@@ -126,8 +141,7 @@ class TerminalSessionManager extends ChangeNotifier {
|
|||||||
await controller.connect();
|
await controller.connect();
|
||||||
// After await, verify this controller is still the active one — a
|
// After await, verify this controller is still the active one — a
|
||||||
// concurrent reconnect may have replaced it.
|
// concurrent reconnect may have replaced it.
|
||||||
if (!_sessions.contains(session) ||
|
if (!_sessions.contains(session) || session.controller != controller) {
|
||||||
session.controller != controller) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.status = TerminalSessionStatus.connected;
|
session.status = TerminalSessionStatus.connected;
|
||||||
@@ -141,8 +155,7 @@ class TerminalSessionManager extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!_sessions.contains(session) ||
|
if (!_sessions.contains(session) || session.controller != controller) {
|
||||||
session.controller != controller) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session.status = TerminalSessionStatus.error;
|
session.status = TerminalSessionStatus.error;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import '../docker/docker_session_manager.dart';
|
||||||
|
import '../shell/sftp_session_manager.dart';
|
||||||
|
|
||||||
|
/// Owns auxiliary connections opened from terminal pages. These connections
|
||||||
|
/// are deliberately separate from the managers used by the bottom-nav tabs.
|
||||||
|
class TerminalSessionResources {
|
||||||
|
final Map<String, SftpSessionManager> _sftpManagers = {};
|
||||||
|
final Map<String, DockerSessionManager> _dockerManagers = {};
|
||||||
|
|
||||||
|
SftpSessionManager sftpForHost(String hostId) =>
|
||||||
|
_sftpManagers.putIfAbsent(hostId, SftpSessionManager.new);
|
||||||
|
|
||||||
|
DockerSessionManager dockerForHost(String hostId) =>
|
||||||
|
_dockerManagers.putIfAbsent(hostId, DockerSessionManager.new);
|
||||||
|
|
||||||
|
Future<void> disposeHost(String hostId) async {
|
||||||
|
final sftpManager = _sftpManagers.remove(hostId);
|
||||||
|
await sftpManager?.disconnectAll();
|
||||||
|
sftpManager?.dispose();
|
||||||
|
|
||||||
|
final dockerManager = _dockerManagers.remove(hostId);
|
||||||
|
dockerManager?.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
for (final manager in _sftpManagers.values) {
|
||||||
|
manager.dispose();
|
||||||
|
}
|
||||||
|
for (final manager in _dockerManagers.values) {
|
||||||
|
manager.dispose();
|
||||||
|
}
|
||||||
|
_sftpManagers.clear();
|
||||||
|
_dockerManagers.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../core/api/api_client.dart';
|
import '../core/api/api_client.dart';
|
||||||
import '../features/auth/auth_session.dart';
|
import '../features/auth/auth_session.dart';
|
||||||
|
import 'docker_providers.dart';
|
||||||
import 'auth_state.dart';
|
import 'auth_state.dart';
|
||||||
import 'plus_info_notifier.dart';
|
import 'plus_info_notifier.dart';
|
||||||
import 'storage_providers.dart';
|
import 'storage_providers.dart';
|
||||||
@@ -63,6 +64,12 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
try {
|
try {
|
||||||
await _ref.read(terminalSessionManagerProvider).closeAll();
|
await _ref.read(terminalSessionManagerProvider).closeAll();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
await _ref.read(sftpSessionManagerProvider).disconnectAll();
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
_ref.read(dockerSessionManagerProvider).disconnectAll();
|
||||||
|
} catch (_) {}
|
||||||
try {
|
try {
|
||||||
final secureStorage = _ref.read(secureStorageProvider);
|
final secureStorage = _ref.read(secureStorageProvider);
|
||||||
await secureStorage.deleteToken();
|
await secureStorage.deleteToken();
|
||||||
|
|||||||
@@ -3,21 +3,32 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../features/shell/sftp_session_manager.dart';
|
import '../features/shell/sftp_session_manager.dart';
|
||||||
import '../features/terminal/server_status_monitor_manager.dart';
|
import '../features/terminal/server_status_monitor_manager.dart';
|
||||||
import '../features/terminal/terminal_session_manager.dart';
|
import '../features/terminal/terminal_session_manager.dart';
|
||||||
|
import '../features/terminal/terminal_session_resources.dart';
|
||||||
import 'terminal_settings_notifier.dart';
|
import 'terminal_settings_notifier.dart';
|
||||||
|
|
||||||
/// Single TerminalSessionManager for the whole app. Used to be passed
|
/// Single TerminalSessionManager for the whole app. Used to be passed
|
||||||
/// through constructors; now lives here so any page can reach it. Disposed
|
/// through constructors; now lives here so any page can reach it. Disposed
|
||||||
/// alongside the ProviderScope at app teardown.
|
/// alongside the ProviderScope at app teardown.
|
||||||
final terminalSessionManagerProvider = Provider<TerminalSessionManager>((ref) {
|
final terminalSessionManagerProvider = Provider<TerminalSessionManager>((ref) {
|
||||||
|
final resources = ref.watch(terminalSessionResourcesProvider);
|
||||||
final manager = TerminalSessionManager(
|
final manager = TerminalSessionManager(
|
||||||
statusMonitorManager: ref.watch(serverStatusMonitorManagerProvider),
|
statusMonitorManager: ref.watch(serverStatusMonitorManagerProvider),
|
||||||
shouldAutoStartStatusMonitor: () =>
|
shouldAutoStartStatusMonitor: () =>
|
||||||
ref.read(terminalSettingsProvider).autoServerStatus,
|
ref.read(terminalSettingsProvider).autoServerStatus,
|
||||||
|
onLastSessionForHostClosed: resources.disposeHost,
|
||||||
);
|
);
|
||||||
ref.onDispose(manager.dispose);
|
ref.onDispose(manager.dispose);
|
||||||
return manager;
|
return manager;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final terminalSessionResourcesProvider = Provider<TerminalSessionResources>((
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
final resources = TerminalSessionResources();
|
||||||
|
ref.onDispose(resources.dispose);
|
||||||
|
return resources;
|
||||||
|
});
|
||||||
|
|
||||||
final serverStatusMonitorManagerProvider = Provider<ServerStatusMonitorManager>(
|
final serverStatusMonitorManagerProvider = Provider<ServerStatusMonitorManager>(
|
||||||
(ref) {
|
(ref) {
|
||||||
final manager = ServerStatusMonitorManager();
|
final manager = ServerStatusMonitorManager();
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:easynode_native/features/terminal/terminal_session_resources.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('TerminalSessionResources', () {
|
||||||
|
test('reuses terminal-scoped managers for the same host', () {
|
||||||
|
final resources = TerminalSessionResources();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
identical(
|
||||||
|
resources.sftpForHost('host-a'),
|
||||||
|
resources.sftpForHost('host-a'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
identical(
|
||||||
|
resources.dockerForHost('host-a'),
|
||||||
|
resources.dockerForHost('host-a'),
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
|
resources.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps managers for different hosts independent', () {
|
||||||
|
final resources = TerminalSessionResources();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
identical(
|
||||||
|
resources.sftpForHost('host-a'),
|
||||||
|
resources.sftpForHost('host-b'),
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
identical(
|
||||||
|
resources.dockerForHost('host-a'),
|
||||||
|
resources.dockerForHost('host-b'),
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
|
||||||
|
resources.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user