
Säkerhetskontrollen som gjorde vår app omöjlig att avsluta
Automatisk uppdatering var den svåraste funktionen vi någonsin skeppat — sex releaser på fyra dagar för att hindra den från att förstöra appen. Den värsta buggen var en vi själva införde när vi försökte vara försiktiga: en 500-millisekunders "säkerhetskontroll" som förvandlade en misslyckad uppdatering till en process du bokstavligen inte kunde avsluta.
Varje skrivbordsapputvecklare underskattar automatisk uppdatering exakt en gång. Det ser ut som ett löst problem — ett bibliotek laddar ner en ny version och startar om din app. Sedan skeppar du det, och du lär dig att "starta om din app" är en av de farligaste sakerna ett program kan ombes göra, eftersom det händer i precis det ögonblick då din app river ner sig själv och har minst felmarginal.
GeekByes automatiska uppdatering tog sex releaser på fyra dagar — v1.5.14 till v1.5.19 — att stabilisera. Detta är berättelsen om den värsta buggen i den sträckan, som vi själva orsakade genom att försöka vara försiktiga.
Sex releaser, och den enda som spelade roll
Bågen började trivialt. v1.5.14 fixade en pinsam stavfelsklassad bugg: uppdateringsflödet pekade på ett GitHub-repositorienamn som inte fanns, så uppdateraren kontrollerade en 404. v1.5.15 lade till en manuell "Check for Updates"-knapp och ett riktigt felmeddelande. Sedan började quitAndInstall-buggarna, och releaserna kom snabbt — för när din uppdateringsmekanism är trasig kan du inte skeppa fixen för den genom uppdateringsmekanismen. Varje iteration är ett manuellt ominstallationsspel.
Den som spelar roll är v1.5.18. Hela dess innehåll var en enda commit med en titel som fortfarande får mig att rygga tillbaka: restore original quitAndInstall behavior to prevent unkillable app.
Hur "att vara försiktig" förstörde appen
Här är upplägget. När en uppdatering laddats ner ska Electrons quitAndInstall stänga appen och byta in den nya versionen. I en tidigare release oroade sig någon — rimligt nog — för att avsluta villkorslöst var riskabelt. Tänk om installationen felade? Vore det inte säkrare att avsluta bara om allt såg friskt ut?
Så koden fick en vakt som såg vettig ut:
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)
Logiken: avfyra installationen, vänta en halv sekund, och tvinga det slutliga app.quit() bara om flaggan updateDownloaded fortfarande är sann — annars håll appen öppen så att användaren inte lämnas strandsatt.
Fällan är en rad bort, i felhanteraren. Den hanteraren satte this.updateDownloaded = false. Så föreställ dig en misslyckad installation: händelsen error avfyras och rensar flaggan. Men quitAndInstall hade redan börjat nedrivningen — den hade stängt fönstren och tagit bort appens avslutningslyssnare. Sedan vaknar 500 ms-timern, kontrollerar den nu falska flaggan, beslutar "fel upptäckt, håll appen öppen" och hoppar över app.quit().
Nu har du, specifikt på macOS, det värsta möjliga tillståndet. macOS avslutar inte en app bara för att dess sista fönster stängdes — det är window-all-closed-beteendet som varje Mac-app förlitar sig på. Så processen är fortfarande vid liv, men den har inget fönster, ingen menyradsväg, och dess avslutningslyssnare har rivits ut. Det finns inget att klicka på. Cmd-Q har inget att prata med. Enda utvägen är Force Quit från Activity Monitor. "Säkerhets"-kontrollen hade förvandlat en misslyckad uppdatering — en återhämtningsbar irritation — till en zombie du inte kunde döda.
Fixen: nedrivningen måste vara villkorslös
Korrigeringen i v1.5.18 är nästan aggressivt tråkig, vilket är poängen. Den tar bort finurligheten:
- Ta bort lyssnarna
window-all-closedochbefore-quitsom kunde störa. - Förstör varje fönster —
window.destroy(), intewindow.close(). Stängning kan vetos av en hanterare; förstörelse kan inte. När du förbinder dig att stänga av frågar du inte artigt. - Anropa
quitAndInstall. - Anropa
app.quit()villkorslöst.
Ingen flagga, ingen timer, inget "håll den öppen för säkerhets skull". För sanningen om en avstängningsväg är att en halvfärdig avstängning är värre än något av utfallen. Att avsluta helt är okej. Att förbli helt öppen är okej. Det enda tillstånd du aldrig får nå är nedriven men fortfarande igång — och det är precis det tillstånd en villkorlig avslutning kan strandsätta dig i.
Två lärdomar till som samma vecka lärde ut
Den oavslutbara buggen är rubriken, men det sex releaser långa krånglet härdade två andra vanor värda att stjäla.
Filtrera din kraschtelemetri på exakta signaturer, inte breda nyckelord. Mitt i krånglet upptäckte vi att vår felrapportering var konfigurerad att slänga allt som innehöll ord som permission, token eller microphone — ett försök att skära ner brus som tyst svalde verkliga krascher som råkade nämna de orden. Vi slet ut de generella filtren och ersatte dem med exakta nekandesträngar (det specifika meddelande macOS avger när en behörighet nekas) och specifika övergående nätverkskoder som ERR_NETWORK_CHANGED. Brusreducering och buggdöljande är samma ratt vriden åt motsatta håll; om du filtrerar på känsla filtrerar du bort just det du behövde se.
Varje automatisk väg behöver en manuell nödutgång. Automatisk uppdatering är till sin natur bästa-möjliga-ansträngning — nätverk fladdrar, installationer misslyckas. Så varje felläge fick en mänsklig reservutväg: den manuella "Check for Updates"-knappen, återförsök med exponentiell backoff, en omkontrolltimer och — reserverad specifikt för fallet där en användares manuella försök misslyckades — ett meddelande i klarspråk "radera appen och installera om från webbplatsen". Den automatiska vägen är en bekvämlighet; den manuella vägen är garantin.
Slutsatsen
- En vakt runt en oåterkallelig handling är farligare än handlingen. Den villkorliga avslutningen försökte hindra en dålig uppdatering från att avsluta appen, och skapade istället ett tillstånd värre än att avsluta eller inte. Avstängnings- och installationsvägar bör vara villkorslösa och idempotenta — aldrig grindade på en föränderlig flagga som en annan hanterare kan vända bort under dig.
- På macOS är "inga fönster" inte "ingen app". All nedrivningslogik måste ta hänsyn till plattformen där en fönsterlös process fortsätter köra. Testa felvägen, på det faktiska operativsystemet, inte bara den lyckliga vägen.
- Funktionen du skeppar genom uppdateringssystemet kan inte testas genom uppdateringssystemet. Den asymmetrin är varför automatisk uppdatering förtjänar paranoid, villkorslös, tungt manuellt verifierad kod. Du får bara fixa det på det enkla sättet efter att det redan fungerar.
Detta är det tidigaste kapitlet i tillförlitlighetsarbetet som till slut blev GeekBye v2. För vart den vägen ledde, se vad en version 2 faktiskt kräver (v2.0.0) och hela bågen i anatomin av att skeppa mjukvara till perfektion.