
De veiligheidscontrole die onze app onmogelijk te sluiten maakte
Automatisch updaten was de moeilijkste feature die we ooit hebben uitgebracht — zes releases in vier dagen om te voorkomen dat het de app onbruikbaar maakte. De ergste bug was er een die we zelf introduceerden terwijl we voorzichtig probeerden te zijn: een "veiligheidscontrole" van 500 milliseconden die een mislukte update veranderde in een proces dat je letterlijk niet kon afsluiten.
Elke ontwikkelaar van desktop-apps onderschat het automatisch updaten precies één keer. Het lijkt een opgelost probleem — een bibliotheek downloadt een nieuwe versie en herstart je app. Dan breng je het uit, en je leert dat "herstart je app" een van de gevaarlijkste dingen is die je een programma kunt vragen, want het gebeurt precies op het moment dat je app zichzelf aan het afbreken is en de minste marge voor fouten heeft.
Het automatisch updaten van GeekBye kostte zes releases in vier dagen — v1.5.14 tot en met v1.5.19 — om te stabiliseren. Dit is het verhaal van de ergste bug in die reeks, die we zelf veroorzaakten door voorzichtig te willen zijn.
Zes releases, en de ene die ertoe deed
De reeks begon alledaags. v1.5.14 verhielp een gênante bug van het typefout-genre: de update-feed wees naar een GitHub-reponaam die niet bestond, dus de updater controleerde een 404. v1.5.15 voegde een handmatige "Check for Updates"-knop en een echte foutmelding toe. Toen begonnen de quitAndInstall-bugs, en de releases kwamen snel — want als je updatemechanisme kapot is, kun je de oplossing ervoor niet via het updatemechanisme uitbrengen. Elke iteratie is een gok met handmatige herinstallatie.
De ene die ertoe doet is v1.5.18. De volledige inhoud was één enkele commit met een titel waar ik nog steeds van ineenkrimp: restore original quitAndInstall behavior to prevent unkillable app.
Hoe "voorzichtig zijn" de app onbruikbaar maakte
Dit is de opzet. Wanneer een update is gedownload, hoort Electrons quitAndInstall de app te sluiten en de nieuwe versie in te wisselen. In een eerdere release maakte iemand — begrijpelijkerwijs — zich zorgen dat onvoorwaardelijk afsluiten riskant was. Wat als de installatie faalde? Zou het niet veiliger zijn om alleen af te sluiten als alles er gezond uitzag?
Dus groeide de code een controle die verstandig leek:
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)
De logica: start de installatie, wacht een halve seconde, en forceer de laatste app.quit() alleen als de updateDownloaded-vlag nog steeds waar is — anders houd je de app open zodat de gebruiker niet gestrand raakt.
De valkuil ligt één regel verderop, in de foutafhandelaar. Die afhandelaar zette this.updateDownloaded = false. Stel je dus een mislukte installatie voor: het error-event vuurt en wist de vlag. Maar quitAndInstall was al begonnen met de afbraak — het had de vensters gesloten en de afsluitlisteners van de app verwijderd. Dan ontwaakt de timer van 500 ms, controleert de nu-onware vlag, besluit "fout gedetecteerd, houd de app open," en slaat app.quit() over.
Nu heb je, specifiek op macOS, de slechtst mogelijke toestand. macOS sluit een app niet af alleen omdat het laatste venster is gesloten — dat is het window-all-closed-gedrag waar elke Mac-app op rekent. Dus het proces leeft nog, maar het heeft geen venster, geen pad via de menubalk, en zijn afsluitlisteners zijn eruit gerukt. Er is niets om op te klikken. Cmd-Q heeft niets om mee te praten. De enige uitweg is Force Quit vanuit Activity Monitor. De "veiligheids"-controle had een mislukte update — een herstelbaar ongemak — omgezet in een zombie die je niet kon doden.
De oplossing: afbraak moet onvoorwaardelijk zijn
De correctie in v1.5.18 is bijna agressief saai, en dat is het punt. Ze haalt de slimmigheid weg:
- Verwijder de
window-all-closed- enbefore-quit-listeners die konden interfereren. - Vernietig elk venster —
window.destroy(), nietwindow.close(). Sluiten kan door een handler worden geblokkeerd; vernietigen niet. Als je je vastlegt op afsluiten, vraag je niet beleefd. - Roep
quitAndInstallaan. - Roep
app.quit()onvoorwaardelijk aan.
Geen vlag, geen timer, geen "houd het voor de zekerheid open." Want de waarheid over een afsluitpad is dat een half afgemaakte afsluiting erger is dan beide uitkomsten. Volledig afsluiten is prima. Volledig openblijven is prima. De ene toestand die je nooit mag bereiken is afgebroken maar nog draaiend — en dat is precies de toestand waarin een voorwaardelijke afsluiting je kan laten stranden.
Twee lessen meer die dezelfde week ons leerde
De onstopbare bug is de kop, maar het zes-releases-geharrewar verstevigde nog twee andere gewoontes die het stelen waard zijn.
Filter je crash-telemetrie op exacte handtekeningen, niet op brede sleutelwoorden. Midden in het geharrewar ontdekten we dat onze foutrapportage zo was ingesteld dat alles met woorden als permission, token of microphone werd weggegooid — een poging om ruis te snijden die stilletjes echte crashes verzwolg die toevallig die woorden noemden. We rukten de blanco filters eruit en verving ze door exacte weigeringsstrings (het specifieke bericht dat macOS uitzendt wanneer een permissie wordt geweigerd) en specifieke tijdelijke netwerkcodes zoals ERR_NETWORK_CHANGED. Ruisreductie en bugs-verbergen zijn dezelfde knop, tegengesteld gedraaid; als je op gevoel filtert, filter je het ding weg dat je nodig had te zien.
Elk automatisch pad heeft een handmatige nooduitgang nodig. Automatisch updaten is van nature best-effort — netwerken haperen, installaties falen. Dus elke foutmodus kreeg een menselijke terugvaloptie: de handmatige "Check for Updates"-knop, herpogingen met exponentiële backoff, een hercontrole-timer, en — specifiek gereserveerd voor het geval waarin de handmatige poging van een gebruiker faalde — een begrijpelijk "verwijder de app en herinstalleer vanaf de website"-bericht. Het automatische pad is een gemak; het handmatige pad is de garantie.
De conclusie
- Een controle rond een onomkeerbare actie is gevaarlijker dan de actie zelf. De voorwaardelijke afsluiting probeerde te voorkomen dat een slechte update de app afsloot, en creëerde in plaats daarvan een toestand die erger was dan zowel afsluiten als niet afsluiten. Afsluit- en installatiepaden horen onvoorwaardelijk en idempotent te zijn — nooit afhankelijk van een veranderbare vlag die een andere handler onder je vandaan kan omzetten.
- Op macOS betekent "geen vensters" niet "geen app." Elke afbraaklogica moet rekening houden met het platform waar een vensterloos proces blijft draaien. Test het foutpad, op het echte besturingssysteem, niet alleen het gelukkige pad.
- De feature die je uitbrengt via het updatesysteem kan niet worden getest via het updatesysteem. Die asymmetrie is waarom automatisch updaten paranoïde, onvoorwaardelijke, zwaar handmatig geverifieerde code verdient. Je mag het pas op de makkelijke manier repareren nadat het al werkt.
Dit is het vroegste hoofdstuk van het betrouwbaarheidswerk dat uiteindelijk GeekBye v2 werd. Voor waar die weg heen leidde, zie wat een versie 2 echt kost (v2.0.0) en de hele reeks in de anatomie van software tot in de perfectie uitbrengen.