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; using System.Reactive.Threading.Tasks; namespace AuroraDesk.Presentation.ViewModels.Pages; /// /// SSH 多客户端实时交互页面 ViewModel /// public sealed class SshPageViewModel : RoutableViewModel, IDisposable { private readonly ISshSessionService _sshSessionService; private readonly ILogger? _logger; private readonly ObservableCollection _sessions = new(); private SshSessionItemViewModel? _selectedSession; private string _host = "127.0.0.1"; private int _port = 22; private string _userName = "root"; private string? _password; private string? _privateKeyPath; private string? _privateKeyPassphrase; private string? _displayName; private string? _preferredShell; private string? _sshExecutablePath; private bool _allowAnyHostKey = true; private bool _isBusy; private string? _statusMessage; private SshAuthenticationMode _selectedAuthenticationMode = SshAuthenticationMode.Password; public SshPageViewModel( IScreen hostScreen, ISshSessionService sshSessionService, ILogger? logger = null) : base(hostScreen, "ssh-manager") { _sshSessionService = sshSessionService; _logger = logger; AddSessionCommand = ReactiveCommand.CreateFromTask(AddSessionAsync, this.WhenAnyValue(x => x.IsBusy, busy => !busy)); RemoveSessionCommand = ReactiveCommand.CreateFromTask(RemoveSessionAsync); SelectPasswordAuthCommand = ReactiveCommand.Create(() => { SelectedAuthenticationMode = SshAuthenticationMode.Password; }); SelectPrivateKeyAuthCommand = ReactiveCommand.Create(() => { SelectedAuthenticationMode = SshAuthenticationMode.PrivateKey; }); RefreshSnapshotCommand = ReactiveCommand.Create(RefreshSnapshot); RefreshSnapshot(); } public ObservableCollection Sessions => _sessions; public SshSessionItemViewModel? 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? PrivateKeyPath { get => _privateKeyPath; set => this.RaiseAndSetIfChanged(ref _privateKeyPath, value); } public string? PrivateKeyPassphrase { get => _privateKeyPassphrase; set => this.RaiseAndSetIfChanged(ref _privateKeyPassphrase, value); } public string? DisplayName { get => _displayName; set => this.RaiseAndSetIfChanged(ref _displayName, value); } public string? PreferredShell { get => _preferredShell; set => this.RaiseAndSetIfChanged(ref _preferredShell, value); } public string? SshExecutablePath { get => _sshExecutablePath; set => this.RaiseAndSetIfChanged(ref _sshExecutablePath, 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 SshAuthenticationMode 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 == SshAuthenticationMode.Password; public bool IsPrivateKeyMode => SelectedAuthenticationMode == SshAuthenticationMode.PrivateKey; public ReactiveCommand AddSessionCommand { get; } public ReactiveCommand RemoveSessionCommand { get; } public ReactiveCommand SelectPasswordAuthCommand { get; } public ReactiveCommand SelectPrivateKeyAuthCommand { get; } public ReactiveCommand RefreshSnapshotCommand { get; } public void Dispose() { foreach (var session in _sessions.ToArray()) { session.Dispose(); } } private async Task AddSessionAsync() { SshSessionItemViewModel? sessionViewModel = null; try { IsBusy = true; StatusMessage = "正在创建 SSH 会话..."; if (IsPasswordMode && string.IsNullOrWhiteSpace(Password)) { StatusMessage = "请填写密码或切换至私钥登录。"; return; } if (IsPrivateKeyMode && string.IsNullOrWhiteSpace(PrivateKeyPath)) { StatusMessage = "请提供私钥路径或切换至密码登录。"; return; } var options = new SshSessionOptions( Host, Port, UserName, IsPasswordMode ? Password : null, IsPrivateKeyMode ? PrivateKeyPath : null, IsPrivateKeyMode ? PrivateKeyPassphrase : null, DisplayName, AllowAnyHostKey, PreferredShell, SshExecutablePath); var info = await _sshSessionService.StartSessionAsync(options, CancellationToken.None).ConfigureAwait(false); sessionViewModel = new SshSessionItemViewModel(info, _sshSessionService, _logger); sessionViewModel.Initialize(); var firstStatus = await sessionViewModel .WhenAnyValue(x => x.Status) .SkipWhile(status => status == SshSessionStatus.Connecting) .Take(1) .Timeout(TimeSpan.FromSeconds(10)) .ToTask(CancellationToken.None) .ConfigureAwait(false); if (firstStatus is SshSessionStatus.Error or SshSessionStatus.Disconnected) { var latestError = await Dispatcher.UIThread.InvokeAsync(() => sessionViewModel.Messages.LastOrDefault(m => m.IsError)?.Content); sessionViewModel.Dispose(); StatusMessage = string.IsNullOrWhiteSpace(latestError) ? "创建会话失败:未能保持连接。" : $"创建会话失败:{latestError}"; return; } const string handshakeToken = "__aurora_ssh_handshake__"; var handshakeSent = await _sshSessionService.SendAsync(sessionViewModel.SessionId, $"echo {handshakeToken}", CancellationToken.None).ConfigureAwait(false); if (!handshakeSent) { sessionViewModel.Dispose(); StatusMessage = "创建会话失败:无法发送握手命令。"; return; } try { await _sshSessionService.ObserveMessages(sessionViewModel.SessionId) .Where(m => m.Direction == SshMessageDirection.Incoming && m.Content.Contains(handshakeToken, StringComparison.Ordinal)) .Take(1) .Timeout(TimeSpan.FromSeconds(5)) .ToTask(CancellationToken.None) .ConfigureAwait(false); } catch (TimeoutException) { sessionViewModel.Dispose(); StatusMessage = "创建会话失败:握手命令无响应。"; return; } await Task.Delay(TimeSpan.FromSeconds(0.5)).ConfigureAwait(false); var stableStatus = await Dispatcher.UIThread.InvokeAsync(() => sessionViewModel.Status); if (stableStatus is SshSessionStatus.Error or SshSessionStatus.Disconnected) { var latestError = await Dispatcher.UIThread.InvokeAsync(() => sessionViewModel.Messages.LastOrDefault(m => m.IsError)?.Content); sessionViewModel.Dispose(); StatusMessage = string.IsNullOrWhiteSpace(latestError) ? "创建会话失败:未能保持连接。" : $"创建会话失败:{latestError}"; return; } await Dispatcher.UIThread.InvokeAsync(() => { _sessions.Add(sessionViewModel); SelectedSession = sessionViewModel; }); StatusMessage = $"会话 {sessionViewModel.DisplayName} 已创建。"; } catch (Exception ex) { StatusMessage = $"创建会话失败: {ex.Message}"; _logger?.LogError(ex, "创建 SSH 会话失败。"); sessionViewModel?.Dispose(); } finally { IsBusy = false; } } private async Task RemoveSessionAsync(SshSessionItemViewModel? session) { if (session is null) { return; } _logger?.LogInformation("准备移除 SSH 会话 {SessionId}", session.SessionId); await _sshSessionService.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 = _sshSessionService.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 SshSessionItemViewModel(info, _sshSessionService, _logger); vm.Initialize(); _sessions.Add(vm); } } if (SelectedSession == null || !_sessions.Contains(SelectedSession)) { SelectedSession = _sessions.FirstOrDefault(); } } } /// /// 表示单个 SSH 会话的视图模型 /// public sealed class SshSessionItemViewModel : ReactiveObject, IDisposable { private readonly ISshSessionService _sshSessionService; private readonly ILogger? _logger; private readonly CompositeDisposable _disposables = new(); private readonly ObservableCollection _messages = new(); private string _displayName; private SshSessionStatus _status; private string _outgoingText = string.Empty; private DateTime _createdAtUtc; public SshSessionItemViewModel(SshSessionInfo info, ISshSessionService sshSessionService, ILogger? logger = null) { _sshSessionService = sshSessionService; _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 == SshSessionStatus.Connected && !string.IsNullOrWhiteSpace(text))); DisconnectCommand = ReactiveCommand.CreateFromTask(DisconnectAsync, this.WhenAnyValue(x => x.Status, status => status is SshSessionStatus.Connected or SshSessionStatus.Connecting)); } public Guid SessionId { get; } public ObservableCollection Messages => _messages; public string DisplayName { get => _displayName; private set => this.RaiseAndSetIfChanged(ref _displayName, value); } public SshSessionStatus 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 == SshSessionStatus.Connected; public ReactiveCommand SendCommand { get; } public ReactiveCommand DisconnectCommand { get; } public void Initialize() { try { var statusSubscription = _sshSessionService .ObserveStatus(SessionId) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(status => { Status = status; }); _disposables.Add(statusSubscription); var messageSubscription = _sshSessionService .ObserveMessages(SessionId) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(message => { var entry = SshMessageEntry.FromMessage(message); _messages.Add(entry); }); _disposables.Add(messageSubscription); } catch (Exception ex) { _logger?.LogError(ex, "订阅 SSH 会话 {SessionId} 流失败。", SessionId); } } private async Task SendAsync() { var text = OutgoingText; if (string.IsNullOrWhiteSpace(text)) { return; } var result = await _sshSessionService.SendAsync(SessionId, text, CancellationToken.None).ConfigureAwait(false); if (result) { await Dispatcher.UIThread.InvokeAsync(() => { OutgoingText = string.Empty; }); } } private Task DisconnectAsync() { return _sshSessionService.StopSessionAsync(SessionId); } public void Dispose() { _disposables.Dispose(); } } /// /// UI 消息项 /// public sealed record SshMessageEntry( Guid SessionId, string Timestamp, SshMessageDirection Direction, string Content, bool IsError) { public static SshMessageEntry FromMessage(SshMessage message) { var timestamp = message.Timestamp.ToLocalTime().ToString("HH:mm:ss"); return new SshMessageEntry( message.SessionId, timestamp, message.Direction, message.Content, message.IsError); } } public enum SshAuthenticationMode { Password, PrivateKey }