TermiShell: eine webbasierte Shell für den Raspberry Pi (Entwicklungsnotizen)

Einführung

Im Zuge der Entwicklung von PiCockpitfüge ich ein webbasiertes Terminal namens TermiShell.

Bild

TermiShell-Symbol, von: Stephanie Harvey über unsplash.com

TermiShell wird es Ihnen ermöglichen, sich über PiCockpit.com (und den picockpit-client) in Ihren Raspberry Pi einzuloggen - ohne zusätzliche Anwendungen auf beiden Seiten. Das ist vor allem für unterwegs sehr komfortabel.

TermiShell wird nicht in der kommenden v2.0 Version von PiCockpit enthalten sein.weil es zusätzliche Vorbereitungsarbeit erfordert und die bevorstehende Veröffentlichung erheblich verzögern würde. Außerdem möchte ich keine Abstriche bei der Sicherheit machen (was die Alternative wäre, damit es sofort funktioniert).

Die Arbeit wird sich jedoch auch auf viele andere Funktionen von PiCockpit positiv auswirken - zum Beispiel die Möglichkeit, Videos (von der Pi-Kamera) zu streamen, Dateien vom Pi hoch- und herunterzuladen und viele andere Funktionen, die Datenströme benötigen.

Ich stelle für mich selbst, aber auch für andere interessierte Entwickler Informationen über meine Überlegungen zur Realisierung eines solchen webbasierten Terminals und Hintergrundinformationen zusammen.

Was ist ein Pseudoterminal (pty)?

Python bietet eingebaute Funktionen, um Prozesse auszuführen, ihre Ausgaben (stdout und stderr) zu erfassen und ihnen Eingaben (stdin) zu senden.

Hier ist ein erster Versuch mit subprocess:

# dow, Tag der Arbeit 1.5.2020
importieren os
System einführen
Einfuhrzeit
Threading importieren
importieren contextlib
importieren subprocess

print("Hallo Welt!")
print("Omxplayer wird ausgeführt")

def output_reader(proc):
     contFlag = True
     while contFlag:
         chars = proc.stdout.read(1)
         if chars != None:
             if chars != b":
                 print(chars.decode('utf-8'), end="", flush=True)
             sonst:
                 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)

while True:
     char = sys.stdin.read(1)
     print(char.decode('utf-8'), end="", flush=True)
     proc.stdin.write(char)

proc.stdin.write(b'z')

t.join()

Bitte beachten Sie die folgenden Punkte im obigen Code (der nicht perfekt ist, sondern nur ein Beispielcode - und in der Tat möglicherweise nicht wie erwartet funktioniert!)

  • bufsize ist auf 0 gesetzt - um die Pufferung zu unterdrücken
  • Ich habe einen Thread eingerichtet, um die Ausgabe des Prozesses Zeichen für Zeichen zu lesen
  • Ich habe stdin so eingerichtet, dass es keine Pufferung hat. Dies wiederum erfordert, dass es im Binärmodus geöffnet wird (rb)
  • Ich lese ein Zeichen nach dem anderen aus der Benutzereingabe. Ich gebe sie mit flush=True als Echo aus (andernfalls wird die Ausgabe in Python standardmäßig für print zeilengepuffert)
  • Ich schreibe den Charakter zum Prozess. Erinnern Sie sich, es wird nicht gepuffert, weil wir bufsize=0 eingestellt haben

Dennoch kommt es zu folgendem Problem: Die Ausgaben der Anwendung (in diesem Fall omxplayer) werden nicht wie erwartet zeichenweise empfangen, sondern beim Beenden auf einmal ausgegeben.

Obwohl die Pufferung auf 0 eingestellt ist. Warum?

Pufferung und interaktive Prozesse

Linux stdio-Puffer. Auch darin ist es geschickt. Wenn der Prozess nicht mit einem interaktiven Prozess (Terminal) verbunden, sondern mit einer Pipe, Ausgabe wird gepuffert bis der Puffer gefüllt ist.

Dann wird es effizient in die andere Anwendung kopiert.

Für viele Anwendungsfälle ist dies ein gutes, ressourcenschonendes Verhalten. Aber nicht, wenn Sie versuchen, die Anwendung interaktiv zu steuern.

Es gibt auch nichts, was Sie als Entwickler tun können, um das Verhalten der anderen Anwendung zu beeinflussen, wenn sie über eine Pipe mit Ihnen kommuniziert.

Sie müssten die andere Anwendung neu kompilieren (und das Pufferungsverhalten manuell anpassen).

Hier finden Sie einige weitere Ressourcen zu diesem Thema:

Menschen - und Anwendungen, die sich sperren, wenn nicht sofort eine Ausgabe erfolgt - benötigen natürlich eine interaktive Ausgabe. An dieser Stelle kommt das Pseudoterminal ins Spiel.

Das Pseudoterminal

Das Pseudoterminal simuliert ein interaktives Terminal für die Anwendung. stdio denkt, dass es mit einem Menschen spricht, und puffert nicht. Die Ausgabe erfolgt so, wie Sie es von der Interaktion mit Linux-Anwendungen auf der Kommandozeile (z. B. über SSH) erwarten würden.

Bild

Bild

Wie Sie in der Ausgabe von ps aux sehen können, ist einigen Anwendungen kein TTY (Terminal) zugewiesen (wird mit einem Fragezeichen "?" angezeigt) - in diesem Fall sollten Sie davon ausgehen, dass die Anwendungen ein anderes Standardpufferverhalten aufweisen.

Pseudoterminals sehen in ps aux wie folgt aus:

Bild

Ich werde die Informationen für Sie entschlüsseln:

  • sshd verbindet sich mit dem Pseudoterminal 0 (pts/0).
  • bash und einige andere Prozesse werden auf dem Pseudoterminal 0 (pts/0) gestartet
  • Ich benutze sudo su, um einen Befehl als root auszuführen (der wiederum su und dann bash ausführt): python3 ttt.py
  • dieses Skript (das ich Ihnen in Kürze zeigen werde) erstellt ein neues Pseudoterminal pts/1
  • Ich laufe /bin/login aus meinem Skript, um die Benutzeranmeldedaten zu überprüfen. Da ich sie korrekt eingegeben habe, wird die bash (die Standardshell) auf pts/1 gestartet
  • hier pinge ich miau.de an - dieser Prozess wird auch in pts/1 ausgeführt
  • Ich starte auch eine zweite SSH-Verbindung, die sich mit pts/2 verbindet - in diesem Fall, um ps aux auszuführen, damit ich den obigen Screenshot erstellen kann.

So sieht die erste SSH-Verbindung aus:

Bild

(Hinweis für den aufmerksamen Leser: Ich habe es zwei Mal versucht, daher zuerst der Ping an Google)

Bild

Lesen Sie weiter:

Python-Backend & mehr zu Pseudoterminals

Python verfügt über eingebaute Hilfsprogramme für das Pseudoterminal: pty. Ich fand die Dokumentation schwer zu bekommen in, was ein Kind / Master / Sklave beziehen sich auf.

Ich habe ein Beispiel gefunden hierin der  Rufen Sie auf. Quellcode. Das "gute Zeug" beginnt in Zeile 1242 (def start - kein Wortspiel beabsichtigt Lächeln). Wie Sie sehen können, gibt es einen Verweis auf pexpect.spawn, das zusätzliche Aufgaben für das Einrichten und Abbauen übernimmt.

Daher habe ich beschlossen, einfach die erwarten als eine Wrapper-Bibliothek.

erwarten

Die Dokumentation für pexpect können hier gefunden werden. Sein Haupteinsatzgebiet ist die Automatisierung interaktiver Anwendungen.

Denken Sie zum Beispiel an einen FTP-Client für die Kommandozeile. Er kann auch dazu verwendet werden, Tests von Anwendungen zu automatisieren.

pexpect erstellt ein Pseudoterminal für die von ihm gestarteten Anwendungen.

Wie bereits erwähnt, hat das Pseudoterminal den großen und notwendigen Vorteil, dass diese Anwendungen in einen anderen Pufferungsmodus versetzt werden, den wir von interaktiven Anwendungen gewohnt sind.

Pseudoterminals verhalten sich wie echte Terminals - sie haben eine Bildschirmgröße (Anzahl der Spalten und Zeilen), und Anwendungen schreiben Steuersequenzen darauf, um die Anzeige zu beeinflussen.

Bildschirmgröße und SIGWINCH

Linus Akesson hat eine gute Erklärung zu TTYs im Allgemeinenund auch über SIGWINCH. SIGWINCH ist ein Signal, ähnlich wie SIGHUP oder SIGINT.

Im Falle von SIGWINCH wird es an den Kindprozess gesendet, um ihn zu informieren, wenn sich die Terminalgröße ändert.

Dies kann zum Beispiel passieren, wenn Sie die Größe Ihres PuTTY-Fensters ändern.

Editoren wie nano und andere (z.B. alsamixer, ...), die den Vollbildschirm nutzen und mit ihm interagieren, müssen die Bildschirmgröße kennen, um ihre Berechnungen korrekt durchzuführen und die Ausgabe richtig darzustellen.

Sie warten also auf dieses Signal, und wenn es eintrifft, setzen sie ihre Berechnungen zurück.

Bild

Wie Sie an diesem Vollbild-Beispiel sehen können, stellt pexpect eine kleinere Bildschirmgröße ein als mein tatsächlich verfügbarer Platz in PuTTY - daher ist die Ausgabegröße begrenzt.

Dies bringt uns zu:

Steuersequenzen (ANSI-Escape-Codes)

Wie ist es möglich, die Ausgabe auf der Kommandozeile zu färben?

Bild

Wie ist es möglich, dass Anwendungen interaktive Fortschrittsbalken in der Befehlszeile anzeigen? (z. B. wget):

Bild

Dies geschieht mit Hilfe von Steuersequenzen, eingebettet in der Ausgabe der Anwendung. (Dies ist ein Beispiel für die zusätzliche Übermittlung von In-Band-Informationen, ähnlich wie die Telefongesellschaften früher auch In-Band-Informationen über die Rechnungsstellung usw. übermittelten, was es Abhörern ermöglichte, das System zu missbrauchen!)

So wird zum Beispiel eine neue Zeile erzeugt: \r\n

\r steht für Wagenrücklauf und bewegt den Cursor an den Anfang der Zeile, ohne in die nächste Zeile zu springen.

\n Zeilenvorschub, bewegt den Cursor zur nächsten Zeile.

Ja, sogar unter Linux - in Dateien bedeutet \n allein normalerweise neue Zeile und impliziert einen Wagenrücklauf, aber damit die Anwendungsausgabe von der Terminal, \r\n ist die eigentliche Ausgabe, um zur nächsten Zeile zu gelangen!

Dies wird vom TTY-Gerätetreiber ausgegeben.

Lesen Sie mehr über dieses Verhalten in der pexpect-Dokumentation.

Um farbigen Text zu erstellen oder den Cursor an eine beliebige Stelle des Bildschirms zu bewegen, können Escape-Sequenzen verwendet werden.

Diese Escape-Sequenzen beginnen mit dem ESC-Byte (27 / hex 0x1B / oct 033)und gefolgt von einem zweiten Byte im Bereich 0x40 - 0x5F (ASCII @A-Z[\]^_ )

Eine der nützlichsten Sequenzen ist der [ Steuersequenzeinleiter (oder CSI-Sequenzen) (in diesem Fall folgt auf ESC ein [).

Diese Sequenzen können verwendet werden, um den Cursor zu positionieren, einen Teil des Bildschirms zu löschen, Farben einzustellen und vieles mehr.

So können Sie die Farbe Ihrer Eingabeaufforderung in der ~/.bashrc wie folgt einstellen:

PS1=’${debian_chroot:+($debian_chroot)}\t \[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;37m\]\w \$\[\033[00m\] ‘

Bevor ich dieses Thema verstanden habe, waren das für mich "Zauberformeln". Sie können jetzt die Steuersequenzen erkennen, die Ihre PuTTY-Interpretation direkt steuern

  • \[\033[01;32m\]

In diesem Fall beziehen sich \[ und \] auf die bash

Die Steuersequenz befindet sich im Inneren: \033 steht für ESC (in oktaler Darstellung), dann haben wir die [, und dann 01;32m ist die eigentliche Sequenz, die die Vordergrundfarbe einstellt.

Lesen Sie weiter:

Tatsächlicher Pexpect-Code

Hier sind einige nützliche Code-Schnipsel von pexpect (beachten Sie, dass pexpect zuerst auf Ihrem System auf die übliche Weise installiert werden muss, siehe pexpect-Dokumentation).

importieren pexpect
Einfuhrzeit

Kind = pexpect.spawn(['login'], maxread=1)

time.sleep(0.5)
print(child.read_nonblocking(size=30, timeout=0))

child.delaybeforeesend = Keine

# sendet eine Pfeiltaste nach rechts
child.send("\033[C")

Kind.interagieren()

maxread=1 setzt die Pufferung auf Null.

Wichtig: Wir würden die delaybeforesend auf None setzen, da wir echte Benutzereingaben durch pexpect leiten werden, das von Natur aus Verzögerungen eingebaut hat; und wir wollen die Latenz nicht unnötig erhöhen!

Für die tatsächliche nicht-interaktive Verwendung (Hauptanwendungsfall von pexpect) wird die Standardeinstellung empfohlen.

interact() zeigt die Ausgabe der Anwendung direkt an und sendet Ihre Benutzereingaben an die Anwendung.

Bitte beachten Sie: Im obigen Live-Beispiel wurde child.interact() direkt nach der Anweisung pexpect.spawn() ausgeführt.

Web-Frontend

Die Sache wird immer komplizierter. Was wir auf der anderen Seite brauchen, ist eine JavaScript-Anwendung, die in der Lage ist, diese Steuersequenzen zu verstehen und darzustellen. (Ich habe auch gesehen, dass sie als VT-100-kompatibel bezeichnet werden).

Meine Forschung hat mich zu folgenden Ergebnissen geführt xterm.js

Es verspricht Leistung, Kompatibilität, Unicode-Unterstützung, Eigenständigkeit, usw.

Es wird über npm ausgeliefert, was für meinen neuen Workflow für picockpit-frontend gut ist.

Viele Anwendungen basieren auf xterm.js, darunter auch Microsoft Visual Studio Code.

Transport

Nachdem die Komponenten auf beiden Seiten fertig sind, fehlt nur noch der Transport. Hier müssen wir über die Latenz sprechen und darüber, was die Nutzer erwarten.

Da es sich um eine interaktive Anwendung handelt, müssen wir die Zeichen, die der Benutzer eintippt, einzeln an das Backend senden, das diese Zeichen (je nach aktiver Anwendung) sofort zurückschicken sollte.

Hier ist keine Zwischenspeicherung möglich - das Einzige, was möglich ist, ist, dass die Anwendung weitere Daten als Antwort oder von sich aus sendet, die wir als Datenpaket senden können.

Die Benutzereingaben müssen jedoch Zeichen für Zeichen übertragen werden.

Mein erster Gedanke dabei war, einzelne, abgespeckte MQTT-Nachrichten zu senden.

Dies steht jedoch im Konflikt mit der Privatsphäre des Nutzers.

Obwohl die Daten über WebSockets (und damit https) laufen, gehen die Nachrichten unverschlüsselt durch den picockpit.com MQTT-Broker (VerneMQ).

Die Shell-Interaktionen umfassen Passwörter und sensible Daten - daher MUSS ein sicherer Kanal eingerichtet werden.

Derzeit denke ich an die Verwendung von Websockets, und möglicherweise eine Art von Bibliothek zusammen für sie, um mehrere verschiedene Datenströme durch eine Verbindung jeweils zu multiplexen.

Der Transport ist der Grund dafür, dass ich TermiShell bis zur Version 3.0 von PiCockpit verschiebe - zusätzliche Werkzeuge sind in den Transport eingeflossen, so dass der Transport auch für andere Anwendungen und Anwendungsfälle wiederverwendet werden kann.

Das könnte vielleicht interessant sein: