Steven
Steven6 min de leitura

A verificação de segurança que tornou nosso app impossível de fechar

A atualização automática foi a funcionalidade mais difícil que já lançamos — seis versões em quatro dias para impedir que ela deixasse o app inutilizável. O pior bug foi um que nós mesmos introduzimos ao tentar ser cuidadosos: uma verificação "de segurança" de 500 milissegundos que transformava uma atualização falha em um processo que, literalmente, não dava para fechar.

Engenharia
Electron
Confiabilidade
Lançamentos do GeekBye
A verificação de segurança que tornou nosso app impossível de fechar

Todo desenvolvedor de app de desktop subestima a atualização automática exatamente uma vez. Parece um problema resolvido — uma biblioteca baixa uma versão nova e reinicia seu app. Aí você lança, e aprende que "reinicie seu app" é uma das coisas mais perigosas que se pode pedir a um programa, porque acontece no momento exato em que seu app está se desmontando e tem a menor margem de erro possível.

A atualização automática do GeekBye precisou de seis versões em quatro dias — da v1.5.14 à v1.5.19 — para estabilizar. Esta é a história do pior bug desse trecho, que nós mesmos causamos ao tentar ser cuidadosos.

Seis versões, e a que importava

O arco começou banal. A v1.5.14 corrigiu um bug de nível erro de digitação vergonhoso: o feed de atualizações apontava para um nome de repositório do GitHub que não existia, então o atualizador estava consultando um 404. A v1.5.15 adicionou um botão manual de "Check for Updates" e uma mensagem de erro de verdade. Depois começaram os bugs do quitAndInstall, e as versões vieram rápido — porque quando seu mecanismo de atualização está quebrado, você não pode lançar a correção dele através do mecanismo de atualização. Cada iteração é uma aposta de reinstalação manual.

A que importa é a v1.5.18. Todo o seu conteúdo foi um único commit com um título que ainda me faz encolher: restaurar o comportamento original do quitAndInstall para evitar um app impossível de matar.

Como "ser cuidadoso" deixou o app inutilizável

Eis o cenário. Quando uma atualização é baixada, o quitAndInstall do Electron deveria fechar o app e trocar pela versão nova. Numa versão anterior, alguém — razoavelmente — se preocupou de que fechar incondicionalmente fosse arriscado. E se a instalação desse erro? Não seria mais seguro fechar só se tudo parecesse saudável?

Então o código ganhou uma guarda que parecia sensata:

autoUpdater.quitAndInstall(false, true)
setTimeout(() => {
  if (this.updateDownloaded)
    app.quit() // only quit if the update is still "good"
  else console.log('Error detected — keeping app open')
}, 500)

A lógica: dispara a instalação, espera meio segundo, e só força o app.quit() final se a flag updateDownloaded ainda for verdadeira — caso contrário mantém o app aberto para o usuário não ficar na mão.

A armadilha está a uma linha de distância, no tratador de erros. Esse tratador definia this.updateDownloaded = false. Então imagine uma instalação falha: o evento error dispara e limpa a flag. Mas o quitAndInstall tinha começado o desmonte — tinha fechado as janelas e removido os ouvintes de fechamento do app. Aí o temporizador de 500ms acorda, verifica a flag agora falsa, decide "erro detectado, manter o app aberto" e pula o app.quit().

Agora você tem, no macOS especificamente, o pior estado possível. O macOS não fecha um app só porque sua última janela fechou — esse é o comportamento window-all-closed no qual todo app de Mac se apoia. Então o processo continua vivo, mas não tem janela, nem caminho pela barra de menu, e seus ouvintes de fechamento foram arrancados. Não há nada para clicar. O Cmd-Q não tem com quem falar. A única saída é o Force Quit pelo Activity Monitor. A verificação "de segurança" tinha convertido uma atualização falha — um aborrecimento recuperável — em um zumbi que você não conseguia matar.

A correção: o desmonte tem que ser incondicional

A correção da v1.5.18 é quase agressivamente entediante, e esse é o ponto. Ela remove a esperteza:

  1. Remover os ouvintes window-all-closed e before-quit que podiam interferir.
  2. Destruir cada janela — window.destroy(), não window.close(). O fechamento pode ser vetado por um tratador; a destruição não. Quando você está se comprometendo a desligar, não pede com educação.
  3. Chamar o quitAndInstall.
  4. Chamar o app.quit() incondicionalmente.

Sem flag, sem temporizador, sem "deixa aberto por via das dúvidas". Porque a verdade sobre um caminho de desligamento é que um desligamento pela metade é pior do que qualquer um dos dois resultados. Fechar por completo tudo bem. Ficar aberto por completo tudo bem. O único estado que você nunca pode alcançar é desmontado mas ainda em execução — e é exatamente o estado em que um fechamento condicional pode te deixar na mão.

Mais duas lições que a mesma semana ensinou

O bug do app impossível de matar é a manchete, mas a correria de seis versões endureceu outros dois hábitos que vale a pena roubar.

Filtre sua telemetria de travamentos por assinaturas exatas, não por palavras-chave amplas. No meio da correria descobrimos que nosso reporte de erros estava configurado para descartar qualquer coisa que contivesse palavras como permission, token ou microphone — uma tentativa de cortar ruído que estava engolindo silenciosamente travamentos reais que por acaso mencionavam essas palavras. Arrancamos os filtros genéricos e os substituímos por strings de negação exatas (a mensagem específica que o macOS emite quando uma permissão é recusada) e códigos de rede transitórios específicos como ERR_NETWORK_CHANGED. Redução de ruído e ocultação de bugs são o mesmo botão girado em sentidos opostos; se você filtrar no feeling, vai filtrar justamente aquilo que precisava ver.

Todo caminho automático precisa de uma saída de emergência manual. A atualização automática é de melhor esforço por natureza — redes falham, instalações caem. Então cada modo de falha ganhou um plano B humano: o botão manual de "Check for Updates", tentativas com recuo exponencial, um temporizador de reverificação e — reservado especificamente para o caso em que a tentativa manual de um usuário falhava — uma mensagem em linguagem simples de "apague o app e reinstale pelo site". O caminho automático é uma conveniência; o caminho manual é a garantia.

A conclusão

  1. Uma guarda em volta de uma ação irreversível é mais perigosa do que a ação. O fechamento condicional tentou impedir que uma atualização ruim fechasse o app, e em vez disso criou um estado pior do que fechar ou não fechar. Caminhos de desligamento e instalação deveriam ser incondicionais e idempotentes — nunca condicionados a uma flag mutável que outro tratador pode virar por baixo de você.
  2. No macOS, "sem janelas" não é "sem app". Toda lógica de desmonte tem que levar em conta a plataforma onde um processo sem janelas continua rodando. Teste o caminho da falha, no sistema operacional de verdade, não só o caminho feliz.
  3. A funcionalidade que você lança pelo sistema de atualização não pode ser testada pelo sistema de atualização. Essa assimetria é o motivo pelo qual a atualização automática merece código paranoico, incondicional e verificado à mão a fundo. Você só pode consertá-la pelo jeito fácil depois de ela já funcionar.

Este é o capítulo mais antigo do trabalho de confiabilidade que acabou virando o GeekBye v2. Para ver aonde esse caminho levou, veja o que uma versão 2 realmente exige (v2.0.0) e todo o arco em a anatomia de entregar software à perfeição.

Artigos Relacionados

Uma conexão perdida não deveria derrubar o app inteiro — mas o nosso derrubava
Steven
Steven7 min de leitura

Uma conexão perdida não deveria derrubar o app inteiro — mas o nosso derrubava

Quando nosso backend ficou offline no meio de uma reunião, ele não só pausou a transcrição — derrubou o app inteiro. A causa era um único evento não tratado, e a correção tinha dez linhas. Este é o grupo de lançamentos que fez o GeekBye continuar logado e continuar conectado através das coisas que antes o matavam.

Engenharia
Confiabilidade
Electron
O silêncio era estrutural
Steven
Steven8 min de leitura

O silêncio era estrutural

As duas últimas releases da GeekBye v1 giram em torno da mesma verdade incômoda: a transcrição em tempo real sobre uma rede real não é sem perdas, e o gesto honesto é parar de fingir que é. A v1.8.20 guardava em disco uma cópia de cada chunk de áudio antes de descartá-lo durante uma reconexão, e começou a marcar em voz alta os buracos na transcrição. A v1.9.0 parou de enviar silêncio para economizar largura de banda — e descobriu que o silêncio era justamente o sinal que o transcritor usava para saber que uma frase havia terminado. Duas releases sobre o custo de jogar coisas fora.

Engenharia
Audio
Confiabilidade
Imprimir uma reunião em PDF sem uma biblioteca de PDF
Steven
Steven10 min de leitura

Imprimir uma reunião em PDF sem uma biblioteca de PDF

A GeekBye exporta uma reunião como PDF, e não há nenhuma biblioteca de PDF em lugar nenhum do código. Ela renderiza HTML numa janela de navegador invisível e a imprime. Essa escolha é a história toda: tornou a funcionalidade fácil de construir e lhe deu todas as falhas que um navegador de verdade tem — um flash branco, um limite de comprimento de URL, e uma quebra de página que cortava as capturas ao meio. A correção do bug mais feio foi uma única linha de CSS.

Engenharia
Electron
Desktop