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
}