TermiShell: un shell basado en la web para la Raspberry Pi (notas de desarrollo)

Introducción

En el transcurso del desarrollo de PiCockpitVoy a añadir un terminal basado en la web llamado TermiShell.

imagen

Icono de TermiShell, por: Stephanie Harvey vía unsplash.com

TermiShell va a permitirle iniciar una sesión en su Raspberry Pi utilizando PiCockpit.com (y el picockpit-cliente) - no se requiere ninguna aplicación adicional en ambos lados. Esto debería ser muy cómodo, especialmente cuando se está en movimiento.

TermiShell no va a ser lanzado en la próxima versión v2.0 de PiCockpitporque requiere un trabajo de preparación adicional, y retrasaría considerablemente la próxima publicación. También prefiero no recortar la seguridad (que sería la alternativa para que funcione de inmediato).

El trabajo, sin embargo, tendrá un impacto positivo en muchas otras capacidades de PiCockpit también - por ejemplo, la capacidad de transmitir vídeo (de la cámara Pi), cargas / descargas de archivos desde el Pi, y muchas otras funcionalidades que requieren flujos de datos.

Estoy recopilando información para mí mismo, y también para otros desarrolladores interesados sobre mis ideas acerca de cómo realizar tal terminal basado en la web, y la información de fondo.

¿Qué es un pseudoterminal (pty)?

Python ofrece funcionalidades incorporadas para ejecutar procesos, y para capturar su salida (stdout y stderr) y enviarles la entrada (stdin).

He aquí un primer intento con el subproceso:

# dow, día de trabajo 1.5.2020
Importar el sistema operativo
importar sys
tiempo de importación
importación de hilos
importar contextlib
importar subproceso

print("¡Hola mundo!")
print("Ejecutando omxplayer")

def lector_salida(proc):
     contFlag = True
     mientras contFlag:
         chars = proc.stdout.read(1)
         si chars != None:
             si chars != b":
                 print(chars.decode('utf-8'), end="", flush=True)
             Si no:
                 contFlag = False

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

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

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

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

proc.stdin.write(b'z')

t.join()

Tenga en cuenta lo siguiente en el código anterior (que no es perfecto, sólo es un código de demostración, y de hecho podría no funcionar como se espera):

  • bufsize se establece en 0 - para suprimir el almacenamiento en búfer
  • He creado un hilo para leer la salida del proceso carácter por carácter
  • He configurado stdin para que tenga cero búferes. Esto a su vez requiere que se abra en modo binario (rb)
  • Leo los caracteres de uno en uno desde la entrada del usuario. Me hago eco de ellos usando flush=True (de lo contrario, la salida es con búfer de línea en Python por defecto para la impresión)
  • Escribo el personaje al proceso. Recuerda, no se almacena en el búfer porque configuramos bufsize=0

Aun así, nos encontramos con la siguiente situación: la salida de la aplicación (en este caso omxplayer), no se recibe carácter por carácter, como se esperaba - sino que se vuelca toda de una vez, al salir.

A pesar de que el buffering está ajustado a 0. ¿Por qué?

procesos de amortiguación e interactivos

búferes stdio de Linux. También es inteligente en esto. Si el proceso es no conectado a un proceso interactivo (terminal), sino a una tubería, la salida se amortigua hasta que se llene el buffer.

A continuación, se copia eficazmente en la otra aplicación.

Este es un comportamiento bueno y eficiente en cuanto a recursos para muchos casos de uso. Pero no si se trata de controlar la aplicación de forma interactiva.

Tampoco hay nada que usted, el desarrollador, pueda hacer para influir en el comportamiento de la otra aplicación cuando se comunica con usted a través de una tubería.

Tendrías que recompilar la otra aplicación (y ajustar manualmente el comportamiento del buffer).

Aquí tiene más recursos sobre este tema:

Los seres humanos -y las aplicaciones que se bloquean si no se proporciona una salida inmediata- necesitan obviamente una salida interactiva. Aquí es donde entra el pseudoterminal.

El pseudoterminal

El pseudoterminal simula un terminal interactivo con la aplicación. stdio piensa que está hablando con un humano, y no hace un buffer. La salida es la que se espera de la interacción con aplicaciones Linux en la línea de comandos (por ejemplo, a través de SSH).

imagen

imagen

Como puede ver en la salida de ps aux, algunas aplicaciones no tienen un TTY (terminal) asignado (apareciendo con un signo de interrogación "?") - en este caso, espere que las aplicaciones muestren un comportamiento de búfer por defecto diferente.

Los pseudoterminales tienen este aspecto en ps aux:

imagen

Descifraré la información para usted:

  • sshd se conecta a la pseudoterminal 0 (pts/0).
  • bash, y algunos otros procesos se inician en el pseudoterminal 0 (pts/0)
  • Uso sudo su para ejecutar un comando como root (que a su vez ejecuta su, y luego bash): python3 ttt.py
  • este script (que te mostraré dentro de un rato) crea un nuevo pseudoterminal pts/1
  • Corro /bin/login de mi script para comprobar las credenciales del usuario. Como las introduje correctamente, bash (el shell por defecto) se inicia en pts/1
  • aquí hago ping a miau.de - este proceso también se ejecuta en pts/1
  • También inicio una segunda conexión SSH, que se conecta a pts/2 - en este caso para ejecutar ps aux, para poder crear la captura de pantalla de arriba.

Este es el aspecto de la primera conexión SSH:

imagen

(Nota para el lector astuto: Lo he intentado dos veces, de ahí el primer ping a google)

imagen

Más información:

Backend de Python y más en los pseudoterminales

Python tiene utilidades incorporadas para la pseudo-terminal: pty. Me pareció que la documentación es difícil de conseguir, lo que un niño / maestro / esclavo se refiere.

He encontrado un ejemplo aquíen el  Invocar código fuente. Lo "bueno" empieza en la línea 1242 (def start - no es un juego de palabras Sonrisa). Como puedes ver, hay una referencia a pexpect.spawn haciendo cosas adicionales para la configuración y el desmontaje.

Por lo tanto, simplemente decidí utilizar pexpect como una biblioteca envolvente.

pexpect

La documentación de pexpect se puede encontrar aquí. Su principal caso de uso es la automatización de aplicaciones interactivas.

Piensa, por ejemplo, en un cliente FTP de línea de comandos. También puede utilizarse para automatizar las pruebas de las aplicaciones.

pexpect crea un pseudoterminal para las aplicaciones que lanza.

Como se ha comentado anteriormente, el pseudoterminal tiene la gran y necesaria ventaja de poner a estas aplicaciones en un modo de buffering diferente al que estamos acostumbrados de las aplicaciones interactivas.

Los pseudoterminales se comportan como los terminales reales: tiene un tamaño de pantalla (número de columnas y filas), y las aplicaciones escriben en él secuencias de control para afectar a la visualización.

Tamaño de la pantalla y SIGWINCH

Linus Akesson tiene una buena explicación sobre los TTY en generaly también sobre SIGWINCH. SIGWINCH es una señal, similar a SIGHUP o SIGINT.

En el caso de SIGWINCH, se envía al proceso hijo para informarle cada vez que cambia el tamaño del terminal.

Esto puede ocurrir, por ejemplo, si se cambia el tamaño de la ventana de PuTTY.

Los editores como nano y otros (por ejemplo, alsamixer, ...) que utilizan la pantalla completa e interactúan con ella, necesitan conocer el tamaño de la pantalla para hacer sus cálculos correctamente y renderizar la salida correctamente.

Por lo tanto, escuchan esta señal y, si llega, reajustan sus cálculos.

imagen

Como puedes ver en este ejemplo de pantalla completa, pexpect establece un tamaño de pantalla más pequeño que mi espacio real disponible en PuTTY - por lo tanto el tamaño de salida es limitado.

Esto nos lleva a:

Secuencias de control (códigos de escape ANSI)

¿Cómo es posible colorear la salida en la línea de comandos?

imagen

¿Cómo es posible que las aplicaciones muestren barras de progreso interactivas en la línea de comandos? (por ejemplo, wget):

imagen

Para ello se utilizan secuencias de control, incrustado en la salida de la aplicación. (Este es un ejemplo de información dentro de la banda que se comunica adicionalmente, al igual que las compañías telefónicas solían señalar la información de facturación, etc. también dentro de la banda, ¡permitiendo a los phreakers abusar del sistema!)

Esto, por ejemplo, producirá una nueva línea: \N - La línea de la derecha.

\r es para el retorno de carro, mueve el cursor al principio de la línea sin avanzar a la siguiente línea.

\N - El avance de línea, mueve el cursor a la siguiente línea.

Sí, incluso en Linux - en los archivos normalmente \n significa nueva línea e implica un retorno de carro, pero para que la salida de la aplicación sea interpretada correctamente por el terminal¡, \r\n es la salida real para ir a la siguiente línea!

Esta es la salida del controlador del dispositivo TTY.

Lea más sobre este comportamiento en la documentación de pexpect.

Para crear texto de color, o mover el cursor a cualquier punto de la pantalla, se pueden utilizar secuencias de escape.

Estas secuencias de escape comienzan con el byte ESC (27 / hex 0x1B / oct 033)y van seguidos de un segundo byte en el rango 0x40 - 0x5F (ASCII @A-Z[\]^_ )

una de las secuencias más útiles es el introductor de secuencias de control [ (o secuencias CSI) (en este caso ESC va seguido de [).

estas secuencias pueden utilizarse para posicionar el cursor, borrar parte de la pantalla, establecer colores y mucho más.

Por lo tanto, puedes establecer el color de tu prompt en el ~/.bashrc de la siguiente manera:

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

Esto solía ser "conjuros mágicos" para mí antes de entender este tema. ahora puede reconocer las secuencias de control, que impulsan su interpretación PuTTY directamente

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

En este caso [ y \] están relacionados con el bash

La secuencia de control está dentro: \033 es para ESC (en representación octal), luego tenemos el [, y luego 01;32m es la secuencia real que establece el color de primer plano.

Más información:

Código real pexpect

Aquí hay algunos fragmentos útiles de código de pexpect (nota, pexpect necesita ser instalado en su sistema primero de la manera habitual, vea la documentación de pexpect).

Importar pexpect
tiempo de importación

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

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

child.delaybeforesend = None

# esto envía una tecla de flecha hacia la derecha
child.send("\033[C")

child.interact()

maxread=1 establece el buffering a ninguno.

Importante: Deberíamos establecer el delaybeforesend en None, ya que vamos a canalizar la entrada del usuario real a través de pexpect, que tiene retrasos incorporados por naturaleza; ¡y no queremos aumentar la latencia innecesariamente!

Para el uso real no interactivo (principal caso de uso de pexpect), se recomienda el valor por defecto.

interact() mostrará la salida de la aplicación directamente, y enviará la entrada del usuario a la aplicación.

Tenga en cuenta: para el ejemplo en vivo anterior, child.interact() se ejecutó directamente después de la declaración pexpect.spawn().

Web-frontend

La trama se complica. Lo que necesitamos por otro lado es una aplicación JavaScript capaz de entender y renderizar estas secuencias de control. (También he visto que se denominan compatibles con VT-100).

Mi investigación me ha llevado a xterm.js

Promete rendimiento, compatibilidad, soporte de unicode, ser autocontenido, etc.

Se envía a través de npm, lo que es bueno para mi nuevo flujo de trabajo para picockpit-frontend.

Muchas aplicaciones se basan en xterm.js, incluyendo Microsoft Visual Studio Code.

Transporte

Teniendo listos los componentes de ambos lados, lo único que falta es el transporte. Aquí tenemos que hablar de la latencia, y de lo que esperan los usuarios.

Como se trata de una aplicación interactiva, necesitaremos enviar los caracteres que el usuario escriba uno a uno al backend - que debería (dependiendo de la aplicación activa) devolver inmediatamente estos caracteres.

En este caso no es posible el almacenamiento en búfer, lo único que es posible es que la aplicación envíe más datos como respuesta, o por su cuenta, que podemos enviar como un paquete de datos.

Pero la entrada del usuario tiene que ser transmitida carácter por carácter.

Mi idea inicial sobre esto era enviar mensajes MQTT individuales y despojados.

Pero esto entra en conflicto con la privacidad del usuario.

Aunque los datos se ejecutan a través de WebSockets (y por lo tanto https), los mensajes pasan sin cifrar a través del broker MQTT de picockpit.com (VerneMQ).

Las interacciones de Shell incluirán contraseñas y datos sensibles, por lo que se debe establecer un canal seguro.

Actualmente estoy pensando en utilizar websockets, y posiblemente algún tipo de biblioteca para ello, para multiplexar varios flujos de datos diferentes a través de una conexión cada uno.

La parte del transporte es la razón por la que estoy retrasando TermiShell hasta el lanzamiento de la versión 3.0 de PiCockpit - se han introducido herramientas adicionales en el transporte, lo que permite reutilizarlo para otras aplicaciones y casos de uso.

Posiblemente esto sea interesante: