TermiShell: 用于Raspberry Pi的基于网络的外壳(开发说明)。
简介
在发展的过程中 码头,我将添加一个基于网络的终端,名为 终端设备.
TermiShell图标,由。 斯蒂芬妮-哈维 通过unsplash.com
TermiShell将允许你使用PiCockpit.com(和picockpit-client)登录到你的Raspberry Pi中 - 双方都不需要额外的应用程序。这应该是非常舒适的,尤其是在旅途中。
TermiShell不会在即将发布的PiCockpit v2.0版本中发布。,因为这需要额外的准备工作,而且会大大推迟即将发布的版本。我也不愿意在安全方面偷工减料(这将是使其立即发挥作用的替代方案)。
然而,这项工作也将对PiCockpit的许多其他功能产生积极影响--例如,流式视频(来自Pi摄像头)、从Pi上传/下载文件以及其他许多需要数据流的功能的能力。
我正在为自己,也为其他感兴趣的开发者编纂资料,介绍我对如何实现这样一个基于网络的终端的想法,以及背景资料。
什么是伪终端(pty)?
Python 提供了内置的功能来执行进程,并捕获它们的输出 (stdout 和 stderr) 和发送它们的输入 (stdin)。
这里是用子进程进行的第一次尝试。
# dow, 工作日 1.5.2020
输入os
输入sys
进口时间
输入线程
输入contextlib
输入subprocessprint("Hello world!")
print("Running omxplayer")def output_reader(proc):
contFlag = True
而contFlag。
chars = proc.stdout.read(1)
如果chars != None:
如果chars != b"。
print(chars.decode('utf-8'), end="", flush=True)
否则。
contFlag = Falseproc = subprocess.Popen(
['omxplayer', '/home/pi/thais.mp4']。
stdin=subprocess.PIPE。
stdout=subprocess.PIPE。
stderr=subprocess.PIPE。
bufsize=0)t = threading.Thread(target=output_reader, args=(proc,) )
t.start()sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', buffering=0)
虽然是真的。
char = sys.stdin.read(1)
print(char.decode('utf-8'), end="", flush=True)
proc.stdin.write(char)proc.stdin.write(b'z')。
t.join()
请注意上述代码中的以下内容(这并不完美,只是演示代码--事实上可能不会像预期那样工作!)。
- bufsize被设置为0--以抑制缓冲。
- 我建立了一个线程来逐个读取进程的输出字符
- 我将stdin设置为零缓冲。这又要求它以二进制模式(rb)打开。
- 我从用户的输入中一个一个地读取字符。我使用flush=True对它们进行回显 (否则在Python中默认打印时输出是行缓冲的)
- 我把人物写到过程中。请记住。 它没有被缓冲,因为我们设置了bufsize=0
即便如此,我们还是遇到了以下情况:从应用程序(在这种情况下是omxplayer)输出的数据,并没有像预期的那样一个字符一个字符地接收--而是在退出时一次性地倾倒出来。
即使缓冲设置为0,为什么?
缓冲和互动过程
Linux的stdio缓冲区。它在这方面也很聪明。如果该进程是 不是 连接到一个交互式进程(终端),而是连接到一个管道。 输出是缓冲的 直到缓冲区被填满。
然后,它被有效地复制到其他应用程序。
这对很多使用情况来说是很好的、节省资源的行为。但是,如果你试图以交互方式控制应用程序,就不是这样了。
当你通过管道与你交谈时,你,即开发者,也没有办法影响其他应用程序的行为。
你需要重新编译其他应用程序(并手动调整缓冲行为)。
这里有一些关于这个主题的进一步资源。
人类--以及如果没有立即提供输出的应用程序锁定--显然需要交互式输出。这就是伪终端的作用。
伪终端
伪终端模拟应用程序的交互式终端,stdio认为它是在与人对话,并且不做缓冲。输出与你在命令行上与Linux应用程序交互时一样(例如,通过SSH)。
正如你在ps aux的输出中看到的,一些应用程序没有分配给它们的TTY(终端)(显示为问号"?")--在这种情况下,希望应用程序显示不同的默认缓冲行为。
伪终端在ps aux中是这样的。
我将为你解读这些信息。
- sshd连接到伪终端0(pts/0)。
- bash和其他一些进程在伪终端0(pts/0)上启动。
- 我使用sudo su以root身份运行一个命令(反过来运行su,然后是bash): python3 ttt.py
- 这个脚本(我一会儿就会给你看)。 创造 一个新的伪终端PTS/1
- 我跑 /bin/login 从我的脚本中检查用户凭证。因为我正确地输入了它们,bash(默认的shell)在pts/1上被启动。
- 我在这里ping miau.de - 这个过程也在pts/1中执行。
- 我还启动了第二个SSH连接,它连接到pts/2 - 在这种情况下,运行ps aux,以便能够创建上面的截图。
这就是第一个SSH连接的样子。
(请精明的读者注意。我试了两次,因此首先向谷歌ping了一下)
进一步阅读。
伪基站的Python后端及更多信息
Python有内置的用于伪终端的工具。 ǞǞǞ.我发现文件很难进入,孩子/主人/奴隶指的是什么。
我发现了一个例子 这里,在 启用 源代码。"好东西 "从第1242行开始(def start - 无双关之意 ).正如你所看到的,有一个参考pexpect.spawn为设置和拆除做额外的事情。
因此,我干脆决定使用 期待 作为一个包装库。
期待
pexpect的文档 可以在这里找到.它的主要用例是使交互式应用程序自动化。
想想看,例如,一个命令行FTP客户端。它也可以用于自动测试应用程序。
pexpect为它所启动的应用程序创建一个假终端。
正如上面所讨论的,伪终端有一个很大的也是必须的优势,那就是把这些应用放到一个不同的缓冲模式中,而我们习惯于从交互式应用中获得缓冲。
伪终端的行为与真正的终端一样--它有一个屏幕尺寸(列数和行数),应用程序向它写入控制序列以影响显示。
屏幕尺寸和SIGWINCH
Linus Akesson 对一般的TTY有一个很好的解释,也是关于SIGWINCH的。SIGWINCH是一种信号,类似于SIGHUP或SIGINT。
在SIGWINCH的情况下,每当终端尺寸发生变化时,它就会被发送给子进程以通知它。
这可能发生,例如,如果你调整你的PuTTY窗口大小。
像nano和其他使用全屏并与之互动的编辑器(例如alsamixer,...),需要知道屏幕的大小,以便正确地进行计算并正确地渲染输出。
因此,他们倾听这个信号,如果它到来,就会重置他们的计算。
从这个全屏的例子可以看出,pexpect设置的屏幕尺寸比我在PuTTY中的实际可用空间要小--因此输出尺寸是有限的。
这使我们想到。
控制序列(ANSI转义代码)。
如何在命令行上给输出着色?
应用程序如何可能在命令行上显示交互式进度条?(例如wget)。
这是用控制序列完成的。 嵌入 在应用程序的输出中。(这是一个带内信息被额外传达的例子,就像电话公司曾经在带内发出计费信息等信号一样,允许窃听者滥用该系统!)
例如,这将产生一个新行。\r\n
\r是指回车,它将光标移到该行的开头而不前进到下一行。
\换行,将光标移到下一行。
是的,甚至在Linux中--在文件中,通常单独的\n意味着换行,并意味着回车,但为了使应用程序的输出能够被 终端, \r\n是实际输出到下一行!
这是由TTY设备驱动程序输出的。
要创建彩色文本,或将光标移动到屏幕上的任何一点,可以使用转义序列。
这些转义序列以ESC字节开始 (27 / hex 0x1B / oct 033),后面是第二个字节,范围是0x40 - 0x5F(ASCII @A-Z[\]^_ )。
最有用的序列之一是[控制序列引入器(或CSI序列)(在这种情况下ESC后面是[)。
这些序列可以用来定位光标,擦除屏幕的一部分,设置颜色,以及更多。
因此,你可以在~/.bashrc中这样设置提示的颜色。
PS1=’${debian_chroot:+($debian_chroot)}\t \[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;37m\]\w \$\[\033[00m\] ‘
在我理解这个主题之前,这对我来说曾经是 "神奇的咒语"。你现在可以识别控制序列,它直接驱动你的PuTTY解释
- \[\033[01;32m\]
在这种情况下, \[和 \]是与bash
控制序列就在里面。\033是ESC(八进制表示),然后我们有[,然后01;32m是设置前景颜色的实际序列。
进一步阅读。
实际预期的代码
这里有一些有用的pexpect代码片段(注意,pexpect需要先以常规方式安装在你的系统上,见pexpect文档)。
输入pexpect
进口时间child = pexpect.spwn(['login'], maxread=1)
时间.睡眠(0.5)
print(child.read_nonblocking(size=30, timeout=0))child.delaybeforesend = None
#,这将发送一个右方向键
child.send("\033[C")child.interactive()
maxread=1将缓冲设置为零。
重要提示:我们会将delaybeforesend设置为None,因为我们将通过pexpect输送真实的用户输入,而pexpect本质上有内置的延迟;我们不希望不必要地增加延迟。
对于实际的非交互式使用(pexpect的主要使用情况),推荐使用默认值。
interact()将直接显示应用程序的输出,并将你的用户输入发送到应用程序。
请注意:对于上面的活生生的例子,child.interactive()直接在pexpect.spoon()语句之后运行。
Web-frontend
情节更加复杂了。我们在另一边需要的是一个能够理解和呈现这些控制序列的JavaScript应用程序。(我也看到它们被称为VT-100兼容)。
我的研究使我发现 xterm.js
它承诺了性能、兼容性、支持unicode、自成一体等。
它是通过npm发货的,这对我的picockpit-frontend的新工作流程很有利。
许多应用程序都是基于xterm.js的,包括微软Visual Studio Code。
运输
两边的组件都准备好了,唯一缺少的就是传输。在这里,我们必须谈谈延迟问题,以及用户的期望。
由于这是一个交互式的应用程序,我们将需要把用户输入的字符一个一个地发送到后台--后台应该(取决于活动的应用程序)立即回传这些字符。
这里不可能有缓冲--唯一可能的是,应用程序将发送更多的数据作为回复,或自行发送,我们可以将其作为一个数据包发送。
但用户的输入必须是逐个字符的流式输入。
我最初的想法是发送单独的、精简的MQTT消息。
但这与用户的隐私相冲突。
尽管数据是通过WebSockets运行的(因此是https),但消息通过picockpit.com的MQTT代理(VerneMQ)是未加密的。
外壳互动将包括密码和敏感数据 - 因此必须建立一个安全通道。
目前我正在考虑使用websockets,可能还有某种库,通过一个连接复用几个不同的数据流。
传输位是我将TermiShell推迟到PiCockpit的3.0版本的原因--额外的工具已经进入传输,允许传输被重用在其他应用和用例。
可能这可能是有趣的。