namespace XFEExtension.NetCore.XFEConsole; using System.Collections.Concurrent; using System.Net.WebSockets; using System.Security.Cryptography; using System.Text; using XFEExtension.NetCore.CyberComm; using XFEExtension.NetCore.DelegateExtension; /// /// XFE控制台进程服务器 /// public sealed class XFEConsoleProgramServer : IAsyncDisposable { private readonly ConcurrentDictionary terminals = new(); /// 创建供远程终端连接的调试程序服务器。 public XFEConsoleProgramServer(int port = 3280, bool localOnly = true, string password = "", string? programName = null, string? programId = null) : this(localOnly ? [$"http://localhost:{port}/"] : [$"http://*:{port}/"], password, programName, programId) { } /// 使用指定监听地址创建调试程序服务器。 public XFEConsoleProgramServer(string[] listenAddresses, string password = "", string? programName = null, string? programId = null) { ArgumentNullException.ThrowIfNull(listenAddresses); if (listenAddresses.Length == 0) throw new ArgumentException("至少需要一个监听地址。", nameof(listenAddresses)); Password = password ?? string.Empty; ProgramName = string.IsNullOrWhiteSpace(programName) ? AppDomain.CurrentDomain.FriendlyName : programName; ProgramId = string.IsNullOrWhiteSpace(programId) ? Guid.NewGuid().ToString("N") : programId; Server = new CyberCommServer(listenAddresses) { StartedHandler = HandleStartedAsync, WebSocketConnectedHandler = HandleConnectedAsync, WebSocketClosedHandler = HandleClosedAsync, WebSocketMessageHandler = HandleMessageAsync, ErrorHandler = HandleServerError }; } /// 底层通信服务器。 public CyberCommServer Server { get; } /// 终端连接密码。 public string Password { get; } /// 调试程序名称。 public string ProgramName { get; } /// 调试程序唯一标识。 public string ProgramId { get; } /// 当前已通过鉴权的终端。 public IReadOnlyCollection ConnectedTerminals => terminals.Values.ToArray(); /// 服务器启动时触发。 public event XFEEventHandler? ServerStarted; /// 终端通过鉴权并连接时触发。 public event XFEEventHandler? Connected; /// 已鉴权终端断开时触发。 public event XFEEventHandler? Disconnected; /// 通信发生错误时触发。 public event XFEEventHandler? ErrorOccurred; /// 开始监听;监听就绪后返回。 public Task StartAsync(CancellationToken cancellationToken = default) => Server.StartAsync(cancellationToken); /// 等待服务器停止。 public Task RunAsync(CancellationToken cancellationToken = default) => Server.RunAsync(cancellationToken); /// 停止监听并断开所有终端。 public async Task StopAsync(CancellationToken cancellationToken = default) { await Server.StopAsync(cancellationToken).ConfigureAwait(false); terminals.Clear(); } /// 向所有已鉴权终端发送控制台输出。 public async Task OutputMessage(string message, bool isLine, CancellationToken cancellationToken = default) { var payload = XFEConsoleProtocol.CreateOutputMessage(message, isLine); var snapshot = terminals.ToArray(); if (snapshot.Length == 0) return; await Task.WhenAll(snapshot.Select(pair => SendOutputAsync(pair.Key, pair.Value, payload, cancellationToken))).ConfigureAwait(false); } private ValueTask HandleStartedAsync(CancellationToken cancellationToken) { terminals.Clear(); ServerStarted?.Invoke(this); return ValueTask.CompletedTask; } private async ValueTask HandleConnectedAsync(CyberCommServerEventArgs eventArgs, CancellationToken cancellationToken) { var terminalName = eventArgs.WSHeader[XFEConsoleProtocol.TerminalNameHeader]; var terminalId = eventArgs.WSHeader[XFEConsoleProtocol.TerminalIdHeader]; var suppliedPassword = eventArgs.WSHeader[XFEConsoleProtocol.PasswordHeader]; var accepted = !string.IsNullOrWhiteSpace(terminalName) && !string.IsNullOrWhiteSpace(terminalId) && suppliedPassword is not null && PasswordMatches(Password, suppliedPassword); if (!accepted) { var reason = suppliedPassword is null ? "连接请求缺少密码。" : "密码错误。"; await RejectAndCloseAsync(eventArgs, reason).ConfigureAwait(false); return; } var terminal = new XFEConsoleTerminalInfo(terminalName!, terminalId!, eventArgs); if (!terminals.TryAdd(eventArgs.CurrentWebSocket, terminal)) { await RejectAndCloseAsync(eventArgs, "终端连接已存在。").ConfigureAwait(false); return; } try { await eventArgs.ReplyMessage(XFEConsoleProtocol.CreateAuthenticationMessage(true, string.Empty, ProgramName, ProgramId)).ConfigureAwait(false); Connected?.Invoke(this, terminal); } catch (Exception exception) { terminals.TryRemove(eventArgs.CurrentWebSocket, out _); ErrorOccurred?.Invoke(this, exception); await CloseIgnoringErrorsAsync(eventArgs).ConfigureAwait(false); } } private ValueTask HandleClosedAsync(CyberCommServerEventArgs eventArgs, CancellationToken cancellationToken) { if (terminals.TryRemove(eventArgs.CurrentWebSocket, out var terminal)) Disconnected?.Invoke(this, terminal); return ValueTask.CompletedTask; } private ValueTask HandleMessageAsync(CyberCommServerEventArgs eventArgs, CancellationToken cancellationToken) { if (eventArgs.MessageType == BackMessageType.Error && eventArgs.Exception is not null) ErrorOccurred?.Invoke(this, eventArgs.Exception); return ValueTask.CompletedTask; } private void HandleServerError(Exception exception) => ErrorOccurred?.Invoke(this, exception); private async Task SendOutputAsync(WebSocket webSocket, XFEConsoleTerminalInfo terminal, string payload, CancellationToken cancellationToken) { try { cancellationToken.ThrowIfCancellationRequested(); await terminal.EventArgs.ReplyMessage(payload).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception exception) { terminals.TryRemove(webSocket, out _); ErrorOccurred?.Invoke(this, exception); } } private async Task RejectAndCloseAsync(CyberCommServerEventArgs eventArgs, string reason) { try { await eventArgs.ReplyMessage(XFEConsoleProtocol.CreateAuthenticationMessage(false, reason, ProgramName, ProgramId)).ConfigureAwait(false); } catch (Exception exception) { ErrorOccurred?.Invoke(this, exception); } await CloseIgnoringErrorsAsync(eventArgs).ConfigureAwait(false); } private static async Task CloseIgnoringErrorsAsync(CyberCommServerEventArgs eventArgs) { try { await eventArgs.Close().ConfigureAwait(false); } catch { try { eventArgs.ForceClose(); } catch { } } } private static bool PasswordMatches(string expected, string actual) { var expectedBytes = Encoding.UTF8.GetBytes(expected); var actualBytes = Encoding.UTF8.GetBytes(actual); return expectedBytes.Length == actualBytes.Length && CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes); } /// public async ValueTask DisposeAsync() { await StopAsync().ConfigureAwait(false); await Server.DisposeAsync().ConfigureAwait(false); } }