TermiShell: una shell basata sul web per il Raspberry Pi (note di sviluppo)

Introduzione

Nel corso dello sviluppo di PiCockpitHo intenzione di aggiungere un terminale basato sul web chiamato TermiShell.

immagine

Icona TermiShell, di: Stephanie Harvey via unsplash.com

TermiShell vi permetterà di accedere al vostro Raspberry Pi usando PiCockpit.com (e il picockpit-client) - nessuna applicazione aggiuntiva richiesta da entrambe le parti. Questo dovrebbe essere molto comodo, specialmente quando si è in viaggio.

TermiShell non sarà rilasciato nella prossima versione v2.0 di PiCockpit, perché richiede un ulteriore lavoro di preparazione, e ritarderebbe significativamente l'imminente rilascio. Preferirei anche non tagliare gli angoli sulla sicurezza (che sarebbe l'alternativa per farlo funzionare subito).

Il lavoro, tuttavia, avrà un impatto positivo anche su molte altre capacità di PiCockpit - per esempio la capacità di trasmettere video in streaming (dalla fotocamera del Pi), l'upload / download di file dal Pi, e molte altre funzionalità che richiedono flussi di dati.

Sto compilando informazioni per me stesso, e anche per altri sviluppatori interessati sui miei pensieri su come realizzare un tale terminale basato sul web, e informazioni di base.

Cos'è uno pseudo-terminale (pty)?

Python offre funzionalità integrate per eseguire processi, e per catturare il loro output (stdout e stderr) e inviare loro input (stdin).

Ecco un primo tentativo con subprocesso:

# dow, giorno di lavoro 1.5.2020
importare os
importazione sys
tempo di importazione
importare la filettatura
importare contextlib
importa subprocesso

stampa("Ciao mondo!")
stampa("Esecuzione di omxplayer")

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

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

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

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

mentre 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()

Si prega di notare quanto segue nel codice di cui sopra (che non è perfetto, solo codice dimostrativo - e infatti potrebbe non funzionare come previsto!)

  • bufsize è impostato a 0 - per sopprimere il buffering
  • Ho impostato un thread per leggere l'output del processo carattere per carattere
  • Ho impostato stdin per avere zero buffering. Questo a sua volta richiede di essere aperto in modalità binaria (rb)
  • Leggo i caratteri uno alla volta dall'input dell'utente. Li echo usando flush=True (altrimenti l'output è line-buffered in Python per default per la stampa)
  • Scrivo il personaggio al processo. Ricordate, non è bufferizzato perché abbiamo impostato bufsize=0

Anche così, ci imbattiamo nella seguente situazione: l'output dell'applicazione (in questo caso omxplayer), non viene ricevuto carattere per carattere, come previsto - piuttosto viene scaricato tutto in una volta, all'uscita.

Anche se il buffering è impostato su 0. Perché?

buffering e processi interattivi

buffer stdio di Linux. È intelligente anche in questo. Se il processo è non collegato ad un processo interattivo (terminale), ma ad una pipe, l'uscita viene bufferizzata finché il buffer non è stato riempito.

Poi viene copiato in modo efficiente nell'altra applicazione.

Questo è un buon comportamento, efficiente in termini di risorse, per molti casi d'uso. Ma non se si sta cercando di controllare interattivamente l'applicazione.

Non c'è anche niente che voi, lo sviluppatore, possiate fare per influenzare il comportamento dell'altra applicazione quando vi parla attraverso un tubo.

Dovreste ricompilare l'altra applicazione (e regolare manualmente il comportamento del buffering).

Qui ci sono altre risorse su questo argomento:

Gli esseri umani - e le applicazioni che si bloccano se non viene fornito immediatamente un output - hanno ovviamente bisogno di un output interattivo. È qui che entra in gioco lo pseudoterminale.

Lo pseudoterminale

Lo pseudoterminale simula un terminale interattivo per l'applicazione. stdio pensa di stare parlando con un umano, e non fa buffer. L'output è come ci si aspetterebbe dall'interazione con le applicazioni Linux sulla linea di comando (ad esempio tramite SSH).

immagine

immagine

Come si può vedere nell'output di ps aux, alcune applicazioni non hanno un TTY (terminale) assegnato a loro (mostrandosi con un punto interrogativo "?") - in questo caso, aspettatevi che le applicazioni mostrino un diverso comportamento di buffering di default.

Gli pseudoterminali hanno questo aspetto in ps aux:

immagine

Decodificherò le informazioni per voi:

  • sshd si connette allo pseudoterminale 0 (pts/0).
  • bash e alcuni altri processi sono avviati sullo pseudoterminale 0 (pts/0)
  • Uso sudo su per eseguire un comando come root (che a sua volta esegue su, e poi bash): python3 ttt.py
  • questo script (che vi mostrerò tra poco) crea un nuovo pseudoterminale pts/1
  • Corro /bin/login dal mio script per controllare le credenziali dell'utente. Poiché le ho inserite correttamente, bash (la shell predefinita) viene avviata su pts/1
  • qui ping miau.de - questo processo viene eseguito anche in pts/1
  • Avvio anche una seconda connessione SSH, che si collega a pts/2 - in questo caso per eseguire ps aux, per essere in grado di creare lo screenshot sopra.

Questo è l'aspetto della prima connessione SSH:

immagine

(Nota per il lettore astuto: Ho provato due volte, da cui prima il ping a google)

immagine

Ulteriori letture:

backend Python e altro su pseudoterminali

Python ha delle utilità integrate per lo pseudo-terminale: pty. Ho trovato la documentazione per essere difficile da ottenere in, ciò che un bambino / maestro / schiavo si riferiscono a.

Ho trovato un esempio qui, nel  Inviare codice sorgente. La "roba buona" inizia nella linea 1242 (def start - no pun intended Sorriso). Come potete vedere, c'è un riferimento a pexpect.spawn che fa cose aggiuntive per il setup e il tear down.

Pertanto, ho semplicemente deciso di usare pexpect come libreria wrapper.

pexpect

La documentazione per pexpect può essere trovato qui. Il suo caso d'uso principale è quello di automatizzare applicazioni interattive.

Pensate, per esempio, a un client FTP a riga di comando. Può anche essere usato per automatizzare i test delle applicazioni.

pexpect crea uno pseudoterminale per le applicazioni che lancia.

Come discusso sopra, lo pseudoterminale ha il grande e necessario vantaggio di mettere queste applicazioni in una modalità di buffering diversa, a cui siamo abituati dalle applicazioni interattive.

Gli pseudoterminali si comportano come i terminali reali - hanno una dimensione dello schermo (numero di colonne e righe), e le applicazioni vi scrivono sequenze di controllo per influenzare la visualizzazione.

Dimensione dello schermo e SIGWINCH

Linus Akesson ha una buona spiegazione sui TTY in generalee anche su SIGWINCH. SIGWINCH è un segnale, simile a SIGHUP o SIGINT.

Nel caso di SIGWINCH, viene inviato al processo figlio per informarlo ogni volta che la dimensione del terminale cambia.

Questo può accadere, per esempio, se si ridimensiona la finestra di PuTTY.

Gli editor come nano e altri (ad esempio alsamixer, ...) che utilizzano lo schermo intero e interagiscono con esso, hanno bisogno di conoscere la dimensione dello schermo per fare i loro calcoli correttamente e rendere l'output correttamente.

Così, ascoltano questo segnale, e se arriva, azzerano i loro calcoli.

immagine

Come puoi vedere da questo esempio a schermo intero, pexpect imposta una dimensione dello schermo più piccola del mio effettivo spazio disponibile in PuTTY - quindi la dimensione dell'output è limitata.

Questo ci porta a:

Sequenze di controllo (codici di escape ANSI)

Come è possibile colorare l'output sulla linea di comando?

immagine

Come è possibile che le applicazioni mostrino barre di progresso interattive sulla linea di comando? (ad esempio, wget):

immagine

Questo viene fatto utilizzando sequenze di controllo, incorporato nell'output dell'applicazione. (Questo è un esempio di informazioni in-band che vengono comunicate in aggiunta, proprio come le compagnie telefoniche segnalavano le informazioni di fatturazione, ecc. in-band, permettendo ai phreaker di abusare del sistema!)

Questo, per esempio, produrrà una nuova linea: \r\n

\r sta per ritorno a capo, sposta il cursore all'inizio della linea senza avanzare alla linea successiva.

\n avanzamento di riga, sposta il cursore alla riga successiva.

Sì, anche in Linux - nei file di solito \n da solo significa nuova linea e implica un ritorno a capo, ma perché l'output dell'applicazione sia interpretato correttamente dal terminale, \r\n è l'uscita effettiva per passare alla linea successiva!

Questo viene emesso dal driver del dispositivo TTY.

Leggi di più su questo comportamento nella documentazione di pexpect.

Per creare testo colorato, o spostare il cursore in qualsiasi punto dello schermo, si possono usare sequenze di escape.

Queste sequenze di escape iniziano con il byte ESC (27 / hex 0x1B / ott 033), e sono seguiti da un secondo byte nell'intervallo 0x40 - 0x5F (ASCII @A-Z[\\]^_ )

una delle sequenze più utili è l'introduttore di sequenza di controllo [ (o sequenze CSI) (ESC è seguito da [ in questo caso).

queste sequenze possono essere utilizzate per posizionare il cursore, cancellare parte dello schermo, impostare i colori e molto altro.

Così, potete impostare il colore del vostro prompt nel ~/.bashrc in questo modo:

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

Questi erano "incantesimi" per me prima di capire questo argomento. ora potete riconoscere le sequenze di controllo, che guidano direttamente l'interpretazione di PuTTY

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

In questo caso \[ e \] sono legati al bash

La sequenza di controllo è all'interno: \033 è per ESC (in rappresentazione ottale), poi abbiamo il [, e poi 01;32m è la sequenza effettiva che imposta il colore di primo piano.

Ulteriori letture:

Codice attuale pexpect

Qui ci sono alcuni utili frammenti di codice pexpect (nota, pexpect ha bisogno di essere installato sul tuo sistema nel solito modo, vedi la documentazione di pexpect).

importare pexpect
tempo di importazione

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

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

child.delaybeforesend = Nessuno

# questo invia un tasto freccia destra
child.send("\033[C")

bambino.interact()

maxread=1 imposta il buffering a nessuno.

Importante: noi imposteremmo delaybeforesend a None, poiché incanaleremo l'input dell'utente reale attraverso pexpect, che ha ritardi incorporati per natura; e non vogliamo aumentare la latenza inutilmente!

Per l'uso effettivo non interattivo (caso d'uso principale di pexpect), si raccomanda il default.

interact() mostrerà direttamente l'output dell'applicazione e invierà l'input dell'utente all'applicazione.

Nota: per l'esempio live qui sopra, child.interact() è stato eseguito direttamente dopo l'istruzione pexpect.spawn().

Web-frontend

La trama si infittisce. Ciò di cui abbiamo bisogno dall'altra parte è un'applicazione JavaScript capace di capire e rendere queste sequenze di controllo. (Ho anche visto che si parla di compatibilità VT-100).

La mia ricerca mi ha portato a xterm.js

Promette prestazioni, compatibilità, supporto unicode, essere autocontenuto, ecc.

Viene spedito tramite npm, il che è buono per il mio nuovo flusso di lavoro per picockpit-frontend.

Molte applicazioni sono basate su xterm.js, incluso Microsoft Visual Studio Code.

Trasporto

Avendo i componenti su entrambi i lati pronti, l'unica cosa che manca è il trasporto. Qui dobbiamo parlare della latenza, e di ciò che gli utenti si aspettano.

Dato che questa è un'applicazione interattiva, avremo bisogno di inviare i caratteri che l'utente digita uno per uno al backend - che dovrebbe (a seconda dell'applicazione attiva) immediatamente riecheggiare questi caratteri.

Nessun buffering è possibile qui - l'unica cosa possibile è che l'applicazione invii più dati come risposta, o per conto suo, che possiamo inviare come pacchetto di dati.

Ma l'input dell'utente deve essere trasmesso carattere per carattere.

Il mio pensiero iniziale su questo era quello di inviare singoli messaggi MQTT spogliati.

Ma questo è in conflitto con la privacy dell'utente.

Anche se i dati vengono eseguiti tramite WebSockets (e quindi https), i messaggi passano in chiaro attraverso il broker MQTT di picockpit.com (VerneMQ).

Le interazioni con la shell includeranno password e dati sensibili - quindi un canale sicuro DEVE essere stabilito.

Attualmente sto pensando di usare i websockets, e possibilmente qualche tipo di libreria per questo, per multiplexare diversi flussi di dati diversi attraverso una connessione ciascuno.

Il trasporto è la ragione per cui sto ritardando TermiShell fino alla versione 3.0 di PiCockpit - ulteriori strumenti sono stati inseriti nel trasporto, permettendo che il trasporto sia riutilizzato anche per altre applicazioni e casi d'uso.

Forse questo potrebbe essere interessante: