七亿批量远程管理工具(RdpManager)更新到 v1.6.4。这次就干了一件事:把 SSH 终端从原来的 RichTextBox 换成自己写的一个终端模拟器。
起因是有个用户提了一句「SSH 里没法用 vi 编辑文件,你这终端不完整」。我一开始还有点不服气,自己连上去试——敲命令、看输出都正常,但一进 vi / nano / top / htop 这种全屏程序,界面直接花掉,光标到处乱跳,根本没法用。确实是我理亏。
为什么 RichTextBox 做不出全屏程序
老终端本质上是「一个能打字的富文本框」:服务器回什么就往框里追加什么,再自己画一行假的输入行。看日志、敲命令没毛病,但它压根不理解终端控制序列。
vi 这种程序不是「一行一行往下打」,而是直接把整块屏幕画出来:
- 往某个坐标写字符(
ESC[row;colH) - 移动光标、擦掉一块区域
- 切换「备用屏幕」(
ESC[?1049h)——全屏画面其实画在另一张画布上,退出时再切回来 - 改颜色、加粗、反显
这些控制码丢进 RichTextBox 全是天书,vi 一进去自然就废了。
方案:xterm.js 还是自己写
做真终端有两条路:
- 引 xterm.js + WebView2,现成的终端模拟器,功能最全
- 自己写一个 VT100 模拟器,用 WinForms 原生画
第一条五分钟就能跑起来,代价是得往里塞一个几百 MB 的 WebView2 运行时。我们这软件一直主打「绿色免安装、单文件、解压即用」,为一个终端把体积撑到几百 MB,等于把定位给改了。纠结了一阵,还是选了第二条。
自己写就拆成两块,刻意把「逻辑」和「界面」分开:
TerminalBuffer.cs:纯逻辑的屏幕缓冲,维护一张字符网格(每格存字符 + 前景色 + 背景色 + 属性),负责解析 ANSI/VT100 控制序列——SGR 颜色、光标移动、清屏、滚动、备用屏切换、DSR 应答这些TerminalView.cs:自绘控件,把缓冲里的字符网格画到屏幕上,处理键盘、鼠标、粘贴
分开的好处是逻辑那层能脱离界面直接跑单元测试。我拿真实服务器的原始字节流(就是格式化磁盘那种带进度动画的)喂进去,19 个用例全绿了才敢合。
最大的一个坑:键敲进去了,服务器收不到
终端在本地测都正常,一连真机——敲键盘没反应,粘贴也没反应。
先确认「键盘 → 控件」这段。用 Xvfb + xdotool 模拟按键,在控件里打日志,看到事件确实进来了。那就是「控件 → SSH 通道」这段的问题。
一路查到 SSH.NET 的 ShellStream:
_shell.Write(data, 0, data.Length); // 只写进内部缓冲区!Write(byte[]) 只往内部缓冲里写,缓冲区满了才真正发出去;而 Write(string) 内部会自动 Flush。(旧代码用的是 WriteLine,所以这个问题一直没露头。)
加一行就好:
_shell.Write(data, 0, data.Length);
_shell.Flush(); // 立即发出去就这么一行,前前后后对比了好几次才定位到——因为「发送层」不像 UI 那样能直接截图看见,只能靠日志一点点试。
顺手改的几个小地方
- 字号能调了:终端工具栏加了个「字号」下拉框,嫌字小嫌字大自己挑
- 加了个小企鹅图标:SSH 终端窗口的标题栏和任务栏,在
root@前面挂了个企鹅,一眼能认出这是 SSH 会话 - 系统托盘:SSH 终端最小化后收进托盘(还是那只企鹅),连接不断,双击图标还原窗口
已知的边界
自研终端毕竟不是 xterm 的完整实现,有几个已知限制,先记在这:
- 窗口缩放不会通知远端改列宽,远端还以为屏幕是原来那么宽(SIGWINCH 那套没做)
- 鼠标事件没转发给远端,所以 vi 的鼠标模式、htop 里点击排序这些用不了
- 256 色会近似映射到 16 色
这些后面慢慢补,常用的 vi / nano / top / htop 已经能正常干活了。
小结
引 xterm.js 是省事,但会把「绿色单文件」这个卖点弄没了;自己写多花了点时间,换回来的是一个干净、可控、不依赖外部运行时的终端。写完整终端模拟器不算小事,磨了挺久。至少「SSH 打不开 vi」这个被念叨了很久的问题,算是翻篇了——当然,下一个版本又会被挑出什么毛病,那就不好说了。
v1.6.4 更新日志:
SSH 终端重写为真正的终端模拟器:支持 vi / vim / nano / top / htop 等全屏程序,整屏刷新、方向键与功能键正常
SSH 终端新增「字号」下拉框,可自选终端字体大小
SSH 终端窗口标题栏 / 任务栏显示小企鹅图标;新增系统托盘图标,最小化后收进托盘、连接不中断,双击企鹅图标还原窗口
评论