
Sikkerhedstjekket der gjorde vores app umulig at afslutte
Automatisk opdatering var den sværeste funktion, vi nogensinde har frigivet — seks udgivelser på fire dage for at forhindre den i at ødelægge appen. Den værste fejl var en, vi selv indførte i forsøget på at være forsigtige: et 500-millisekunders "sikkerheds"-tjek, der forvandlede en mislykket opdatering til en proces, du bogstaveligt talt ikke kunne afslutte.
Enhver desktop-appudvikler undervurderer automatisk opdatering præcis én gang. Det ligner et løst problem — et bibliotek downloader en ny version og genstarter din app. Så frigiver du det, og du lærer, at "genstart din app" er en af de farligste ting, et program kan blive bedt om at gøre, fordi det sker i præcis det øjeblik, hvor din app river sig selv ned og har mindst fejlmargen.
GeekByes automatiske opdatering tog seks udgivelser på fire dage — v1.5.14 til v1.5.19 — at stabilisere. Dette er historien om den værste fejl i den strækning, som vi selv forårsagede ved at forsøge at være forsigtige.
Seks udgivelser, og den ene der betød noget
Buen begyndte trivielt. v1.5.14 rettede en pinlig stavefejlsklasset bug: opdateringsfeedet pegede på et GitHub-repositorienavn, der ikke fandtes, så opdateringsprogrammet tjekkede en 404. v1.5.15 tilføjede en manuel "Check for Updates"-knap og en rigtig fejlmeddelelse. Så begyndte quitAndInstall-fejlene, og udgivelserne kom hurtigt — for når din opdateringsmekanisme er i stykker, kan du ikke frigive rettelsen til den gennem opdateringsmekanismen. Hver iteration er et manuelt geninstallations-hasardspil.
Den, der betyder noget, er v1.5.18. Hele dens indhold var en enkelt commit med en titel, der stadig får mig til at krympe mig: restore original quitAndInstall behavior to prevent unkillable app.
Hvordan "at være forsigtig" ødelagde appen
Her er opsætningen. Når en opdatering er downloadet, skal Electrons quitAndInstall lukke appen og bytte den nye version ind. I en tidligere udgivelse bekymrede nogen sig — med rimelighed — for, at det var risikabelt at afslutte betingelsesløst. Hvad hvis installationen fejlede? Ville det ikke være sikrere kun at afslutte, hvis alt så sundt ud?
Så koden voksede en vagt, der så fornuftig ud:
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)
Logikken: affyr installationen, vent et halvt sekund, og tving den endelige app.quit() kun, hvis flaget updateDownloaded stadig er sandt — ellers hold appen åben, så brugeren ikke bliver efterladt strandet.
Fælden er én linje væk, i fejlhåndteringen. Den håndtering satte this.updateDownloaded = false. Så forestil dig en mislykket installation: hændelsen error udløses og rydder flaget. Men quitAndInstall var allerede begyndt nedrivningen — den havde lukket vinduerne og fjernet appens afslutningslyttere. Så vågner 500 ms-timeren, tjekker det nu falske flag, beslutter "fejl opdaget, hold appen åben" og springer app.quit() over.
Nu har du, specifikt på macOS, den værst mulige tilstand. macOS afslutter ikke en app, bare fordi dens sidste vindue lukkede — det er window-all-closed-adfærden, som enhver Mac-app forlader sig på. Så processen er stadig i live, men den har intet vindue, ingen menulinjevej, og dens afslutningslyttere er revet ud. Der er intet at klikke på. Cmd-Q har intet at tale med. Eneste udvej er Force Quit fra Activity Monitor. "Sikkerheds"-tjekket havde forvandlet en mislykket opdatering — en genoprettelig irritation — til en zombie, du ikke kunne dræbe.
Rettelsen: nedrivning skal være betingelsesløs
Korrektionen i v1.5.18 er næsten aggressivt kedelig, hvilket er pointen. Den fjerner smartheden:
- Fjern lytterne
window-all-closedogbefore-quit, der kunne blande sig. - Ødelæg hvert vindue —
window.destroy(), ikkewindow.close(). Lukning kan blokeres med veto af en håndtering; ødelæggelse kan ikke. Når du forpligter dig til at lukke ned, spørger du ikke høfligt. - Kald
quitAndInstall. - Kald
app.quit()betingelsesløst.
Intet flag, ingen timer, intet "hold den åben for en sikkerheds skyld". For sandheden om en nedlukningsvej er, at en halvfærdig nedlukning er værre end begge udfald. At afslutte helt er fint. At forblive helt åben er fint. Den eneste tilstand, du aldrig må nå, er nedrevet, men stadig kørende — og det er præcis den tilstand, en betinget afslutning kan strande dig i.
To lærdomme mere, som samme uge lærte
Den uafsluttelige bug er overskriften, men de seks udgivelsers rod hærdede to andre vaner, der er værd at stjæle.
Filtrér din nedbrudstelemetri på nøjagtige signaturer, ikke brede nøgleord. Midt i rodet opdagede vi, at vores fejlrapportering var konfigureret til at kassere alt, der indeholdt ord som permission, token eller microphone — et forsøg på at skære støj væk, der stille slugte rigtige nedbrud, som tilfældigvis nævnte de ord. Vi rev de brede filtre ud og erstattede dem med nøjagtige afvisningsstrenge (den specifikke meddelelse, macOS afgiver, når en tilladelse afvises) og specifikke forbigående netværkskoder som ERR_NETWORK_CHANGED. Støjreduktion og fejlskjulning er den samme knap drejet i modsatte retninger; hvis du filtrerer på fornemmelse, filtrerer du netop det bort, du havde brug for at se.
Enhver automatisk vej har brug for en manuel nødudgang. Automatisk opdatering er af natur bedst-mulige-indsats — netværk flakker, installationer fejler. Så hver fejltilstand fik en menneskelig reserveløsning: den manuelle "Check for Updates"-knap, genforsøg med eksponentiel backoff, en gentjek-timer og — reserveret specifikt til tilfældet, hvor en brugers manuelle forsøg fejlede — en klarsproget meddelelse "slet appen og geninstallér fra webstedet". Den automatiske vej er en bekvemmelighed; den manuelle vej er garantien.
Konklusionen
- En vagt omkring en uigenkaldelig handling er farligere end handlingen. Den betingede afslutning forsøgte at forhindre en dårlig opdatering i at afslutte appen og skabte i stedet en tilstand værre end at afslutte eller ej. Nedluknings- og installationsveje bør være betingelsesløse og idempotente — aldrig gated på et foranderligt flag, som en anden håndtering kan vende bort under dig.
- På macOS er "ingen vinduer" ikke "ingen app". Al nedrivningslogik skal tage højde for platformen, hvor en vinduesløs proces bliver ved med at køre. Test fejlvejen, på det faktiske operativsystem, ikke kun den lykkelige vej.
- Funktionen, du frigiver gennem opdateringssystemet, kan ikke testes gennem opdateringssystemet. Den asymmetri er grunden til, at automatisk opdatering fortjener paranoid, betingelsesløs, tungt manuelt verificeret kode. Du får kun lov til at rette det på den nemme måde efter, at det allerede virker.
Dette er det tidligste kapitel i det pålidelighedsarbejde, der til sidst blev GeekBye v2. For hvor den vej førte hen, se hvad en version 2 faktisk kræver (v2.0.0) og hele buen i anatomien af at frigive software til perfektion.