Steven
Steven5 min čtení

Bezpečnostní kontrola, kvůli které nešla naše aplikace ukončit

Automatická aktualizace byla nejtěžší funkce, jakou jsme kdy vydali — šest vydání za čtyři dny, aby přestala odrovnávat aplikaci. Nejhorší chybu jsme si způsobili sami ve snaze být opatrní: 500milisekundová „bezpečnostní" kontrola, která z neúspěšné aktualizace udělala proces, který se doslova nedal ukončit.

Inženýrství
Electron
Spolehlivost
Vydání GeekBye
Bezpečnostní kontrola, kvůli které nešla naše aplikace ukončit

Každý vývojář desktopové aplikace podcení automatickou aktualizaci právě jednou. Vypadá to jako vyřešený problém — knihovna stáhne novou verzi a restartuje vaši aplikaci. Pak to vydáte a zjistíte, že „restartovat aplikaci" je jedna z nejnebezpečnějších věcí, o kterou lze program požádat, protože se to děje přesně ve chvíli, kdy se aplikace sama boří a má nejmenší prostor pro chybu.

Stabilizace automatické aktualizace v GeekBye zabrala šest vydání za čtyři dny — v1.5.14 až v1.5.19. Tohle je příběh nejhorší chyby v tomto úseku, kterou jsme si způsobili sami ve snaze být opatrní.

Šest vydání a to jediné, na kterém záleželo

Oblouk začal všedně. v1.5.14 opravila trapnou chybu třídy překlep: aktualizační kanál ukazoval na název GitHub repozitáře, který neexistoval, takže updater kontroloval 404. v1.5.15 přidala ruční tlačítko „Check for Updates" a skutečnou chybovou hlášku. Pak začaly chyby quitAndInstall a vydání přicházela rychle — protože když je váš aktualizační mechanismus rozbitý, nemůžete opravu poslat skrz aktualizační mechanismus. Každá iterace je sázka na ruční přeinstalaci.

Tím, na kterém záleží, je v1.5.18. Celý jeho obsah byl jediný commit s titulkem, ze kterého se mi pořád svírá žaludek: restore original quitAndInstall behavior to prevent unkillable app.

Jak „opatrnost" odrovnala aplikaci

Tady je zápletka. Když je aktualizace stažená, Electroní quitAndInstall má aplikaci zavřít a vyměnit ji za novou verzi. V dřívějším vydání někdo — rozumně — dostal obavu, že ukončit aplikaci bezpodmínečně je riskantní. Co když instalace skončí chybou? Nebylo by bezpečnější ukončit ji jen tehdy, když všechno vypadá v pořádku?

Tak kód narostl o pojistku, která vypadala rozumně:

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)

Logika: spustit instalaci, počkat půl sekundy a vynutit závěrečné app.quit() jen tehdy, pokud je příznak updateDownloaded stále true — jinak nechat aplikaci otevřenou, aby uživatel neuvízl.

Past je vzdálená jediný řádek, v obsluze chyby. Ta obsluha nastavovala this.updateDownloaded = false. Představte si tedy neúspěšnou instalaci: událost error se spustí a vymaže příznak. Jenže quitAndInstallzačal demontáž — zavřel okna a odebral posluchače ukončení aplikace. Pak se probudí 500ms časovač, zkontroluje nyní nepravdivý příznak, rozhodne se „error detected, keep the app open" a přeskočí app.quit().

A teď máte, konkrétně na macOS, nejhorší možný stav. macOS neukončí aplikaci jen proto, že se zavřelo její poslední okno — to je chování window-all-closed, na které spoléhá každá aplikace pro Mac. Takže proces je pořád naživu, ale nemá žádné okno, žádnou cestu přes lištu menu a jeho posluchače ukončení byly vytrhány. Není na co kliknout. Cmd-Q nemá s čím mluvit. Jediná cesta ven je Force Quit z Activity Monitor. „Bezpečnostní" kontrola přeměnila neúspěšnou aktualizaci — obtěžující, ale zotavitelnou — na zombie, kterou nešlo zabít.

Oprava: demontáž musí být bezpodmínečná

Náprava ve v1.5.18 je téměř agresivně nudná, což je celý smysl. Odstraňuje tu chytristiku:

  1. Odstranit posluchače window-all-closed a before-quit, kteří by mohli zasahovat.
  2. Zničit každé okno — window.destroy(), ne window.close(). Zavření může obsluha vetovat, zničení ne. Když se rozhodujete pro vypnutí, neptáte se zdvořile.
  3. Zavolat quitAndInstall.
  4. Zavolat app.quit() bezpodmínečně.

Žádný příznak, žádný časovač, žádné „nechme to otevřené pro jistotu". Protože pravda o vypínací cestě je taková, že napůl dokončené vypnutí je horší než kterýkoli z obou výsledků. Úplně ukončit je v pořádku. Úplně zůstat otevřený je v pořádku. Jediný stav, do kterého se nikdy nesmíte dostat, je zbouraný, ale stále běžící — a přesně do toho stavu vás podmíněné ukončení dokáže zaseknout.

Dvě další lekce, které nás naučil stejný týden

Neukončitelná chyba je hlavní titulek, ale ten spěch šesti vydání zocelil dva další návyky, které stojí za ukradení.

Filtrujte telemetrii pádů podle přesných signatur, ne širokých klíčových slov. Uprostřed shonu jsme zjistili, že naše hlášení chyb bylo nastavené tak, aby zahazovalo cokoli, co obsahovalo slova jako permission, token nebo microphone — pokus omezit šum, který ale tiše polykal skutečné pády, jež ta slova náhodou zmiňovaly. Plošné filtry jsme vytrhli a nahradili je přesnými řetězci zamítnutí (konkrétní hláškou, kterou macOS vydá při odepření oprávnění) a konkrétními přechodnými síťovými kódy jako ERR_NETWORK_CHANGED. Redukce šumu a skrývání chyb jsou tentýž knoflík otočený opačným směrem; když filtrujete podle dojmu, vyfiltrujete přesně to, co jste potřebovali vidět.

Každá automatická cesta potřebuje ruční záchrannou páku. Automatická aktualizace je ze své podstaty snaha o co nejlepší výsledek — sítě selhávají, instalace se nezdaří. Takže každý způsob selhání dostal lidskou záložní variantu: ruční tlačítko „Check for Updates", opakování s exponenciálním odstupem, časovač pro opětovnou kontrolu a — vyhrazenou konkrétně pro případ, kdy selhal ruční pokus uživatele — srozumitelnou hlášku „smažte aplikaci a přeinstalujte ji z webu". Automatická cesta je pohodlí; ruční cesta je záruka.

Ponaučení

  1. Pojistka kolem nevratné akce je nebezpečnější než ta akce samotná. Podmíněné ukončení se snažilo zabránit tomu, aby špatná aktualizace ukončila aplikaci, a místo toho vytvořilo stav horší, než je ukončení i neukončení. Cesty vypnutí a instalace by měly být bezpodmínečné a idempotentní — nikdy podmíněné mutovatelným příznakem, který vám jiná obsluha může podtrhnout pod nohama.
  2. Na macOS „žádná okna" neznamená „žádná aplikace". Jakákoli logika demontáže musí počítat s platformou, kde proces bez oken běží dál. Otestujte cestu selhání, na skutečném OS, ne jen šťastnou cestu.
  3. Funkci, kterou vydáváte přes aktualizační systém, nelze přes aktualizační systém otestovat. Právě tato asymetrie je důvod, proč si automatická aktualizace zaslouží paranoidní, bezpodmínečný a důkladně ručně ověřený kód. Opravit ji tou snadnou cestou dostanete šanci až potom, co už funguje.

Tohle je nejranější kapitola práce na spolehlivosti, ze které se nakonec stal GeekBye v2. Kam ta cesta vedla, se dočtete v what a version 2 actually takes (v2.0.0) a celý oblouk v the anatomy of shipping software to perfection.

Související články

Co Skutečně Obnáší Verze 2: 206 Commitů Poctivých Stavů
Steven
Steven6 min čtení

Co Skutečně Obnáší Verze 2: 206 Commitů Poctivých Stavů

GeekBye v2 nebylo vydání s novými funkcemi. Bylo to 206 commitů namířených k jediné myšlence: aplikace by nikdy neměla lhát o svém vlastním stavu. Tady je, co to stojí — včetně jednořádkové chyby v lockfile, která nás málem zastavila předtím, než jsme z toho všeho vůbec něco vydali.

Spolehlivost
Inženýrství
Vydání
Vaše Mac aplikace získá přístup k mikrofonu — a pak ho při každém spuštění zapomene
Steven
Steven5 min čtení

Vaše Mac aplikace získá přístup k mikrofonu — a pak ho při každém spuštění zapomene

GeekBye požádal o oprávnění k mikrofonu, udělili jste ho a fungovalo to. Při dalším spuštění: pryč. A aplikace se v Nastavení systému → Mikrofon vůbec neobjevila. Viníkem byla bezpečnostní funkce macOS, která aplikaci tiše spouštěla z cesty, jež mizí — tady je diagnóza a oprava pomocí jediné výzvy.

macOS
Oprávnění
Inženýrství
Jedna proměnná CSS, pět kol revize a Swift toolchain, který lhal
Steven
Steven6 min čtení

Jedna proměnná CSS, pět kol revize a Swift toolchain, který lhal

GeekBye v2.0.7 udělal celý overlay nastavitelně průsvitným — což zní jako jednořádková změna v CSS a rozhodně nebylo. Skutečný příběh je to, co odhalila revize kódu: dvě plochy, které odmítaly zprůhlednět, nahrávací lišta, jež vypadala špatně při stejné neprůhlednosti, a zkompilovaná binárka, která poskočila o 672 bajtů, protože naše vlastní dokumentace řekla revidujícímu špatnou verzi Swiftu.

Inženýrství
Design
Build