Steven
Steven5 min lesing

Sikkerhetssjekken som gjorde appen vår umulig å avslutte

Automatisk oppdatering var den vanskeligste funksjonen vi noensinne har sluppet — seks utgivelser på fire dager for å hindre den i å ødelegge appen. Den verste feilen var en vi selv innførte i forsøket på å være forsiktige: en 500-millisekunders "sikkerhets"-sjekk som forvandlet en mislykket oppdatering til en prosess du bokstavelig talt ikke kunne avslutte.

Ingeniørarbeid
Electron
Pålitelighet
GeekBye-utgivelser
Sikkerhetssjekken som gjorde appen vår umulig å avslutte

Hver skrivebordsapputvikler undervurderer automatisk oppdatering nøyaktig én gang. Det ser ut som et løst problem — et bibliotek laster ned en ny versjon og starter appen din på nytt. Så slipper du det, og du lærer at "start appen din på nytt" er en av de farligste tingene et program kan bli bedt om å gjøre, fordi det skjer i akkurat det øyeblikket appen din river seg selv ned og har minst feilmargin.

GeekByes automatiske oppdatering tok seks utgivelser på fire dager — v1.5.14 til v1.5.19 — å stabilisere. Dette er historien om den verste feilen i den strekningen, som vi selv forårsaket ved å prøve å være forsiktige.

Seks utgivelser, og den ene som betød noe

Buen begynte trivielt. v1.5.14 fikset en pinlig skrivefeilsklasset feil: oppdateringsstrømmen pekte på et GitHub-repositorienavn som ikke fantes, så oppdatereren sjekket en 404. v1.5.15 la til en manuell "Check for Updates"-knapp og en ekte feilmelding. Så begynte quitAndInstall-feilene, og utgivelsene kom raskt — for når oppdateringsmekanismen din er ødelagt, kan du ikke slippe fiksen for den gjennom oppdateringsmekanismen. Hver iterasjon er et manuelt reinstallasjons-sjansespill.

Den som betyr noe, er v1.5.18. Hele innholdet var en enkelt commit med en tittel som fortsatt får meg til å krympe meg: restore original quitAndInstall behavior to prevent unkillable app.

Hvordan "å være forsiktig" ødela appen

Her er oppsettet. Når en oppdatering er lastet ned, skal Electrons quitAndInstall lukke appen og bytte inn den nye versjonen. I en tidligere utgivelse bekymret noen seg — med rette — for at det var risikabelt å avslutte betingelsesløst. Hva om installasjonen feilet? Ville det ikke være tryggere å avslutte bare hvis alt så friskt ut?

Så koden vokste en vakt som så fornuftig 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)

Logikken: fyr av installasjonen, vent et halvt sekund, og tving den endelige app.quit() bare hvis flagget updateDownloaded fortsatt er sant — ellers hold appen åpen så brukeren ikke blir strandet.

Fellen er én linje unna, i feilhåndtereren. Den håndtereren satte this.updateDownloaded = false. Så se for deg en mislykket installasjon: hendelsen error utløses og tømmer flagget. Men quitAndInstall hadde allerede begynt nedrivningen — den hadde lukket vinduene og fjernet appens avslutningslyttere. Så våkner 500 ms-timeren, sjekker det nå falske flagget, bestemmer "feil oppdaget, hold appen åpen" og hopper over app.quit().

Nå har du, spesifikt på macOS, den verst mulige tilstanden. macOS avslutter ikke en app bare fordi det siste vinduet ble lukket — det er window-all-closed-oppførselen som hver Mac-app stoler på. Så prosessen er fortsatt i live, men den har ingen vindu, ingen menylinjevei, og avslutningslytterne er revet ut. Det er ingenting å klikke på. Cmd-Q har ingenting å snakke med. Eneste utvei er Force Quit fra Activity Monitor. "Sikkerhets"-sjekken hadde forvandlet en mislykket oppdatering — en gjenopprettelig irritasjon — til en zombie du ikke kunne drepe.

Fiksen: nedrivning må være betingelsesløs

Korrigeringen i v1.5.18 er nesten aggressivt kjedelig, som er poenget. Den fjerner smartheten:

  1. Fjern lytterne window-all-closed og before-quit som kunne blande seg.
  2. Ødelegg hvert vindu — window.destroy(), ikke window.close(). Lukking kan vetos av en håndterer; ødeleggelse kan ikke. Når du forplikter deg til å stenge ned, spør du ikke høflig.
  3. Kall quitAndInstall.
  4. Kall app.quit() betingelsesløst.

Ingen flagg, ingen timer, ingen "hold den åpen for sikkerhets skyld". For sannheten om en nedstengningsvei er at en halvferdig nedstengning er verre enn begge utfallene. Å avslutte helt er greit. Å forbli helt åpen er greit. Den eneste tilstanden du aldri må nå, er nedrevet, men fortsatt kjørende — og det er nettopp den tilstanden en betinget avslutning kan strande deg i.

To lærdommer til som samme uke lærte

Den uavsluttelige feilen er overskriften, men det seks utgivelser lange rotet herdet to andre vaner verdt å stjele.

Filtrer krasjtelemetrien din på nøyaktige signaturer, ikke brede nøkkelord. Midt i rotet oppdaget vi at feilrapporteringen vår var konfigurert til å forkaste alt som inneholdt ord som permission, token eller microphone — et forsøk på å kutte støy som stille slukte ekte krasj som tilfeldigvis nevnte de ordene. Vi rev ut de brede filtrene og erstattet dem med nøyaktige avvisningsstrenger (den spesifikke meldingen macOS avgir når en tillatelse avslås) og spesifikke forbigående nettverkskoder som ERR_NETWORK_CHANGED. Støyreduksjon og feilskjuling er den samme bryteren vridd i motsatte retninger; hvis du filtrerer på følelse, filtrerer du bort nettopp det du trengte å se.

Hver automatisk vei trenger en manuell nødutgang. Automatisk oppdatering er av natur beste-innsats — nettverk flakker, installasjoner feiler. Så hver feilmodus fikk en menneskelig reserveløsning: den manuelle "Check for Updates"-knappen, nye forsøk med eksponentiell backoff, en gjensjekk-timer og — reservert spesifikt for tilfellet der en brukers manuelle forsøk feilet — en klarspråklig melding "slett appen og reinstaller fra nettstedet". Den automatiske veien er en bekvemmelighet; den manuelle veien er garantien.

Konklusjonen

  1. En vakt rundt en ugjenkallelig handling er farligere enn handlingen. Den betingede avslutningen prøvde å hindre en dårlig oppdatering i å avslutte appen, og skapte i stedet en tilstand verre enn å avslutte eller ikke. Nedstengnings- og installasjonsveier bør være betingelsesløse og idempotente — aldri gated på et foranderlig flagg som en annen håndterer kan snu bort under deg.
  2. På macOS er "ingen vinduer" ikke "ingen app". All nedrivningslogikk må ta hensyn til plattformen der en vindusløs prosess fortsetter å kjøre. Test feilveien, på det faktiske operativsystemet, ikke bare den lykkelige veien.
  3. Funksjonen du slipper gjennom oppdateringssystemet kan ikke testes gjennom oppdateringssystemet. Den asymmetrien er grunnen til at automatisk oppdatering fortjener paranoid, betingelsesløs, tungt manuelt verifisert kode. Du får bare fikse det på den enkle måten etter at det allerede fungerer.

Dette er det tidligste kapittelet i pålitelighetsarbeidet som til slutt ble GeekBye v2. For hvor den veien førte, se hva en versjon 2 faktisk krever (v2.0.0) og hele buen i anatomien av å slippe programvare til perfeksjon.

Relaterte artikler

Anatomien til å levere programvare til perfeksjon: hvordan kodegjennomgang fanget det testene ikke kunne
Steven
Steven6 min lesing

Anatomien til å levere programvare til perfeksjon: hvordan kodegjennomgang fanget det testene ikke kunne

Gjennom hele GeekBye v2-serien skjer det samme igjen og igjen: et fiks består hver eneste test på utviklerens maskin, og så beviser kodegjennomgangen at det ville ha feilet for nesten alle andre. Dette er arbeidsflyten bak ni utgivelser — gjennomgangsporten, fiks-først-funnene og test-før-levering-disiplinen som gjør «det virker hos meg» om til «det virker».

Ingeniørarbeid
Prosess
Kodegjennomgang
Mac-appen din gir mikrofontilgang — og glemmer den ved hver oppstart
Steven
Steven6 min lesing

Mac-appen din gir mikrofontilgang — og glemmer den ved hver oppstart

GeekBye ba om mikrofontillatelse, du ga den, og det virket. Neste oppstart: borte. Og appen dukket aldri opp i Systeminnstillinger → Mikrofon i det hele tatt. Synderen var en macOS-sikkerhetsfunksjon som stille kjørte appen fra en sti som forsvinner — her er diagnosen og løsningen med ett enkelt trykk.

macOS
Tillatelser
Utvikling
Én CSS-variabel, fem runder med kodegjennomgang, og en Swift-verktøykjede som løy
Steven
Steven7 min lesing

Én CSS-variabel, fem runder med kodegjennomgang, og en Swift-verktøykjede som løy

GeekBye v2.0.7 gjorde hele overlegget justerbart gjennomskinnelig — noe som høres ut som en CSS-endring på én linje, og det var det absolutt ikke. Den egentlige historien er hva kodegjennomgangen fanget opp: to flater som nektet å tone ut, en opptakslinje som så feil ut ved samme gjennomsiktighet, og en kompilert binærfil som vaklet frem og tilbake med 672 byte fordi vår egen dokumentasjon fortalte en anmelder feil Swift-versjon.

Utvikling
Design
Bygg