Terminal Ao Vivo
O MangoStudio consegue abrir um shell interativo real na máquina que um ambiente descreve e
mostrá-lo no navegador: como um painel Terminal na barra lateral do chat, ou em uma janela
própria a partir de /terminal. Várias sessões podem rodar ao mesmo tempo. O shell roda onde o
runtime do ambiente roda — Local, stdio, WSL, SSH, contêiner ou máquina pareada — nunca no hub
por exceção.
Onde cada coisa roda
| Responsabilidade | Dono |
|---|---|
| O PTY, o processo do shell, seu ambiente e seu ciclo de vida | Runtime (crates/mangostudio-runtime) |
| Quem pode abrir uma sessão, o registro, limites, expiração ociosa | Hub (apps/api) |
| Retransmitir bytes ao navegador e controle de fluxo por socket | Hub (apps/api) |
| Formato da sessão, enquadramento do socket, limites | Shared (apps/shared) |
| Renderização, teclas, redimensionamento, confirmações | Frontend (xterm.js) |
O runtime abre o PTY por conta própria (crates/mangostudio-runtime/src/terminal/pty.rs): no
Unix o shell é líder de sessão com o PTY como terminal de controle, e por isso controla seus
próprios jobs; no Windows ele roda sob ConPTY. Nos dois casos ele fica no mesmo guardião ou Job
com encerramento no fechamento que qualquer processo supervisionado. Não há addon nativo para
distribuir. O runtime
guarda os últimos 256 KiB de saída de cada sessão para que um visualizador que volte veja onde
estava.
Protocolo
Oito métodos do runtime carregam a sessão — terminal.open, attach, detach, write,
resize, ack, close, list — e um tópico de evento, terminal.output, transmite bytes ao
hub chaveados pelo id de sessão cunhado pelo hub. Todo método exige a capacidade shell; uma
máquina readonly recusa todos eles, inclusive a listagem. O runtime anuncia terminal: true
no manifesto e no relatório de saúde apenas quando o dono concedeu shell, existe um shell e o
build consegue abrir um PTY. Ausente significa indisponível.
O runtime nunca emite terminal.output antes de um hub chamar terminal.attach naquela sessão.
Essa invariante é o que torna o tópico aditivo: um hub antigo demais para decodificar o payload
nunca o recebe.
O navegador conversa com /api/terminal/:id, uma rota WebSocket própria. /api/ws continua
apenas para invalidações. Os frames são binários com um byte de tipo como prefixo: cliente
data, resize, ack, ping; servidor data, exit, notice, pong. O codec vive em
@mangostudio/shared/terminal.
Controle de fluxo
As opções de socket que toda rota WebSocket do hub compartilha fecham a conexão com 64 KiB de backpressure em vez de desacelerá-la. Por isso o controle de fluxo é explícito e vive em três lugares:
- Navegador. Depois que o xterm.js interpreta um bloco, o cliente confirma os bytes. As confirmações são agrupadas, não enviadas por frame.
- Hub. As confirmações são repassadas ao runtime como
terminal.ack. O relay mantém uma fila própria por socket, envia apenas enquanto a quantidade em buffer do socket fica abaixo de 48 KiB, retoma no drain e, passando de 1 MiB enfileirado, descarta os bytes mais antigos e envia um avisoqueue_overflow. - Runtime. A emissão é controlada por crédito: no máximo 256 KiB podem estar em voo sem
confirmação. A saída além da janela espera em um buffer pendente de 1 MiB; passando disso, os
bytes mais antigos são descartados e um marcador
droppedsai quando o fluxo retoma. O terminal o desenha como uma linha esmaecida.
Um loop de yes custa, portanto, memória limitada em cada salto, e o servidor nunca fecha o
socket por ele ser rápido.
Sessões
As sessões pertencem ao runtime e sobrevivem ao socket do navegador. Fechar o painel ou trocar de aba desanexa; anexar de novo reproduz o scrollback. Um visualizador segura uma sessão por vez: abri-la em uma janela a tira da barra lateral, que então oferece trazê-la de volta.
Um visualizador anexa em três passos, e a ordem é o contrato: terminal.detach para silenciar
o que o último deixou correndo, depois a inscrição na saída, depois terminal.attach. Só essa
ordem faz cada frame ser ou scrollback ou mais novo que ele — o tópico não tem buffer, então
inscrever-se depois da resposta perde o que veio na mesma leitura, e inscrever-se antes do
detach de silenciamento reproduz bytes que o scrollback já carrega.
Uma sessão termina quando o usuário a fecha, quando fica sem visualizador por
idle_timeout_minutes, quando a conexão do runtime cai ou quando o hub para. O registro do hub
fica em memória; sessões não sobrevivem a um reinício do hub.
Quem pode abrir uma
A mesma capacidade de shell.run: allow.shell no runtime. No runtime Local — um processo
filho do hub na própria conta de SO do hub — um terminal exige adicionalmente que a conexão
Local esteja atestada: a reivindicação de dono único que o caminho de agentes externos já aplica. Um segundo usuário do MangoStudio no mesmo hub
fecha todo terminal Local e recusa novos com TERMINAL_NOT_ISOLATED.
O ambiente do shell é o do próprio runtime com variáveis com cara de segredo removidas exatamente
como shell.run as remove, mais TERM=xterm-256color, COLORTERM=truecolor e
MANGOSTUDIO_TERMINAL=1. O log de auditoria registra o shell, o diretório de trabalho e o
tamanho em terminal.open; teclas nunca são gravadas.
Configuração
[terminal]
enabled = true
idle_timeout_minutes = 30
max_sessions_per_user = 8
scrollback_kib = 256
Ambiente: MANGO_TERMINAL_ENABLED, MANGO_TERMINAL_IDLE_TIMEOUT_MINUTES,
MANGO_TERMINAL_MAX_SESSIONS_PER_USER, MANGO_TERMINAL_SCROLLBACK_KIB. Com enabled = false
o painel fica oculto e aberturas respondem TERMINAL_DISABLED.
Limites e erros
| Recusa | Código |
|---|---|
| Terminais desligados no hub | TERMINAL_DISABLED |
| Limite por usuário atingido | TERMINAL_LIMIT |
| Runtime Local em um hub multiusuário | TERMINAL_NOT_ISOLATED |
| Runtime não oferece PTY, ou o dono recusou | UNSUPPORTED |
GET /api/terminals/availability?environmentId=… responde todas essas antes de alguém digitar,
para que o painel explique em vez de falhar na abertura. Ambientes em contêiner precisam de um
shell na imagem; o painel diz isso.
Windows
Sessões em um runtime Windows usam ConPTY (crates/mangostudio-runtime/src/subprocess/windows_job.rs)
com PowerShell (pwsh preferido, powershell.exe como alternativa), iniciadas dentro do mesmo
Job com encerramento no fechamento de um filho limitado. Redimensionar funciona. Os testes de
terminal com shell real do runtime são só Unix (#[cfg(unix)]), então o ramo ConPTY não tem teste
próprio; o caminho POSIX é coberto por testes com PTY real.