You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
14 KiB

1 month ago
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using AuroraDesk.Core.Entities;
using AuroraDesk.Core.Interfaces;
using AuroraDesk.Presentation.ViewModels.Base;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Avalonia.Threading;
namespace AuroraDesk.Presentation.ViewModels.Pages;
/// <summary>
/// Plink 多客户端实时交互页面 ViewModel
/// </summary>
public sealed class PlinkPageViewModel : RoutableViewModel, IDisposable
{
private readonly IPlinkSessionService _plinkSessionService;
private readonly ILogger<PlinkPageViewModel>? _logger;
private readonly ObservableCollection<PlinkSessionItemViewModel> _sessions = new();
private PlinkSessionItemViewModel? _selectedSession;
private string _host = "127.0.0.1";
private int _port = 22;
private string _userName = "root";
private string? _password;
private string? _displayName;
private string? _privateKeyPath;
private string? _additionalArguments;
private string? _plinkExecutablePath;
private bool _allowAnyHostKey = true;
private bool _isBusy;
private string? _statusMessage;
private PlinkAuthenticationMode _selectedAuthenticationMode = PlinkAuthenticationMode.Password;
public PlinkPageViewModel(
IScreen hostScreen,
IPlinkSessionService plinkSessionService,
ILogger<PlinkPageViewModel>? logger = null)
: base(hostScreen, "plink-manager")
{
_plinkSessionService = plinkSessionService;
_logger = logger;
AddSessionCommand = ReactiveCommand.CreateFromTask(AddSessionAsync,
this.WhenAnyValue(x => x.IsBusy, busy => !busy));
RemoveSessionCommand = ReactiveCommand.CreateFromTask<PlinkSessionItemViewModel>(RemoveSessionAsync);
SelectPasswordAuthCommand = ReactiveCommand.Create(() =>
{
SelectedAuthenticationMode = PlinkAuthenticationMode.Password;
});
SelectPrivateKeyAuthCommand = ReactiveCommand.Create(() =>
{
SelectedAuthenticationMode = PlinkAuthenticationMode.PrivateKey;
});
RefreshSnapshotCommand = ReactiveCommand.Create(RefreshSnapshot);
RefreshSnapshot();
}
public ObservableCollection<PlinkSessionItemViewModel> Sessions => _sessions;
public PlinkSessionItemViewModel? SelectedSession
{
get => _selectedSession;
set => this.RaiseAndSetIfChanged(ref _selectedSession, value);
}
public string Host
{
get => _host;
set => this.RaiseAndSetIfChanged(ref _host, value);
}
public int Port
{
get => _port;
set => this.RaiseAndSetIfChanged(ref _port, value);
}
public string UserName
{
get => _userName;
set => this.RaiseAndSetIfChanged(ref _userName, value);
}
public string? Password
{
get => _password;
set => this.RaiseAndSetIfChanged(ref _password, value);
}
public string? DisplayName
{
get => _displayName;
set => this.RaiseAndSetIfChanged(ref _displayName, value);
}
public string? PrivateKeyPath
{
get => _privateKeyPath;
set => this.RaiseAndSetIfChanged(ref _privateKeyPath, value);
}
public string? AdditionalArguments
{
get => _additionalArguments;
set => this.RaiseAndSetIfChanged(ref _additionalArguments, value);
}
public string? PlinkExecutablePath
{
get => _plinkExecutablePath;
set => this.RaiseAndSetIfChanged(ref _plinkExecutablePath, value);
}
public bool AllowAnyHostKey
{
get => _allowAnyHostKey;
set => this.RaiseAndSetIfChanged(ref _allowAnyHostKey, value);
}
public bool IsBusy
{
get => _isBusy;
private set => this.RaiseAndSetIfChanged(ref _isBusy, value);
}
public string? StatusMessage
{
get => _statusMessage;
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
public PlinkAuthenticationMode SelectedAuthenticationMode
{
get => _selectedAuthenticationMode;
private set
{
if (_selectedAuthenticationMode == value)
{
return;
}
this.RaiseAndSetIfChanged(ref _selectedAuthenticationMode, value);
this.RaisePropertyChanged(nameof(IsPasswordMode));
this.RaisePropertyChanged(nameof(IsPrivateKeyMode));
}
}
public bool IsPasswordMode => SelectedAuthenticationMode == PlinkAuthenticationMode.Password;
public bool IsPrivateKeyMode => SelectedAuthenticationMode == PlinkAuthenticationMode.PrivateKey;
public ReactiveCommand<Unit, Unit> AddSessionCommand { get; }
public ReactiveCommand<PlinkSessionItemViewModel, Unit> RemoveSessionCommand { get; }
public ReactiveCommand<Unit, Unit> SelectPasswordAuthCommand { get; }
public ReactiveCommand<Unit, Unit> SelectPrivateKeyAuthCommand { get; }
public ReactiveCommand<Unit, Unit> RefreshSnapshotCommand { get; }
public void Dispose()
{
foreach (var session in _sessions.ToArray())
{
session.Dispose();
}
}
private async Task AddSessionAsync()
{
PlinkSessionItemViewModel? sessionViewModel = null;
try
{
IsBusy = true;
StatusMessage = "正在创建新会话...";
var options = new PlinkSessionOptions(
Host,
Port,
UserName,
IsPasswordMode ? Password : null,
IsPrivateKeyMode ? PrivateKeyPath : null,
DisplayName,
AdditionalArguments,
PlinkExecutablePath,
AllowAnyHostKey);
var info = await _plinkSessionService.StartSessionAsync(options, CancellationToken.None).ConfigureAwait(false);
sessionViewModel = new PlinkSessionItemViewModel(info, _plinkSessionService, _logger);
sessionViewModel.Initialize();
await Dispatcher.UIThread.InvokeAsync(() =>
{
_sessions.Add(sessionViewModel);
SelectedSession = sessionViewModel;
});
await Task.Delay(TimeSpan.FromSeconds(1.5)).ConfigureAwait(false);
var currentStatus = await Dispatcher.UIThread.InvokeAsync(() => sessionViewModel.Status);
if (currentStatus is PlinkSessionStatus.Error or PlinkSessionStatus.Disconnected)
{
var latestError = await Dispatcher.UIThread.InvokeAsync(() =>
{
return sessionViewModel.Messages.LastOrDefault(m => m.IsError)?.Content;
});
await Dispatcher.UIThread.InvokeAsync(() =>
{
_sessions.Remove(sessionViewModel);
if (ReferenceEquals(SelectedSession, sessionViewModel))
{
SelectedSession = _sessions.LastOrDefault();
}
});
sessionViewModel.Dispose();
StatusMessage = string.IsNullOrWhiteSpace(latestError)
? "创建会话失败:未能保持连接。"
: $"创建会话失败:{latestError}";
return;
}
StatusMessage = $"会话 {sessionViewModel.DisplayName} 已创建。";
}
catch (Exception ex)
{
StatusMessage = $"创建会话失败: {ex.Message}";
_logger?.LogError(ex, "创建 Plink 会话失败。");
sessionViewModel?.Dispose();
}
finally
{
IsBusy = false;
}
}
private async Task RemoveSessionAsync(PlinkSessionItemViewModel? session)
{
if (session is null)
{
return;
}
_logger?.LogInformation("准备移除 Plink 会话 {SessionId}", session.SessionId);
await _plinkSessionService.StopSessionAsync(session.SessionId).ConfigureAwait(false);
await Dispatcher.UIThread.InvokeAsync(() =>
{
_sessions.Remove(session);
if (ReferenceEquals(SelectedSession, session))
{
SelectedSession = _sessions.LastOrDefault();
}
session.Dispose();
StatusMessage = $"会话 {session.DisplayName} 已移除。";
});
}
private void RefreshSnapshot()
{
var snapshot = _plinkSessionService.GetSessions();
foreach (var session in _sessions.ToArray())
{
if (snapshot.All(x => x.SessionId != session.SessionId))
{
session.Dispose();
_sessions.Remove(session);
}
}
foreach (var info in snapshot)
{
if (_sessions.All(x => x.SessionId != info.SessionId))
{
var vm = new PlinkSessionItemViewModel(info, _plinkSessionService, _logger);
vm.Initialize();
_sessions.Add(vm);
}
}
if (SelectedSession == null || !_sessions.Contains(SelectedSession))
{
SelectedSession = _sessions.FirstOrDefault();
}
}
}
/// <summary>
/// 表示单个 Plink 会话的视图模型
/// </summary>
public sealed class PlinkSessionItemViewModel : ReactiveObject, IDisposable
{
private readonly IPlinkSessionService _plinkSessionService;
private readonly ILogger? _logger;
private readonly CompositeDisposable _disposables = new();
private readonly ObservableCollection<PlinkMessageEntry> _messages = new();
private string _displayName;
private PlinkSessionStatus _status;
private string _outgoingText = string.Empty;
private DateTime _createdAtUtc;
public PlinkSessionItemViewModel(PlinkSessionInfo info, IPlinkSessionService plinkSessionService, ILogger? logger = null)
{
_plinkSessionService = plinkSessionService;
_logger = logger;
SessionId = info.SessionId;
_displayName = info.DisplayName;
_status = info.Status;
_createdAtUtc = info.CreatedAtUtc;
SendCommand = ReactiveCommand.CreateFromTask(SendAsync,
this.WhenAnyValue(x => x.Status, x => x.OutgoingText,
(status, text) => status == PlinkSessionStatus.Connected && !string.IsNullOrWhiteSpace(text)));
DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync,
this.WhenAnyValue(x => x.Status, status => status is PlinkSessionStatus.Connected or PlinkSessionStatus.Connecting));
}
public Guid SessionId { get; }
public ObservableCollection<PlinkMessageEntry> Messages => _messages;
public string DisplayName
{
get => _displayName;
private set => this.RaiseAndSetIfChanged(ref _displayName, value);
}
public PlinkSessionStatus Status
{
get => _status;
private set => this.RaiseAndSetIfChanged(ref _status, value);
}
public string OutgoingText
{
get => _outgoingText;
set => this.RaiseAndSetIfChanged(ref _outgoingText, value);
}
public DateTime CreatedAtUtc
{
get => _createdAtUtc;
private set => this.RaiseAndSetIfChanged(ref _createdAtUtc, value);
}
public bool IsConnected => Status == PlinkSessionStatus.Connected;
public ReactiveCommand<Unit, Unit> SendCommand { get; }
public ReactiveCommand<Unit, Unit> DisconnectCommand { get; }
public void Initialize()
{
try
{
var statusSubscription = _plinkSessionService
.ObserveStatus(SessionId)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(status =>
{
Status = status;
});
_disposables.Add(statusSubscription);
var messageSubscription = _plinkSessionService
.ObserveMessages(SessionId)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(message =>
{
var entry = PlinkMessageEntry.FromMessage(message);
_messages.Add(entry);
});
_disposables.Add(messageSubscription);
}
catch (Exception ex)
{
_logger?.LogError(ex, "订阅 Plink 会话 {SessionId} 流失败。", SessionId);
}
}
private async Task SendAsync()
{
var text = OutgoingText;
if (string.IsNullOrWhiteSpace(text))
{
return;
}
var result = await _plinkSessionService.SendAsync(SessionId, text, CancellationToken.None).ConfigureAwait(false);
if (result)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
OutgoingText = string.Empty;
});
}
}
private Task DisconnectAsync()
{
return _plinkSessionService.StopSessionAsync(SessionId);
}
public void Dispose()
{
_disposables.Dispose();
}
}
/// <summary>
/// UI 消息项
/// </summary>
public sealed record PlinkMessageEntry(
Guid SessionId,
string Timestamp,
PlinkMessageDirection Direction,
string Content,
bool IsError)
{
public static PlinkMessageEntry FromMessage(PlinkMessage message)
{
var timestamp = message.Timestamp.ToLocalTime().ToString("HH:mm:ss");
return new PlinkMessageEntry(
message.SessionId,
timestamp,
message.Direction,
message.Content,
message.IsError);
}
}
public enum PlinkAuthenticationMode
{
Password,
PrivateKey
}