TermiShell : un shell basé sur le web pour le Raspberry Pi (notes de développement)

Introduction

Au cours du développement de PiCockpitje vais ajouter un terminal basé sur le web appelé TermiShell.

image

Icône TermiShell, par : Stephanie Harvey via unsplash.com

TermiShell va vous permettre de vous connecter à votre Raspberry Pi en utilisant PiCockpit.com (et le picockpit-client) - aucune application supplémentaire n'est requise de part et d'autre. Cela devrait être très confortable, surtout en déplacement.

TermiShell ne sera pas disponible dans la prochaine version 2.0 de PiCockpit.car cela nécessite un travail de préparation supplémentaire et retarderait considérablement la prochaine version. Je préférerais également ne pas faire d'économies sur la sécurité (ce qui serait l'alternative pour que cela fonctionne tout de suite).

Ce travail aura toutefois un impact positif sur de nombreuses autres fonctionnalités de PiCockpit, par exemple la possibilité de diffuser des vidéos (à partir de la caméra du Pi), le téléchargement de fichiers à partir du Pi, et de nombreuses autres fonctionnalités qui nécessitent des flux de données.

Je compile des informations pour moi-même, et aussi pour d'autres développeurs intéressés, sur mes réflexions sur la façon de réaliser un tel terminal basé sur le web, et des informations de fond.

Qu'est-ce qu'un pseudo-terminal (pty) ?

Python offre des fonctionnalités intégrées pour exécuter des processus, et pour capturer leurs sorties (stdout et stderr) et leur envoyer des entrées (stdin).

Voici une première tentative avec le sous-processus :

# dow, jour de travail 1.5.2020
import os
importer sys
temps d'importation
importation de filetage
import contextlib
Importation de sous-processus

print("Bonjour le monde !")
print("Running omxplayer")

def output_reader(proc) :
     contFlag = True
     alors que contFlag :
         chars = proc.stdout.read(1)
         si chars != None :
             si chars != b" :
                 print(chars.decode('utf-8'), end="", flush=True)
             autre :
                 contFlag = False

proc = subprocess.Popen(
     ['omxplayer', '/home/pi/thais.mp4'],
     stdin=sous-processus.PIPE,
     stdout=sous-processus.PIPE,
     stderr=sous-processus.PIPE,
     bufsize=0)

t = threading.Thread(target=output_reader, args=(proc,))
t.start()

sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', buffering=0)

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

proc.stdin.write(b'z')

t.join()

Veuillez noter ce qui suit dans le code ci-dessus (qui n'est pas parfait, juste un code de démonstration - et en fait pourrait ne pas fonctionner comme prévu !)

  • bufsize est fixé à 0 - pour supprimer la mise en mémoire tampon
  • J'ai mis en place un fil de discussion pour lire la sortie du processus caractère par caractère.
  • J'ai configuré stdin pour qu'il n'y ait pas de mise en mémoire tampon. Cela nécessite qu'il soit ouvert en mode binaire (rb).
  • Je lis les caractères un par un à partir de l'entrée de l'utilisateur. Je les répercute en utilisant flush=True (sinon la sortie est tamponnée par ligne dans Python par défaut pour print)
  • J'écris le personnage au processus. Rappelez-vous, il n'est pas mis en mémoire tampon car nous avons configuré bufsize=0

Malgré cela, nous nous heurtons à la situation suivante : la sortie de l'application (dans ce cas omxplayer), n'est pas reçue caractère par caractère, comme prévu - elle est plutôt vidée en une seule fois, à la sortie.

Même si la mise en mémoire tampon est réglée sur 0, pourquoi ?

tampons et processus interactifs

Les tampons stdio de Linux. Il est également intelligent à ce niveau. Si le processus est pas connecté à un processus interactif (terminal), mais à un pipe, la sortie est mise en mémoire tampon jusqu'à ce que le tampon soit rempli.

Il est ensuite efficacement copié sur l'autre application.

Il s'agit d'un comportement efficace et économe en ressources pour de nombreux cas d'utilisation. Mais pas si vous essayez de contrôler l'application de manière interactive.

Vous, le développeur, ne pouvez rien faire pour influencer le comportement de l'autre application lorsqu'elle vous parle par le biais d'un tuyau.

Vous devrez recompiler l'autre application (et ajuster manuellement le comportement de la mise en mémoire tampon).

Voici d'autres ressources sur ce sujet :

Les humains - et le verrouillage des applications si aucune sortie n'est fournie immédiatement - ont évidemment besoin d'une sortie interactive. C'est là qu'intervient le pseudoterminal.

Le pseudo-terminal

Le pseudo-terminal simule un terminal interactif pour l'application. stdio pense qu'il parle à un humain, et ne met pas de tampon. La sortie est telle que vous l'attendez en interagissant avec les applications Linux en ligne de commande (par exemple via SSH).

image

image

Comme vous pouvez le voir dans la sortie de ps aux, certaines applications n'ont pas de TTY (terminal) qui leur est assigné (apparaissant avec un point d'interrogation " ?") - dans ce cas, attendez-vous à ce que les applications montrent un comportement différent de mise en mémoire tampon par défaut.

Les pseudo-terminaux se présentent comme suit dans ps aux :

image

Je vais décoder l'information pour vous :

  • sshd se connecte au pseudoterminal 0 (pts/0).
  • bash, et quelques autres processus sont lancés sur le pseudo-terminal 0 (pts/0)
  • J'utilise sudo su pour lancer une commande en tant que root (qui à son tour lance su, puis bash) : python3 ttt.py
  • ce script (que je vous montrerai dans un petit moment) crée un nouveau pseudo-terminal pts/1
  • Je cours /bin/login de mon script pour vérifier les informations d'identification de l'utilisateur. Comme je les ai saisis correctement, bash (le shell par défaut) est lancé sur pts/1
  • ici je ping miau.de - ce processus est également exécuté dans pts/1
  • Je lance également une deuxième connexion SSH, qui se connecte à pts/2 - dans ce cas, pour exécuter ps aux, afin de pouvoir créer la capture d'écran ci-dessus.

Voici à quoi ressemble la première connexion SSH :

image

(Note au lecteur avisé : J'ai essayé deux fois, d'où le premier ping vers google)

image

Pour en savoir plus :

Backend Python et plus sur les pseudo-terminaux

Python possède des utilitaires intégrés pour le pseudo-terminal : pty. J'ai trouvé qu'il était difficile d'entrer dans la documentation, ce à quoi se réfère un enfant / maître / esclave.

J'ai trouvé un exemple icidans le  Appeler code source. Les "bonnes choses" commencent à la ligne 1242 (def start - sans jeu de mots Sourire). Comme vous pouvez le voir, il y a une référence à pexpect.spawn qui fait des choses supplémentaires pour l'installation et le démontage.

Par conséquent, j'ai simplement décidé d'utiliser pexpecter comme une bibliothèque d'enveloppe.

pexpecter

La documentation de pexpect peuvent être trouvés ici. Son principal cas d'utilisation est l'automatisation des applications interactives.

Pensez, par exemple, à un client FTP en ligne de commande. Il peut également être utilisé pour automatiser les tests d'applications.

pexpect crée un pseudo-terminal pour les applications qu'il lance.

Comme nous l'avons vu plus haut, le pseudoterminal présente l'avantage considérable et nécessaire de placer ces applications dans un mode de mise en mémoire tampon différent de celui auquel nous sommes habitués dans les applications interactives.

Les pseudo-terminaux se comportent comme de vrais terminaux - ils ont une taille d'écran (nombre de colonnes et de lignes) et les applications y écrivent des séquences de contrôle pour affecter l'affichage.

Taille de l'écran et SIGWINCH

Linus Akesson a une bonne explication sur les ATS en généralet aussi sur SIGWINCH. SIGWINCH est un signal, similaire à SIGHUP ou SIGINT.

Dans le cas de SIGWINCH, il est envoyé au processus fils pour l'informer de tout changement de taille du terminal.

Cela peut se produire, par exemple, si vous redimensionnez votre fenêtre PuTTY.

Les éditeurs comme nano et d'autres (par exemple alsamixer, ...) qui utilisent le plein écran et interagissent avec lui, ont besoin de connaître la taille de l'écran pour faire leurs calculs correctement et rendre la sortie correctement.

Ainsi, ils écoutent ce signal, et s'il arrive, ils réinitialisent leurs calculs.

image

Comme vous pouvez le voir dans cet exemple en plein écran, pexpect définit une taille d'écran plus petite que l'espace réellement disponible dans PuTTY - la taille de la sortie est donc limitée.

Cela nous amène à :

Séquences de contrôle (codes d'échappement ANSI)

Comment est-il possible de colorer la sortie sur la ligne de commande ?

image

Comment est-il possible pour les applications d'afficher des barres de progression interactives sur la ligne de commande ? (par exemple, wget) :

image

Cela se fait à l'aide de séquences de contrôle, incorporé dans la sortie de l'application. (Il s'agit d'un exemple d'informations en bande communiquées en plus, tout comme les compagnies de téléphone avaient l'habitude de signaler les informations de facturation, etc. en bande également, ce qui permettait aux pirates d'abuser du système).

Ceci, par exemple, produira une nouvelle ligne : \r\n

\r est pour carriage return, il déplace le curseur au début de la ligne sans avancer à la ligne suivante.

\N le saut de ligne, déplace le curseur à la ligne suivante.

Oui, même sous Linux - dans les fichiers, un \n seul signifie généralement une nouvelle ligne et implique un retour de chariot, mais pour que la sortie de l'application soit interprétée correctement par l'application terminal, \r\n est la sortie réelle pour aller à la ligne suivante !

Ce message est émis par le pilote de périphérique TTY.

Pour en savoir plus sur ce comportement, consultez la documentation de pexpect..

Pour créer du texte en couleur ou déplacer le curseur à n'importe quel endroit de l'écran, on peut utiliser des séquences d'échappement.

Ces séquences d'échappement commencent par l'octet ESC (27 / hex 0x1B / oct 033)et sont suivis d'un deuxième octet compris entre 0x40 et 0x5F (ASCII @A-Z[\]^_ ).

l'une des séquences les plus utiles est l'introducteur de séquence de contrôle [ (ou séquences CSI) (ESC est suivi de [ dans ce cas).

Ces séquences peuvent être utilisées pour positionner le curseur, effacer une partie de l'écran, définir les couleurs, et bien plus encore.

Ainsi, vous pouvez définir la couleur de votre invite dans le fichier ~/.bashrc comme ceci :

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

C'était des "incantations magiques" pour moi avant de comprendre ce sujet. Vous pouvez maintenant reconnaître les séquences de contrôle qui dirigent directement votre interprétation de PuTTY.

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

Dans ce cas, \[ et \] sont liés à la bash

La séquence de contrôle est à l'intérieur : \033 est pour ESC (en représentation octale), puis nous avons le [, et enfin 01;32m est la séquence réelle qui définit la couleur de premier plan.

Pour en savoir plus :

Code réel pexpect

Voici quelques extraits utiles du code de pexpect (attention, pexpect doit d'abord être installé sur votre système de la manière habituelle, voir la documentation de pexpect).

import pexpect
temps d'importation

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

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

child.delaybeforesend = None

# ceci envoie une touche flèche droite
child.send("\033[C")

enfant.interact()

maxread=1 définit la mise en mémoire tampon comme nulle.

Important : Nous devrions définir le delaybeforesend sur None, car nous allons canaliser les données réelles des utilisateurs à travers pexpect, qui a des délais intégrés par nature ; et nous ne voulons pas augmenter la latence inutilement !

Pour une utilisation réelle non interactive (principal cas d'utilisation de pexpect), la valeur par défaut est recommandée.

interact() affichera directement la sortie de l'application, et enverra les entrées de l'utilisateur à l'application.

Remarque : pour l'exemple en direct ci-dessus, child.interact() a été exécuté directement après l'instruction pexpect.spawn().

Web-frontend

L'intrigue s'épaissit. Ce dont nous avons besoin de l'autre côté, c'est d'une application JavaScript capable de comprendre et de restituer ces séquences de contrôle. (J'ai aussi vu qu'on les qualifie de compatibles avec le VT-100).

Mes recherches m'ont conduit à xterm.js

Il promet la performance, la compatibilité, le support de l'unicode, l'autonomie, etc.

Il est expédié par npm, ce qui est bien pour mon nouveau flux de travail pour picockpit-frontend.

De nombreuses applications sont basées sur xterm.js, notamment Microsoft Visual Studio Code.

Transport

Les composants étant prêts des deux côtés, la seule chose qui manque est le transport. Ici, nous devons parler de la latence, et de ce que les utilisateurs attendent.

Comme il s'agit d'une application interactive, nous devrons envoyer les caractères que l'utilisateur tape un par un au backend - qui devra (en fonction de l'application active) renvoyer immédiatement ces caractères.

Aucune mise en mémoire tampon n'est possible ici. La seule chose possible est que l'application envoie d'autres données en tant que réponse, ou par elle-même, que nous pouvons envoyer comme un paquet de données.

Mais l'entrée de l'utilisateur doit être diffusée caractère par caractère.

Ma première idée était d'envoyer des messages MQTT individuels et dépouillés.

Mais cela entre en conflit avec la vie privée de l'utilisateur.

Même si les données passent par des WebSockets (et donc https), les messages passent en clair par le broker MQTT de picockpit.com (VerneMQ).

Les interactions du shell comprendront des mots de passe et des données sensibles - un canal sécurisé DOIT donc être établi.

Actuellement, je pense utiliser les websockets, et éventuellement une sorte de bibliothèque pour cela, pour multiplexer plusieurs flux de données différents à travers une connexion chacun.

Le transport est la raison pour laquelle j'ai retardé TermiShell jusqu'à la version 3.0 de PiCockpit - des outils supplémentaires ont été ajoutés au transport, ce qui permet de réutiliser le transport pour d'autres applications et cas d'utilisation.

Cela pourrait être intéressant :