TermiShell: uma concha baseada na web para o Raspberry Pi (notas de desenvolvimento)

Introdução

No decurso do desenvolvimento de PiCockpitVou adicionar um Terminal baseado na web chamado TermiShell.

imagem

Ícone TermiShell, por: Stephanie Harvey via unsplash.com

TermiShell vai permitir que você entre no seu Raspberry Pi usando PiCockpit.com (e o picockpit-cliente) - nenhuma aplicação adicional é necessária em ambos os lados. Isto deve ser muito confortável, especialmente quando está em movimento.

TermiShell não será lançado no próximo lançamento da v2.0 do PiCockpitporque requer trabalho de preparação adicional, e atrasaria significativamente o próximo lançamento. Eu também preferiria não cortar os cantos da segurança (que seria a alternativa para fazê-lo funcionar imediatamente).

O trabalho, no entanto, terá um impacto positivo em muitas outras capacidades do PiCockpit também - por exemplo, a capacidade de transmitir vídeo (a partir da câmara Pi), uploads / downloads de ficheiros a partir do Pi, e muitas outras funcionalidades que requerem fluxos de dados.

Estou compilando informações para mim mesmo, e também para outros desenvolvedores interessados sobre meus pensamentos sobre como realizar um terminal baseado na web, e informações de fundo.

O que é um pseudo-terminal (pty)?

Python oferece funcionalidades incorporadas para executar processos, e para capturar sua saída (stdout e stderr) e enviar-lhes entrada (stdin).

Aqui está uma primeira tentativa com subprocesso:

# dow, dia de trabalho 1.5.2020
os
sistema de importação
tempo de importação
rosqueamento de importação
contextlib de importação
subprocesso de importação

print("Olá mundo!")
print("Running omxplayer")

def output_reader(proc):
     contFlag = Verdadeiro
     enquanto contFlag:
         chars = proc.stdout.read(1)
         se chars != Nenhum:
             se chars != b":
                 print(chars.decode('utf-8'), end=""", flush=True)
             senão..:
                 contFlag = Falso

proc = subprocesso.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)

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

proc.stdin.write(b'z')

t.join()

Por favor note o seguinte no código acima (que não é perfeito, apenas código de demonstração - e na verdade pode não funcionar como esperado!)

  • bufsize está definido para 0 - para suprimir o amortecimento
  • Configurei um tópico para ler a saída do processo caracter por caracter
  • Eu preparei o stdin para ter zero de tamponamento. Isto, por sua vez, requer que ele seja aberto em modo binário (rb)
  • Eu leio os caracteres um de cada vez a partir da entrada do usuário. Eu os ecoo usando flush=True (caso contrário a saída é line-buffered em Python por padrão para impressão)
  • Eu escrevo o personagem para o processo. Lembre-se, não é tamponado porque montamos bufsize=0

Mesmo assim, nós nos deparamos com a seguinte situação: a saída da aplicação (neste caso omxplayer), não é recebida personagem por personagem, como esperado - ao invés disso, ela é despejada de uma só vez, na saída.

Mesmo que o amortecimento esteja a 0. Porquê?

processos de buffering & interativos

Linux stdio buffers. É inteligente nisto, também. Se o processo for não conectado a um processo interativo (terminal), mas a um tubo, a saída é tamponada até o amortecedor estar cheio.

Depois é copiado de forma eficiente para a outra aplicação.

Este é um comportamento bom e eficiente em termos de recursos para muitos casos de uso. Mas não se você estiver tentando controlar a aplicação de forma interativa.

Também não há nada que você, o desenvolvedor, possa fazer para influenciar o comportamento da outra aplicação ao falar com você através de um tubo.

Você precisaria recompilar a outra aplicação (e ajustar manualmente o comportamento de buffering).

Aqui estão mais alguns recursos sobre este tópico:

Os seres humanos - e as aplicações que bloqueiam se nenhuma saída é fornecida imediatamente - obviamente precisam de uma saída interativa. É aqui que entra o pseudoterminal.

O pseudoterminal

O pseudoterminal simula um terminal interativo para a aplicação. stdio pensa que está falando com um humano, e não faz buffer. A saída é como você esperaria de interagir com aplicações Linux na linha de comando (por exemplo, via SSH).

imagem

imagem

Como você pode ver na saída do ps aux, algumas aplicações não têm um TTY (terminal) atribuído a elas (aparecendo com um ponto de interrogação "?") - neste caso, espere que as aplicações mostrem um comportamento de buffering padrão diferente.

Pseudoterminais parecem-se com isto em ps aux:

imagem

Vou descodificar a informação para si:

  • sshd liga-se ao pseudoterminal 0 (pts/0).
  • bash, e alguns outros processos são iniciados no pseudoterminal 0 (pts/0)
  • Eu uso sudo su para executar um comando como root (que por sua vez executa su, e depois bash): python3 ttt.py
  • este guião (que vos mostrarei dentro de pouco tempo) cria um novo pseudoterminal pts/1
  • eu corro /bin/login do meu script para verificar as credenciais do usuário. Porque as introduzi correctamente, a bash (a shell predefinida) é iniciada no pts/1
  • aqui eu ping miau.de - este processo também é executado em pts/1
  • Também inicio uma segunda conexão SSH, que se liga ao pts/2 - neste caso para executar ps aux, para poder criar a captura de tela acima.

É assim que a primeira ligação SSH se parece:

imagem

(Nota para o leitor astuto: Eu tentei duas vezes, daí primeiro o ping para o google)

imagem

Leitura adicional:

Backend Python & mais sobre pseudoterminais

Python tem utilitários embutidos para o pseudo-terminal: pty. Achei a documentação difícil de entrar, a que se refere uma criança / mestre / escravo.

Eu encontrei um exemplo aquino  Invoque código fonte. O "material bom" começa na linha 1242 (def start - sem trocadilho pretendido Sorria). Como você pode ver, há uma referência ao pexpect.spawn fazendo coisas adicionais para a instalação e desmontagem.

Portanto, eu simplesmente decidi usar expectativa como uma biblioteca de embalagens.

expectativa

A documentação para os espectadores pode ser encontrado aqui. O seu principal uso é a automatização de aplicações interactivas.

Pense, por exemplo, em um cliente FTP de linha de comando. Ele também pode ser usado para automatizar testes de aplicações.

pexpect cria um pseudoterminal para as aplicações que lança.

Como discutido acima, o pseudoterminal tem a grande e necessária vantagem de colocar essas aplicações em um modo de amortecimento diferente, ao qual estamos acostumados a partir de aplicações interativas.

Pseudoterminais comportam-se como terminais reais - tem um tamanho de tela (número de colunas e linhas), e aplicações escrevem seqüências de controle para afetar a tela.

Tamanho da tela e SIGWINCH

Linus Akesson tem uma boa explicação sobre as TTYs em geral...e também sobre o SIGWINCH. SIGWINCH é um sinal, semelhante ao SIGHUP ou SIGINT.

No caso do SIGWINCH, ele é enviado ao processo infantil para informá-lo sempre que o tamanho do terminal mudar.

Isto pode acontecer, por exemplo, se você redimensionar a sua janela PuTTY.

Editores como nano e outros (por exemplo, alsamixer, ...) que usam a tela cheia e interagem com ela, precisam saber o tamanho da tela para fazer seus cálculos corretamente e renderizar a saída corretamente.

Assim, eles ouvem este sinal e, se ele chegar, reiniciam os seus cálculos.

imagem

Como você pode ver neste exemplo em tela cheia, pexpect define um tamanho de tela menor do que o meu espaço disponível real no PuTTY - portanto, o tamanho de saída é limitado.

Isto leva-nos a isso:

Sequências de controlo (códigos de fuga ANSI)

Como é possível colorir a saída na linha de comando?

imagem

Como é possível que as aplicações mostrem barras de progresso interativas na linha de comando? (e.g. wget):

imagem

Isto é feito através de sequências de controlo, incorporado na saída da aplicação. (Este é um exemplo de informação na banda sendo comunicada adicionalmente, muito parecido com as companhias telefônicas usadas para sinalizar informações de faturamento, etc. também na banda, permitindo que os disjuntores abusem do sistema!)

Isto, por exemplo, vai produzir uma nova linha: \...e...

\r é para retorno de carruagem, move o cursor para o início da linha sem avançar para a linha seguinte.

{\i1}n alimentação de linha, move o cursor para a linha seguinte.

Sim, mesmo no Linux - em arquivos geralmente só \n significará uma nova linha e implicará um retorno de carro, mas para que a saída da aplicação seja interpretada corretamente pelo terminal...é o resultado real para ir para a próxima linha!

Isto é emitido pelo driver do dispositivo TTY.

Leia mais sobre este comportamento na documentação da Pepexpect.

Para criar texto colorido, ou mover o cursor para qualquer ponto da tela, podem ser usadas seqüências de escape.

Estas sequências de fuga começam com o byte ESC (27 / hex 0x1B / oct 033)e são seguidos por um segundo byte na faixa 0x40 - 0x5F (ASCII @A-Z[\]^_ )

uma das sequências mais úteis é o introdutor da sequência de controlo [ (ou sequências CSI) (ESC é seguido de [ neste caso).

estas sequências podem ser usadas para posicionar o cursor, apagar parte da tela, definir cores, e muito mais.

Assim, você pode definir a cor do seu prompt no ~/.bashrc desta forma:

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

Isto costumava ser "encantamentos mágicos" para mim antes de eu entender este tópico. agora você pode reconhecer as seqüências de controle, que impulsionam sua interpretação PuTTY diretamente

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

Neste caso, estão relacionados com a festa...

A sequência de controlo está lá dentro: \033 é para ESC (em representação octal), depois temos o [, e depois 01;32m é a sequência real que define a cor do primeiro plano.

Leitura adicional:

Código de expectativa real

Aqui estão alguns trechos úteis de código pexpect (nota, o pexpect precisa ser instalado no seu sistema primeiro da maneira usual, veja a documentação do pexpect).

pexpect de importação
tempo de importação

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

tempo.de sono(0.5)
print(child.read_nononblocking(size=30, timeout=0))

child.delaybeforesend = Nenhum

# isto envia uma seta para a direita
child.send("\033[C")

child.interact()

maxread=1 conjuntos de buffering a nenhum.

Importante: Nós definiríamos o delaybeforesend para Nenhum, pois estaremos funilando a entrada do usuário real através do pexpect, que tem atrasos embutidos por natureza; e nós não queremos aumentar a latência desnecessária!

Para uso não-interactivo real (caso de uso principal de pexpect), o padrão é recomendado.

interact() mostrará a saída da aplicação directamente, e enviará a sua entrada de utilizador para a aplicação.

Por favor note: para o exemplo ao vivo acima, child.interact() foi executado diretamente após a declaração pexpect.spawn().

Web-frontend

O enredo engrossa. O que precisamos do outro lado é de uma aplicação JavaScript capaz de compreender e renderizar estas sequências de controlo. (Eu também as vi serem referidas como compatíveis com o VT-100).

A minha pesquisa levou-me a xterm.js

Promete desempenho, compatibilidade, suporte unicode, ser auto-contido, etc.

É enviado através de npm, o que é bom para o meu novo fluxo de trabalho para o picockpit-frontend.

Muitas aplicações são baseadas no xterm.js, incluindo o Microsoft Visual Studio Code.

Transportes

Tendo componentes em ambos os lados prontos, a única coisa que falta é o transporte. Aqui temos que falar sobre a latência, e o que os usuários esperam.

Como esta é uma aplicação interactiva, teremos de enviar caracteres que o utilizador digita um a um para o back end - que deverá (dependendo da aplicação activa) fazer imediatamente eco desses caracteres de volta.

Aqui não é possível fazer buffering - a única coisa possível é que a aplicação envie mais dados como resposta, ou por conta própria, que nós podemos enviar como um pacote de dados.

Mas a entrada do usuário tem que ser transmitida caráter por caráter.

O meu pensamento inicial sobre isto foi enviar mensagens individuais, despojadas de MQTT.

Mas isto entra em conflito com a privacidade do utilizador.

Mesmo que os dados sejam executados através de WebSockets (e portanto https), as mensagens passam sem criptografia através do corretor MQTT picockpit.com (VerneMQ).

As interações da Shell incluirão senhas e dados sensíveis - assim, um canal seguro DEVE ser estabelecido.

Atualmente eu estou pensando em usar websockets, e possivelmente algum tipo de biblioteca para isso, para multiplexar vários fluxos de dados diferentes através de uma conexão cada um.

O bit de transporte é a razão pela qual estou atrasando a TermiShell até a liberação 3.0 do PiCockpit - ferramentas adicionais foram para o transporte, permitindo que o transporte seja reutilizado para outras aplicações e casos de uso também.

Possivelmente isto possa ser interessante: