前言
最近在学习和处理终端的输入和输出以及命令拦截问题,但终端在编程里属于比较小众的领域,比较有名的类库不多,比如 go 的 go-prompt、termbox-go、pty 以及 ts 的 xterm.js。
不同的终端,在处理输入和输出时,所对应的数据也不一样,以及存在跨平台等问题。总得来说跟终端打交道,就是一件苦差事,不过有这么多优秀的开源类库,让我们在了解和处理终端问题时,提供了和很好的思路。
终端的介绍
终端(英语:Computer terminal),是一台电脑或者计算机系统,用来让用户输入数据,及显示其计算结果的机器,简而言之就是人类用户与计算机交互的设备。终端有些是全电子的,也有些是机电的。其又名终端机,它与一部独立的电脑不同,但也是电脑组成的部分。– by wiki
终端的种类
硬件终端
终端其实就是一种输入输出设备,相对于计算机主机而言属于外设,本身并不提供运算处理功能。早期的计算机终端一般是机电的电传打字机,比如ASR33。但是对于大多数应用来说它们太慢了,在卡片或磁带等物理性的材料上标记好资料之后,放入计算机再印出结果,过程非常费工。1970年代初许多电脑公司认识到电视输入终端比穿孔卡片要好得多,而且可以使得计算机更加容易与用户互动,产生新的应用。当时的毛病在于相对于当时的小型计算机来说要显示一页文字所需要的内存太大了。在集成电路普及以前要显示电视信号所需要的速度对当时的逻辑门所提出的技术挑战也太高了。当时有一家公司宣布要生产一台价值15,000美元的视频终端,吸引了许多购户,但是最后它的工程师们决定这个计划无法完成。另一个解决方法是泰克公司发明的存储管,这台机器可以显示输出给它的信息,但是无法刷新。
约1982年左右的一台ASCII视频终端
后来输出端的显示功能被发展出来后,早期的视频终端使用特别的逻辑门,没有自己的中央处理器。发展微处理器的动机之一就是要简化终端里的电子组件的复杂性。大多数终端的屏幕是绿色或者橙色的,它们与大型计算机相连。典型的终端使用RS-232之类的串行数据通信与主机相连,IBM使用它自己的系统网络体系结构协议通过同轴电缆来连接其主机与终端。
最后所谓的智能终端(如VT52和VT100)被引入。今天依然有许多这两个终端的模拟软件。这些终端之所以被称为“智能”是因为它们理解转义序列,可以定位光标和控制显示位置,这样设计的终端很容易操作,已经有现代电脑的样子了。重要的非VT100终端有IBM 3270、不同的慧智模型和Tektronix 4014。1970年代里世界上有十数个终端生产商,大多数终端的指令不兼容。1970年代和1980年代初最重要的终端生产商有迪吉多、慧智、Televideo、利尔·西格勒公司和Heathkit。
早期的IBM个人计算机虽然也使用绿色的屏幕,但是它不算终端。个人计算机的屏幕不包括任何产生字母的硬件,所有的视频信号是在个人计算机的显卡里产生的。但是使用相应的模拟程序一台个人计算机可以与大型计算机相连模拟终端。使用微处理器的个人计算机大大地取消了对终端的需要,人与电脑的接触直接用图像的操作系统来代劳了。但今天大多数个人计算机的Telnet用户端仍提供最普遍的终端(一般VT100)的模拟,但这不是真正的物理终端。
图形终端
有些终端不但可以显示文字,而且可以显示矢量图形和位图。计算机向终端输出绘图指令,终端则向计算机输送用户输入(通过键盘或者定位设备)。
事实上今天过去简单的图形终端已经完全被全功能视频显示器代替了。今天在计算机中图形用户界面无处不在。大多数终端模拟程序是在图形环境内运行的。我们现在主要是透过这些辅助工具,已经很少有直接终端的存在。
X终端是专门给X窗口系统设计的图形终端,提供连接到服务器系统上运行的KDE、GNOME或其它基于X窗口系统的平台的可能性。
虚拟终端
由于个人电脑的普及今天已经很少有专门的计算机终端作为界面了。现代的操作系统如Linux和BSD及其派生物使用与硬件基本无关的虚拟终端。输出系统一般是屏幕,输入系统则是键盘。
在使用X窗口系统这样的图形用户界面时在屏幕上一般有多个与不同应用相关的窗口开着,而不是只有一个与一个单个过程相连的文字流。在这种情况下用户一般使用终端模拟程序。这样用户可以不必使用专门的终端设备来与计算机交换。
虚拟终端机(英语:Terminal emulator)是在个人电脑上虚拟的一个终端以及为此目的而写的软件。虚拟终端的目的是达到个人电脑及其用户能够与大型计算机的连接。一般来说需要连接的大型计算机是IBM的大型计算机或者所谓的超小型计算机(过去往往是迪吉多的VAX)。
虚拟终端使得个人电脑的用户可以直接使用他的个人电脑来与大型计算机联系,而不必使用专门的终端。
通过虚拟终端的软件虚拟终端还可以扩展大型计算机的标准终端的功能,通过虚拟终端不但可以将个人电脑上的数据传递给大型计算机,而且还可以将大型计算机的数据传递给个人电脑,并在个人电脑上继续加工。
一般大型计算机的终端是字母式的输入和输出接口,因此一个虚拟终端至少需要一个能够模拟这样的字母式(比如ASCII)输入和输出接口的能力。最常见的平台是图像式的用户表面。要使得新的、图像式的程序能够使用老的字母式的或者没有图像式输入和输出能力的程序也需要虚拟终端。
现代的大型计算机也内部使用虚拟终端,这样它们可以向老的、需要终端的程序假装一个终端,而实际上它则将程序的显示转到显卡上。比如Linux以及其它大多数基于个人电脑的类似Unix的操作系统假装有六至十个这样的“虚拟”的终端。
使用go处理终端的输入和输出
通常,如果我们需要设计和使用一种终端,比如拦截终端命令,执行命令前检查文件大小,设置命令白名单拒绝高危命令。
xterm
xterm是一个X Window System上的标准虚拟终端。用户可以在同一个显示器上开启许多xterm,每一个都为其中运行的进程提供独立的输入输出(一般来说此进程是Unix shell)。
xterm 最先是Jim Gettys的学生Mark Vandevoorde在1984年夏天为VS100写的独立虚拟终端,当时X的开发才刚刚开始。很快人们就发现它作为X的一部分比作为独立的程序更为有用,于是它开始针对X而开发。Gettys曾讲述过有关的故事 ,“xterm内部如此恐怖的部分原因,是最初想法把它作为单独进程驱动多个VS100显示窗。”(”part of why xterm’s internals are so horrifying is that it was originally intended that a single process be able to drive multiple VS100 displays.”)
在作为X参考实现的一个部分后多年,1996年左右,开发的主干转移至了XFree86(从X11R6.3版本派生出来),现在由Thomas Dickey维护。
有许多xterm变体可用。大多数的X虚拟终端都是从xterm的变体起步的。
我们目前都基于xterm处理。
处理终端的输入
在go中,有许多方式可以获取用户的输入,通过Scan获取输入、bufio标准输入,这里我们说一个常用的方式:bufio,实时读取键盘输入的每个字符。
func read( c chan rune){
buf := bufio.NewReader(os.Stdin)
for {
r, n, err := buf.ReadRune()
if err != nil {
err := fmt.Errorf("exit read stdin loop as err:%v"err)
log.Debugf(err.Error())
return
}
log.Debugf("receive input:%v, string:%s", r, string(r))
if n > 0 {
c <- r
}
}
}
这已经能够满足大部分场景下的需求了,但是没能真正满足终端的需求,在命令过长导致换行的时候,这种做法会导致命令被截断,而不能有效被读取。
这里提供另外一种,可以更好的控制输入的力度内容:
func read (cb chan byte[]) {
var ttystate *unix.Termios
//Get terminal settings
ttystate, err := termios.Tcgetattr(uintptr(syscall.Stdin))
if err != nil {
log.Debugf(err.Error())
return
}
setNonCanonicalMode(ttystate)
buf := make([]byte, 1024)
for {
if n, err := syscall.Read(syscall.Stdin, buf); err == nil {
cb <- buf[:n]
}
}
}
func setNonCanonicalMode(attr *unix.Termios) {
//Disable canonical mode (canonical mode disabled)&^ AND NOT)
attr.Lflag &^= syscall.ICANON
//Minimum number of characters when reading=One character
attr.Cc[syscall.VMIN] = 1
//Timeout time when reading non-canonical= 0
attr.Cc[syscall.VTIME] = 0
//Reflect the changed settings
termios.Tcsetattr(uintptr(syscall.Stdin), termios.TCSANOW, attr)
}
通过实时获取用户输入,就可以实现客户端的逻辑
处理终端的输出
终端的输出是相对比较复杂的,与输入不同,终端输出带有大量的控制字符
我目前没有发现一个标准的表格对照,这也困扰了我很久,因为无法解析输出的内容,会导致无法实现命令白名单功能。无法对命令有效的拦截,经常误杀命令。
最后在github,找到这个类库,基本能够实现解析终端的输出为最终的命令 terminal parse
使用方法也很简单,在用户输入之后,将终端返回的内容缓存起来,当用户敲回车时,将缓存的内容使用这个解析器解析即可得到,这个解析器无法处理命令过长导致换行的问题,所以在处理之前需要将终端的换行符\r
处理掉
func parse(p []byte) string {
defer func() {
if r := recover(); r != nil {
log.Logger.Warnf(" panic: %s\n", r)
}
}()
s := terminalparser.Screen{
Rows: make([]*terminalparser.Row, 0, 1024),
Cursor: &terminalparser.Cursor{},
}
ss := s.Parse(p)
return strings.Join(ss, "")
}
以上,便是我在处理终端问题时,遇到的一些棘手的事情!感谢阅读!
本博客所有文章除特别声明外,均采用: 署名-非商业性使用-禁止演绎 4.0 国际协议,转载请保留原文链接及作者。