
Il controllo di sicurezza che ha reso la nostra app impossibile da chiudere
L'aggiornamento automatico è stata la funzionalità più difficile che abbiamo mai rilasciato — sei versioni in quattro giorni per evitare che rendesse l'app inutilizzabile. Il bug peggiore è stato uno che abbiamo introdotto noi stessi cercando di essere prudenti: un controllo "di sicurezza" da 500 millisecondi che trasformava un aggiornamento fallito in un processo che, letteralmente, non potevi chiudere.
Ogni sviluppatore di app desktop sottovaluta l''aggiornamento automatico esattamente una volta. Sembra un problema risolto — una libreria scarica una nuova versione e riavvia la tua app. Poi lo rilasci, e scopri che "riavvia la tua app" è una delle cose più pericolose che si possano chiedere a un programma, perché accade nel momento esatto in cui la tua app si sta smontando da sola e ha il minor margine di errore possibile.
L''aggiornamento automatico di GeekBye ha richiesto sei versioni in quattro giorni — dalla v1.5.14 alla v1.5.19 — per stabilizzarsi. Questa è la storia del bug peggiore di quel tratto, che abbiamo causato noi stessi cercando di essere prudenti.
Sei versioni, e quella che contava
L''arco è iniziato in modo banale. La v1.5.14 ha corretto un bug da errore di battitura imbarazzante: il feed degli aggiornamenti puntava a un nome di repository GitHub che non esisteva, quindi l''updater interrogava un 404. La v1.5.15 ha aggiunto un pulsante manuale "Check for Updates" e un vero messaggio di errore. Poi sono iniziati i bug di quitAndInstall, e le versioni sono arrivate in fretta — perché quando il tuo meccanismo di aggiornamento è rotto, non puoi rilasciarne la correzione attraverso il meccanismo di aggiornamento. Ogni iterazione è una scommessa di reinstallazione manuale.
Quella che conta è la v1.5.18. Tutto il suo contenuto era un singolo commit con un titolo che ancora mi fa rabbrividire: ripristinare il comportamento originale di quitAndInstall per evitare un''app impossibile da terminare.
Come "essere prudenti" ha reso l''app inutilizzabile
Ecco lo scenario. Quando un aggiornamento è scaricato, il quitAndInstall di Electron dovrebbe chiudere l''app e sostituirla con la nuova versione. In una versione precedente, qualcuno — ragionevolmente — si è preoccupato che chiudere incondizionatamente fosse rischioso. E se l''installazione dava errore? Non sarebbe più sicuro chiudere solo se tutto sembrava a posto?
Così il codice si è arricchito di una guardia che sembrava 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)
La logica: avvia l''installazione, aspetta mezzo secondo, e forza il app.quit() finale solo se il flag updateDownloaded è ancora vero — altrimenti tieni l''app aperta così l''utente non resta a piedi.
La trappola è a una riga di distanza, nel gestore degli errori. Quel gestore impostava this.updateDownloaded = false. Immagina quindi un''installazione fallita: l''evento error scatta e azzera il flag. Ma quitAndInstall aveva già avviato lo smontaggio — aveva chiuso le finestre e rimosso i listener di chiusura dell''app. Poi il timer da 500ms si sveglia, controlla il flag ora falso, decide "errore rilevato, tieni l''app aperta" e salta app.quit().
Ora hai, su macOS in particolare, il peggior stato possibile. macOS non chiude un''app solo perché la sua ultima finestra si è chiusa — è il comportamento window-all-closed su cui si appoggia ogni app Mac. Quindi il processo è ancora vivo, ma non ha finestra, né un percorso dalla barra dei menu, e i suoi listener di chiusura sono stati strappati via. Non c''è niente su cui cliccare. Cmd-Q non ha nessuno con cui parlare. L''unica uscita è Force Quit da Activity Monitor. Il controllo "di sicurezza" aveva trasformato un aggiornamento fallito — una seccatura recuperabile — in uno zombie che non potevi uccidere.
La correzione: lo smontaggio deve essere incondizionato
La correzione nella v1.5.18 è quasi aggressivamente noiosa, ed è proprio questo il punto. Rimuove la furbizia:
- Rimuovere i listener
window-all-closedebefore-quitche potevano interferire. - Distruggere ogni finestra —
window.destroy(), nonwindow.close(). La chiusura può essere vetata da un gestore; la distruzione no. Quando ti stai impegnando a spegnere, non lo chiedi con gentilezza. - Chiamare
quitAndInstall. - Chiamare
app.quit()incondizionatamente.
Nessun flag, nessun timer, nessun "tienila aperta per sicurezza". Perché la verità su un percorso di spegnimento è che uno spegnimento a metà è peggio di entrambi i risultati. Chiudere del tutto va bene. Restare del tutto aperti va bene. L''unico stato che non devi mai raggiungere è smontato ma ancora in esecuzione — ed è esattamente lo stato in cui una chiusura condizionata può lasciarti a piedi.
Altre due lezioni che ha insegnato la stessa settimana
Il bug dell''app impossibile da terminare è il titolo, ma la corsa alle sei versioni ha temprato altre due abitudini che vale la pena rubare.
Filtra la tua telemetria dei crash per firme esatte, non per parole chiave ampie. A metà della corsa abbiamo scoperto che il nostro reporting degli errori era configurato per scartare qualsiasi cosa contenesse parole come permission, token o microphone — un tentativo di tagliare il rumore che stava inghiottendo silenziosamente crash reali che per caso menzionavano quelle parole. Abbiamo strappato via i filtri generici e li abbiamo sostituiti con stringhe di rifiuto esatte (il messaggio specifico che macOS emette quando un permesso viene negato) e codici di rete transitori specifici come ERR_NETWORK_CHANGED. Riduzione del rumore e occultamento dei bug sono la stessa manopola girata in direzioni opposte; se filtri a sensazione, filtrerai proprio la cosa che avevi bisogno di vedere.
Ogni percorso automatico ha bisogno di una via di fuga manuale. L''aggiornamento automatico è per natura al meglio delle possibilità — le reti vacillano, le installazioni falliscono. Così ogni modalità di guasto ha ricevuto un ripiego umano: il pulsante manuale "Check for Updates", tentativi con backoff esponenziale, un timer di ri-verifica e — riservato specificamente al caso in cui il tentativo manuale di un utente falliva — un messaggio in linguaggio semplice "elimina l''app e reinstallala dal sito web". Il percorso automatico è una comodità; il percorso manuale è la garanzia.
La conclusione
- Una guardia attorno a un''azione irreversibile è più pericolosa dell''azione stessa. La chiusura condizionata ha cercato di impedire che un aggiornamento difettoso chiudesse l''app, e invece ha creato uno stato peggiore sia del chiudere sia del non chiudere. I percorsi di spegnimento e installazione dovrebbero essere incondizionati e idempotenti — mai vincolati a un flag mutabile che un altro gestore può ribaltarti sotto i piedi.
- Su macOS, "nessuna finestra" non è "nessuna app". Ogni logica di smontaggio deve tenere conto della piattaforma dove un processo senza finestre continua a girare. Testa il percorso del guasto, sul sistema operativo reale, non solo il percorso felice.
- La funzionalità che rilasci attraverso il sistema di aggiornamento non può essere testata attraverso il sistema di aggiornamento. Questa asimmetria è il motivo per cui l''aggiornamento automatico merita codice paranoico, incondizionato e verificato a mano a fondo. Puoi ripararlo per la via facile solo dopo che già funziona.
Questo è il primo capitolo del lavoro di affidabilità che alla fine è diventato GeekBye v2. Per vedere dove ha portato quella strada, guarda cosa serve davvero per una versione 2 (v2.0.0) e l''intero arco in l''anatomia del rilasciare software alla perfezione.