TermiShell: een webgebaseerde schil voor de Raspberry Pi (ontwikkelingsnota's)

Inleiding

In de loop van de ontwikkeling van PiCockpitga ik een web-gebaseerde terminal toevoegen genaamd TermiShell.

afbeelding

TermiShell pictogram, door: Stephanie Harvey via unsplash.com

TermiShell gaat u toelaten om in te loggen op uw Raspberry Pi met PiCockpit.com (en de picockpit-client) - geen extra toepassing nodig aan beide zijden. Dit zou zeer comfortabel moeten zijn, vooral wanneer u onderweg bent.

TermiShell zal niet worden vrijgegeven in de komende v2.0 versie van PiCockpitomdat het extra voorbereidend werk vereist, en de komende release aanzienlijk zou vertragen. Ik zou ook liever niet bezuinigen op veiligheid (wat het alternatief zou zijn om het meteen te laten werken).

Het werk zal echter ook een positieve invloed hebben op vele andere mogelijkheden van PiCockpit - bijvoorbeeld de mogelijkheid om video te streamen (van de Pi camera), bestanden te uploaden/downloaden van de Pi, en vele andere functionaliteiten die datastromen vereisen.

Ik verzamel informatie voor mezelf, en ook voor andere geïnteresseerde ontwikkelaars over mijn gedachten over hoe zo'n web-gebaseerde terminal te realiseren, en achtergrond informatie.

Wat is een pseudo-terminal (pty)?

Python biedt ingebouwde functionaliteiten om processen uit te voeren, en hun uitvoer op te vangen (stdout en stderr) en hun invoer te sturen (stdin).

Hier is een eerste poging met subproces:

# dag, dag van het werk 1.5.2020
os importeren
importeren sys
importtijd
importeren threading
import contextlib
importeren subproces

print("Hallo wereld!")
print("Running omxplayer")

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

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

proc.stdin.write(b'z')

t.join()

Let op het volgende in de bovenstaande code (die niet perfect is, gewoon demo code - en in feite misschien niet werkt zoals verwacht!):

  • bufsize is ingesteld op 0 - om buffering te onderdrukken
  • Ik heb een draad opgezet om de uitvoer van het proces karakter voor karakter te lezen
  • Ik heb stdin ingesteld op nul buffering. Dit vereist op zijn beurt dat het geopend wordt in binaire modus (rb)
  • Ik lees karakters één voor één uit de invoer van de gebruiker. Ik echo ze met flush=True (anders is de uitvoer standaard line-buffered in Python voor print)
  • Ik schrijf het karakter naar het proces. Onthoud, het is niet gebufferd omdat we bufsize=0 hebben ingesteld

Desondanks stuiten we op de volgende situatie: uitvoer van de toepassing (in dit geval omxplayer), wordt niet karakter voor karakter ontvangen, zoals verwacht - in plaats daarvan wordt alles in één keer gedumpt, bij afsluiten.

Ook al is buffering op 0 gezet. Waarom?

buffering en interactieve processen

Linux stdio buffers. Het is hier ook slim in. Als het proces niet verbonden met een interactief proces (terminal), maar met een pijp, output wordt gebufferd totdat de buffer gevuld is.

Daarna wordt het efficiënt gekopieerd naar de andere toepassing.

Dit is goed, bron-efficiënt gedrag voor veel gebruikssituaties. Maar niet als je probeert om de toepassing interactief te besturen.

Er is ook niets dat u, de ontwikkelaar, kunt doen om het gedrag van de andere toepassing te beïnvloeden wanneer die met u praat via een pijp.

U zou de andere toepassing opnieuw moeten compileren (en handmatig het buffergedrag moeten aanpassen).

Hier zijn nog enkele bronnen over dit onderwerp:

Mensen - en toepassingen die vergrendelen als er niet onmiddellijk uitvoer wordt geleverd - hebben uiteraard interactieve uitvoer nodig. Dit is waar de pseudoterminal om de hoek komt kijken.

De pseudoterminal

De pseudoterminal simuleert een interactieve terminal voor de applicatie. stdio denkt dat het met een mens praat, en buffert niet. De uitvoer is zoals je zou verwachten van interactie met Linux applicaties op de commandoregel (bv. via SSH).

afbeelding

afbeelding

Zoals je kunt zien in de uitvoer van ps aux, hebben sommige applicaties geen TTY (terminal) toegewezen gekregen (te zien met een vraagteken "?") - verwacht in dit geval dat de applicaties een ander standaard buffergedrag laten zien.

Pseudoterminals zien er als volgt uit in ps aux:

afbeelding

Ik zal de informatie voor je ontcijferen:

  • sshd maakt verbinding met pseudoterminal 0 (pts/0).
  • bash, en enkele andere processen worden gestart op pseudoterminal 0 (pts/0)
  • Ik gebruik sudo su om een commando als root uit te voeren (die op zijn beurt su uitvoert, en dan bash): python3 ttt.py
  • dit script (dat ik je zo dadelijk zal laten zien) creëert een nieuwe pseudoterminal pts/1
  • Ik ren. /bin/login van mijn script om de gebruikersgegevens te controleren. Omdat ik ze correct heb ingevoerd, wordt bash (de standaard shell) gestart op pts/1
  • hier ping ik miau.de - dit proces wordt ook uitgevoerd in pts/1
  • Ik start ook een tweede SSH verbinding, die verbinding maakt met pts/2 - in dit geval om ps aux uit te voeren, om het bovenstaande screenshot te kunnen maken.

Zo ziet de eerste SSH verbinding eruit:

afbeelding

(Noot voor de scherpzinnige lezer: Ik heb het twee keer geprobeerd, vandaar eerst de ping naar google)

afbeelding

Verder lezen:

Python backend & meer op pseudoterminals

Python heeft ingebouwde hulpprogramma's voor de pseudo-terminal: pty. Ik vond de documentatie moeilijk om in te komen, waar een kind / meester / slaaf naar verwijzen.

Ik vond een voorbeeld hier, in de  Roep op. broncode. Het "goede spul" begint in regel 1242 (def start - geen woordspeling bedoeld Glimlach). Zoals je kunt zien, is er een verwijzing naar pexpect.spawn die extra dingen doet voor setup en tear down.

Daarom heb ik gewoon besloten om pexpect als een omhulsel bibliotheek.

pexpect

De documentatie voor pexpect kan hier gevonden worden. Het wordt vooral gebruikt om interactieve toepassingen te automatiseren.

Denk bijvoorbeeld aan een command line FTP client. Het kan ook worden gebruikt om tests van toepassingen te automatiseren.

pexpect maakt een pseudoterminal voor de applicaties die het start.

Zoals hierboven besproken, heeft de pseudoterminal het grote en noodzakelijke voordeel dat deze toepassingen in een andere buffermodus worden geplaatst, die wij van interactieve toepassingen gewend zijn.

Pseudoterminals gedragen zich als echte terminals - het heeft een schermgrootte (aantal kolommen en rijen), en toepassingen schrijven er besturingsreeksen naar om het scherm te beïnvloeden.

Schermgrootte en SIGWINCH

Linus Akesson heeft een goede uitleg over TTY's in het algemeenen ook over SIGWINCH. SIGWINCH is een signaal, vergelijkbaar met SIGHUP of SIGINT.

In het geval van SIGWINCH, wordt het naar het kind-proces gestuurd om het te informeren wanneer de terminal grootte verandert.

Dit kan bijvoorbeeld gebeuren als u de grootte van uw PuTTY-venster wijzigt.

Editors zoals nano en andere (bv. alsamixer, ...) die het volledige scherm gebruiken en ermee interageren, moeten de schermgrootte kennen om hun berekeningen correct uit te voeren en de uitvoer correct weer te geven.

Dus luisteren ze naar dit signaal, en als het aankomt, resetten ze hun berekeningen.

afbeelding

Zoals je kunt zien in dit fullscreen voorbeeld, stelt pexpect een kleinere schermgrootte in dan mijn werkelijk beschikbare ruimte in PuTTY - daarom is de uitvoer beperkt.

Dit brengt ons bij:

Besturingssequenties (ANSI escape codes)

Hoe is het mogelijk om uitvoer op de opdrachtregel in te kleuren?

afbeelding

Hoe is het mogelijk voor applicaties om interactieve voortgangsbalken te tonen op de opdrachtregel? (b.v. wget):

afbeelding

Dit wordt gedaan met behulp van controlereeksen, embedded in de uitvoer van de toepassing. (Dit is een voorbeeld van in-band informatie die extra wordt doorgegeven, zoals de telefoonmaatschappijen vroeger ook factureringsinformatie e.d. in-band doorgaven, waardoor phreakers misbruik van het systeem konden maken)!

Dit, bijvoorbeeld, zal een nieuwe regel opleveren: \.

\r is voor carriage return, het verplaatst de cursor naar het begin van de regel zonder door te gaan naar de volgende regel.

\Regelinvoer, verplaatst de cursor naar de volgende regel.

Ja, zelfs in Linux - in bestanden betekent \n alleen een nieuwe regel en impliceert een carriage return, maar om de uitvoer van de toepassing correct te laten interpreteren door de terminalis de eigenlijke output om naar de volgende regel te gaan!

Dit wordt uitgevoerd door het TTY-apparaatstuurprogramma.

Lees meer over dit gedrag in de pexpect documentatie.

Om gekleurde tekst te maken, of om de cursor naar een willekeurig punt op het scherm te verplaatsen, kunnen escape-reeksen worden gebruikt.

Deze escape-reeksen beginnen met de ESC-byte (27 / hex 0x1B / oct 033)en worden gevolgd door een tweede byte in het bereik 0x40 - 0x5F (ASCII @A-Z[\]^_ )

een van de meest nuttige sequenties is de [ control sequence introducer (of CSI sequences) (ESC wordt in dit geval gevolgd door [).

Deze sequenties kunnen worden gebruikt om de cursor te positioneren, een deel van het scherm te wissen, kleuren in te stellen, en nog veel meer.

Dus, je kunt de kleur van je prompt in ~/.bashrc als volgt instellen:

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

Dit waren "magische bezweringen" voor mij voordat ik dit onderwerp begreep. Je kunt nu de controlereeksen herkennen, die je PuTTY interpretatie direct aansturen

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

In dit geval zijn [ en \] gerelateerd aan de bash

De controle-sequentie zit erin: \033 is voor ESC (in octale weergave), dan hebben we de [, en dan 01;32m is de eigenlijke sequentie die de voorgrondkleur instelt.

Verder lezen:

Werkelijke pexpect code

Hier zijn enkele nuttige fragmenten van pexpect code (let op, pexpect moet eerst op de gebruikelijke manier op uw systeem worden geïnstalleerd, zie de pexpect documentatie).

importeren pexpect
importtijd

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

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

child.delaybeforeend = None

# dit stuurt een rechter pijltjestoets
child.send("\033[C")

kind.interactie()

maxread=1 zet buffering op none.

Belangrijk: We zouden de delaybeforeend op None zetten, omdat we echte gebruikersinvoer door pexpect zullen laten lopen, dat van nature ingebouwde vertragingen heeft; en we willen de latentie niet onnodig verhogen!

Voor werkelijk niet-interactief gebruik (het belangrijkste gebruik van pexpect), wordt de standaardwaarde aanbevolen.

interact() zal de output van de toepassing rechtstreeks tonen, en de input van de gebruiker naar de toepassing sturen.

Let op: in het live voorbeeld hierboven werd child.interact() direct na het pexpect.spawn() statement uitgevoerd.

Web-frontend

De plot wordt dikker. Wat we aan de andere kant nodig hebben is een JavaScript applicatie die in staat is om deze controle sequenties te begrijpen en te renderen. (Ik heb ook gezien dat ze worden aangeduid als VT-100 compatible).

Mijn onderzoek heeft me geleid tot xterm.js

Het belooft prestaties, compatibiliteit, unicode-ondersteuning, op zichzelf staand zijn, enz.

Het wordt verscheept via npm, wat goed is voor mijn nieuwe workflow voor picockpit-frontend.

Veel toepassingen zijn gebaseerd op xterm.js, waaronder Microsoft Visual Studio Code.

Vervoer

Nu de componenten aan beide zijden klaar zijn, ontbreekt alleen nog het transport. Hier moeten we het hebben over latency, en wat gebruikers verwachten.

Aangezien dit een interactieve toepassing is, zullen we tekens die de gebruiker intypt één voor één naar de backend moeten sturen - die (afhankelijk van de actieve toepassing) deze tekens onmiddellijk terug moet echoën.

Hier is geen buffering mogelijk - het enige wat mogelijk is, is dat de applicatie meer gegevens stuurt als antwoord, of op zichzelf, die wij kunnen sturen als een pakket gegevens.

Maar de gebruikersinvoer moet karakter voor karakter worden gestreamd.

Mijn eerste gedachte hierbij was om individuele, uitgeklede MQTT berichten te sturen.

Maar dit is in strijd met de privacy van de gebruiker.

Hoewel de data via WebSockets (en dus https) loopt, gaan de berichten onversleuteld door de picockpit.com MQTT broker (VerneMQ).

Shell-interacties zullen wachtwoorden en gevoelige gegevens omvatten - er MOET dus een beveiligd kanaal tot stand worden gebracht.

Momenteel denk ik aan het gebruik van websockets, en mogelijk een soort bibliotheek daarvoor, om verschillende datastromen te multiplexen via telkens één verbinding.

Het transport is de reden dat ik TermiShell uitstel tot de 3.0 versie van PiCockpit - extra gereedschap is in het transport gaan zitten, waardoor het transport ook voor andere toepassingen en use-cases kan worden hergebruikt.

Misschien is dit wel interessant: