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
输入subprocess

print("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 = False

proc = 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设备驱动程序输出的。

在pexpect文档中阅读更多关于这种行为的信息.

要创建彩色文本,或将光标移动到屏幕上的任何一点,可以使用转义序列。

这些转义序列以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版本的原因--额外的工具已经进入传输,允许传输被重用在其他应用和用例。

可能这可能是有趣的。