Web SSH 的原理与在 ASP.NET Core SignalR 中的实现
前言有个项目,需要在前端有个管理终端可以 SSH 到主控机的终端,如果不考虑用户使用 vim 等需要在控制台内现实界面的软件的话,其实使用 Process 类型去启动相应程序就够了。而这次的需求则需要考虑用户会做相关设置。
原理
这里用到的原理是伪终端。伪终端(pseudo terminal)是现代操作系统的一个功能,他会模拟一对输入输出设备来模拟终端环境去执行相应的进程。伪终端通常会给相应的进程提供例如环境变量或文件等来告知他在终端中运行,这样像 vim 这样的程序可以在最后一行输出命令菜单或者像 npm / pip 这样的程序可以打印炫酷的进度条。通常在我们直接创建子进程的时候,在 Linux 上系统自带了 openpty 方法可以打开伪终端,而在 Windows 上则等到 Windows Terminal 推出后才出现了真正的系统级伪终端。下面付一张来自微软博客的伪终端原理图,Linux 上的原理与之类似。
基本设计
建立连接与监听终端输出
监听前端输入
graph TD;<template>
</template>A[终端窗口收到键盘事件] --> B;<template>
</template>B --> C[后台转发到对应终端]超时与关闭
graph TD;<template>
</template>A[当 SignalR 发送断开连接或终端超时] --> B[关闭终端进程];依赖库
portable_pty
这里用到这个 Rust 库来建立终端,这个库是一个独立的进程,每次建立连接都会运行。这里当初考虑过直接在 ASP.NET Core 应用里调用 vs-pty(微软开发的,用在 vs 里的库,可以直接在 vs 安装位置复制一份),但是 vs-pty 因为种种原因在 .NET 7 + Ubuntu 22.04 的环境下运行不起来故放弃了。
xterm.js
这个是前端展示终端界面用的库,据说 vs code 也在用这个库,虽然文档不多,但是用起来真的很简单。
SignalR
这个不多说了,咱 .NET 系列 Web 实时通信选他就没错。
代码
废话不多讲了,咱还是直接看代码吧,这里代码还是比较长的,我节选了一些必要的代码。具体 SignalR 之类的配置,还请读者自行参考微软官方文档。
[*]main.rs 这个 Rust 代码用于建立伪终端并和 .NET 服务通信,这里使用了最简单的 UDP 方式通信。
use portable_pty::{self, native_pty_system, CommandBuilder, PtySize};
use std::{io::prelude::*, sync::Arc};
use tokio::net::UdpSocket;
#
async fn main() -> Result<(), Box<dyn std::error::Error>> {
<template>
</template><template>
</template>let args = std::env::args().collect::<Vec<_>>();
<template>
</template><template>
</template>// 启动一个终端
<template>
</template><template>
</template>let pty_pair = native_pty_system().openpty(PtySize {
<template>
</template><template>
</template><template>
</template><template>
</template>rows: args.get(2).ok_or("NoNumber")?.parse()?,
<template>
</template><template>
</template><template>
</template><template>
</template>cols: args.get(3).ok_or("NoNumber")?.parse()?,
<template>
</template><template>
</template><template>
</template><template>
</template>pixel_width: 0,
<template>
</template><template>
</template><template>
</template><template>
</template>pixel_height: 0,
<template>
</template><template>
</template>})?;
<template>
</template><template>
</template>// 执行传进来的命令
<template>
</template><template>
</template>let mut cmd = CommandBuilder::new(args.get(4).unwrap_or(&"bash".to_string()));
<template>
</template><template>
</template>if args.len() > 5 {
<template>
</template><template>
</template><template>
</template><template>
</template>cmd.args(&args);
<template>
</template><template>
</template>}
<template>
</template><template>
</template>let mut proc = pty_pair.slave.spawn_command(cmd)?;
<template>
</template><template>
</template>// 绑定输入输出
<template>
</template><template>
</template>let mut reader = pty_pair.master.try_clone_reader()?;
<template>
</template><template>
</template>let mut writer = pty_pair.master.take_writer()?;
<template>
</template><template>
</template>// 绑定网络
<template>
</template><template>
</template>let main_socket = Arc::new(UdpSocket::bind("localhost:0").await?);
<template>
</template><template>
</template>let recv_socket = main_socket.clone();
<template>
</template><template>
</template>let send_socket = main_socket.clone();
<template>
</template><template>
</template>let resize_socket = UdpSocket::bind("localhost:0").await?;
<template>
</template><template>
</template>// 连接到主服务后发送地址
<template>
</template><template>
</template>main_socket
<template>
</template><template>
</template><template>
</template><template>
</template>.connect(args.get(1).ok_or("NoSuchAddr")?)
<template>
</template><template>
</template><template>
</template><template>
</template>.await?;
<template>
</template><template>
</template>main_socket
<template>
</template><template>
</template><template>
</template><template>
</template>.send(&serde_json::to_vec(&ClientAddr {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>main: main_socket.local_addr()?.to_string(),
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>resize: resize_socket.local_addr()?.to_string(),
<template>
</template><template>
</template><template>
</template><template>
</template>})?)
<template>
</template><template>
</template><template>
</template><template>
</template>.await?;
<template>
</template><template>
</template>// 读取终端数据并发送
<template>
</template><template>
</template>let read = tokio::spawn(async move {
<template>
</template><template>
</template><template>
</template><template>
</template>loop {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>let mut buf = ;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>let n = reader.read(&mut buf).unwrap();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if n == 0 {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>continue;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>println!("{:?}", &buf[..n]);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>send_socket.send(&buf[..n]).await.unwrap();
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template>});
<template>
</template><template>
</template>// 接收数据并写入终端
<template>
</template><template>
</template>let write = tokio::spawn(async move {
<template>
</template><template>
</template><template>
</template><template>
</template>loop {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>let mut buf = ;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>let n = recv_socket.recv(&mut buf).await.unwrap();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if n == 0 {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>continue;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>println!("{:?}", &buf[..n]);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>writer.write_all(&buf[..n]).unwrap();
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template>});
<template>
</template><template>
</template>// 接收调整窗口大小的数据
<template>
</template><template>
</template>let resize = tokio::spawn(async move {
<template>
</template><template>
</template><template>
</template><template>
</template>let mut buf = ;
<template>
</template><template>
</template><template>
</template><template>
</template>loop {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>let n = resize_socket.recv(&mut buf).await.unwrap();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if n == 0 {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>continue;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>let size: WinSize = serde_json::from_slice(buf[..n].as_ref()).unwrap();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>pty_pair
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>.master
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>.resize(PtySize {
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>rows: size.rows,
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>cols: size.cols,
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>pixel_width: 0,
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>pixel_height: 0,
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>})
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>.unwrap();
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template>});
<template>
</template><template>
</template>// 等待进程结束
<template>
</template><template>
</template>let result = proc.wait()?;
<template>
</template><template>
</template>write.abort();
<template>
</template><template>
</template>read.abort();
<template>
</template><template>
</template>resize.abort();
<template>
</template><template>
</template>if 0 == result.exit_code() {
<template>
</template><template>
</template><template>
</template><template>
</template>std::process::exit(result.exit_code() as i32);
<template>
</template><template>
</template>}
<template>
</template><template>
</template>return Ok(());
}
/// 窗口大小
#
struct WinSize {
<template>
</template><template>
</template>/// 行数
<template>
</template><template>
</template>rows: u16,
<template>
</template><template>
</template>/// 列数
<template>
</template><template>
</template>cols: u16,
}
/// 客户端地址
#
struct ClientAddr {
<template>
</template><template>
</template>/// 主要地址
<template>
</template><template>
</template>main: String,
<template>
</template><template>
</template>/// 调整窗口大小地址
<template>
</template><template>
</template>resize: String,
}
[*]SshPtyConnection.cs 这个代码用于维持一个后台运行的 Rust 进程,并管理他的双向通信。
<template>
</template><template>
</template>public class SshPtyConnection : IDisposable
<template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 客户端地址
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>private class ClientEndPoint
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>public required string Main { get; set; }
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>public required string Resize { get; set; }
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 窗口大小
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>private class WinSize
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>public int Cols { get; set; }
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>public int Rows { get; set; }
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// SignalR 上下文
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>private readonly IHubContext<SshHub> _hubContext;
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 日志记录器
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>private readonly ILogger<SshPtyConnection> _logger;
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// UDP 客户端
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>private readonly UdpClient udpClient;
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 最后活动时间
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>private DateTime lastActivity = DateTime.UtcNow;
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 是否已释放
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>private bool disposedValue;
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 是否已释放
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>public bool IsDisposed => disposedValue;
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 最后活动时间
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>public DateTime LastActivity => lastActivity;
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 取消令牌
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 窗口大小
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>public event EventHandler<EventArgs> Closed = delegate { };
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 构造函数
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="hubContext"></param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="logger"></param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <exception cref="ArgumentNullException"></exception>
<template>
</template><template>
</template><template>
</template><template>
</template>public SshPtyConnection(IHubContext<SshHub> hubContext, ILogger<SshPtyConnection> logger)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger = logger ?? throw new ArgumentNullException(nameof(logger));
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>lastActivity = DateTime.Now;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>udpClient = new(IPEndPoint.Parse("127.0.0.1:0"));
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 开始监听
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="connectionId">连接 ID</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="username">用户名</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="height">行数</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="width">列数</param>
<template>
</template><template>
</template><template>
</template><template>
</template>public async void StartAsync(string connectionId, string username, int height, int width)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var token = CancellationTokenSource.Token;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogInformation("process starting");
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>// 启动进程
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>using var process = Process.Start(new ProcessStartInfo
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>FileName = OperatingSystem.IsOSPlatform("windows") ? "PtyWrapper.exe" : "pty-wrapper",
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>// 这里用了 su -l username,因为程序直接部署在主控机的 root 下,所以不需要 ssh 只需要切换用户即可,如果程序部署在其他机器上,需要使用 ssh
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>ArgumentList = { udpClient.Client.LocalEndPoint!.ToString() ?? "127.0.0.1:0", height.ToString(), width.ToString(), "su", "-l", username }
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>});
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>// 接收客户端地址
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var result = await udpClient.ReceiveAsync();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var clientEndPoint = await JsonSerializer.DeserializeAsync<ClientEndPoint>(new MemoryStream(result.Buffer), new JsonSerializerOptions
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>PropertyNameCaseInsensitive = true
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>});
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (clientEndPoint == null)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>CancellationTokenSource.Cancel();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>process!.Exited += (_, _) => CancellationTokenSource.Cancel();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var remoteEndPoint = IPEndPoint.Parse(clientEndPoint.Main);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>udpClient.Connect(remoteEndPoint);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var stringBuilder = new StringBuilder();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>// 接收客户端数据,并发送到 SignalR,直到客户端断开连接或者超时 10 分钟
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>while (!token.IsCancellationRequested && lastActivity.AddMinutes(10) > DateTime.Now && !(process?.HasExited ?? false))
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>try
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>lastActivity = DateTime.Now;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var buffer = await udpClient.ReceiveAsync(token);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>await _hubContext.Clients.Client(connectionId).SendAsync("WriteDataAsync", Encoding.UTF8.GetString(buffer.Buffer));
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>stringBuilder.Clear();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>catch (Exception e)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogError(e, "ConnectionId: {ConnectionId} Unable to read data and send message.", connectionId);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>break;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>// 如果客户端断开连接或者超时 10 分钟,关闭进程
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (process?.HasExited ?? false) process?.Kill();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (lastActivity.AddMinutes(10) < DateTime.Now)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogInformation("ConnectionId: {ConnectionId} Pty session has been closed because of inactivity.", connectionId);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>try
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>await _hubContext.Clients.Client(connectionId).SendAsync("WriteErrorAsync", "InactiveTimeTooLong");
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>catch (Exception e)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogError(e, "ConnectionId: {ConnectionId} Unable to send message.", connectionId);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (token.IsCancellationRequested)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogInformation("ConnectionId: {ConnectionId} Pty session has been closed because of session closed.", connectionId);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>try
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>await _hubContext.Clients.Client(connectionId).SendAsync("WriteErrorAsync", "SessionClosed");
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>catch (Exception e)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogError(e, "ConnectionId: {ConnectionId} Unable to send message.", connectionId);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>Dispose();
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 接收 SignalR 数据,并发送到客户端
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="data">数据</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <returns></returns>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <exception cref="AppException"></exception>
<template>
</template><template>
</template><template>
</template><template>
</template>public async Task WriteDataAsync(string data)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (disposedValue)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>throw new AppException("SessionClosed");
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>try
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>lastActivity = DateTime.Now;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>await udpClient.SendAsync(Encoding.UTF8.GetBytes(data));
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>catch (Exception e)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>CancellationTokenSource.Cancel();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>Dispose();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>throw new AppException("SessionClosed", e);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 回收资源
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="disposing"></param>
<template>
</template><template>
</template><template>
</template><template>
</template>protected virtual void Dispose(bool disposing)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (!disposedValue)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (disposing)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>CancellationTokenSource.Cancel();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>udpClient.Dispose();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>disposedValue = true;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>Closed(this, new EventArgs());
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>public void Dispose()
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>Dispose(disposing: true);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>GC.SuppressFinalize(this);
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template>}
[*]SshService 这段代码用于管理 SshPtyConnection 和 SignalR 客户端连接之间的关系
<template>
</template><template>
</template>public class SshService : IDisposable
<template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template>private bool disposedValue;
<template>
</template><template>
</template><template>
</template><template>
</template>private readonly IHubContext<SshHub> _hubContext;
<template>
</template><template>
</template><template>
</template><template>
</template>private readonly ILoggerFactory _loggerFactory;
<template>
</template><template>
</template><template>
</template><template>
</template>private Dictionary<string, SshPtyConnection> _connections;
<template>
</template><template>
</template><template>
</template><template>
</template>public SshService(IHubContext<SshHub> hubContext, ILoggerFactory loggerFactory)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_connections = new Dictionary<string, SshPtyConnection>();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 创建终端连接
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="connectionId">连接 ID</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="username">用户名</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="height">行数</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="width">列数</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <returns></returns>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <exception cref="InvalidOperationException"></exception>
<template>
</template><template>
</template><template>
</template><template>
</template>public Task CreateConnectionAsync(string connectionId, string username, int height, int width)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (_connections.ContainsKey(connectionId))
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>throw new InvalidOperationException();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var connection = new SshPtyConnection(_hubContext, _loggerFactory.CreateLogger<SshPtyConnection>());
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>connection.Closed += (sender, args) =>
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_hubContext.Clients.Client(connectionId).SendAsync("WriteErrorAsync", "SessionClosed");
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_connections.Remove(connectionId);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>};
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_connections.Add(connectionId, connection);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>// 运行一个后台线程
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>connection.StartAsync(connectionId, username, height, width);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return Task.CompletedTask;
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 写入数据
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="connectionId">连接 ID</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="data">数据</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <exception cref="AppException"></exception>
<template>
</template><template>
</template><template>
</template><template>
</template>public async Task ReadDataAsync(string connectionId, string data)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (_connections.TryGetValue(connectionId, out var connection))
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>await connection.WriteDataAsync(data);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>else
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>throw new AppException("SessionClosed");
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 关闭连接
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="connectionId">连接 ID</param>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <exception cref="AppException"></exception>
<template>
</template><template>
</template><template>
</template><template>
</template>public Task CloseConnectionAsync(string connectionId)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (_connections.TryGetValue(connectionId, out var connection))
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>connection.Dispose();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>else
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>throw new AppException("SessionClosed");
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return Task.CompletedTask;
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>/// <summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// 回收资源
<template>
</template><template>
</template><template>
</template><template>
</template>/// </summary>
<template>
</template><template>
</template><template>
</template><template>
</template>/// <param name="disposing"></param>
<template>
</template><template>
</template><template>
</template><template>
</template>protected virtual void Dispose(bool disposing)
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (!disposedValue)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (disposing)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>foreach (var item in _connections.Values)
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>item.Dispose();
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>disposedValue = true;
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template><template>
</template><template>
</template>public void Dispose()
<template>
</template><template>
</template><template>
</template><template>
</template>{
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>Dispose(disposing: true);
<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>GC.SuppressFinalize(this);
<template>
</template><template>
</template><template>
</template><template>
</template>}
<template>
</template><template>
</template>}
[*]WebSsh.vue 这段代码是使用 vue 展示终端窗口的代码
<template>
</template>
[*]SshHub.cs 这个文件是 SignalR 的 Hub 文件,用来做监听的。
<template>
</template><template>
</template><template>
</template><template>
</template>public class SshHub : Hub<template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template>private readonly SshService _sshService;<template>
</template><template>
</template><template>
</template><template>
</template>private readonly ILogger _logger;<template>
</template><template>
</template><template>
</template><template>
</template>public SshHub(SshService sshService, ILogger logger)<template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_sshService = sshService ?? throw new ArgumentNullException(nameof(sshService));<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger = logger ?? throw new ArgumentNullException(nameof(logger));<template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> /// 创建一个新的终端<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> public async Task CreateNewTerminalAsync(int height = 24, int width = 80)<template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>try<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>var username = Context.User?.FindFirst("preferred_username")?.Value;<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (username == null)<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return new BaseResponse<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>Code = 401,<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>Message = "NoUsername"<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>};<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>if (!Context.User?.IsInRole("user") ?? false)<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>username = "root";<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogInformation($"{username}");<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>await _sshService.CreateConnectionAsync(Context.ConnectionId, username, height, width);<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return new BaseResponse();<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>catch (InvalidOperationException)<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return new BaseResponse() { Code = 500, Message = "TerminalAlreadyExist" };<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>catch (Exception e)<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogError(e, "ConnectionId: {ConnectionId} No such pty session.", Context.ConnectionId);<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return new BaseResponse() { Code = 500, Message = "UnableToCreateTerminal" };<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> /// 读取输入数据<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> public async Task ReadDataAsync(string data)<template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>try<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>await _sshService.ReadDataAsync(Context.ConnectionId, data);<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return new BaseResponse();<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>catch (Exception e)<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>_logger.LogError(e, "ConnectionId: {ConnectionId} No such pty session.", Context.ConnectionId);<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>return new BaseResponse { Message = "NoSuchSeesion", Code = 400 };<template>
</template><template>
</template><template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template><template>
</template><template>
</template>}<template>
</template><template>
</template>}<template>
</template><template>
</template>///<template>
</template><template>
</template> /// 客户端接口<template>
</template><template>
</template>///<template>
</template><template>
</template> public interface ISshHubClient<template>
</template><template>
</template>{<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> /// 写入输出数据<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> Task WriteDataAsync(string data);<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> /// 写入错误数据<template>
</template><template>
</template><template>
</template><template>
</template>///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> ///<template>
</template><template>
</template><template>
</template><template>
</template> Task WriteErrorAsync(string data);<template>
</template><template>
</template>}参考文献
[*]Windows Command-Line: Introducing the Windows Pseudo Console (ConPTY)
[*]portable_pty - Rust
[*]xterm.js
[*]教程:使用 TypeScript 和 Webpack 开始使用 ASP.NET Core SignalR
来源:https://www.cnblogs.com/aobaxu/archive/2023/10/31/17799346.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页:
[1]