Steven
Steven6 perc olvasás

A biztonsági ellenőrzés, amitől lehetetlen lett kilépni az appból

Az automatikus frissítés volt a legnehezebb funkció, amit valaha kiadtunk — négy nap alatt hat kiadás kellett, hogy megakadályozzuk az app tönkretételét. A legrosszabb hibát mi magunk okoztuk, miközben óvatosak próbáltunk lenni: egy 500 ezredmásodperces „biztonsági" ellenőrzés egy sikertelen frissítést olyan folyamattá változtatott, amelyből szó szerint nem lehetett kilépni.

Mérnöki munka
Electron
Megbízhatóság
GeekBye kiadások
A biztonsági ellenőrzés, amitől lehetetlen lett kilépni az appból

Minden asztali app fejlesztő pontosan egyszer becsüli alá az automatikus frissítést. Megoldott problémának tűnik — egy könyvtár letölt egy új verziót, és újraindítja az appodat. Aztán kiadod, és megtanulod, hogy az „indítsd újra az appot" az egyik legveszélyesebb dolog, amit egy programtól kérni lehet, mert pontosan abban a pillanatban történik, amikor az appod éppen lebontja önmagát, és a legkisebb a hibatűrése.

A GeekBye automatikus frissítése hat kiadást igényelt négy nap alatt — a v1.5.14-től a v1.5.19-ig —, hogy stabilizálódjon. Ez a történet az abban a szakaszban felmerült legrosszabb hibáról szól, amit mi magunk okoztunk azzal, hogy óvatosak próbáltunk lenni.

Hat kiadás, és az egy, amelyik számított

Az ív hétköznapian indult. A v1.5.14 egy kínos, elgépelés-szintű hibát javított: a frissítési feed egy nem létező GitHub repo névre mutatott, így a frissítő egy 404-et ellenőrzött. A v1.5.15 hozzáadott egy kézi „Check for Updates" gombot és egy valódi hibaüzenetet. Aztán elkezdődtek a quitAndInstall hibák, és gyorsan jöttek a kiadások — mert amikor a frissítési mechanizmusod hibás, nem tudod kiadni rá a javítást magán a frissítési mechanizmuson keresztül. Minden iteráció egy kézi-újratelepítéses szerencsejáték.

Az egy, amelyik számít, a v1.5.18. A teljes tartalma egyetlen commit volt, olyan címmel, amitől máig összerándulok: restore original quitAndInstall behavior to prevent unkillable app.

Hogyan tette tönkre az appot az „óvatosság"

Íme a felállás. Amikor egy frissítés letöltődik, az Electron quitAndInstall hívása állítólag bezárja az appot, és becseréli az új verziót. Egy korábbi kiadásban valaki — jogosan — aggódott amiatt, hogy a feltétel nélküli kilépés kockázatos. Mi van, ha a telepítés hibázik? Nem lenne biztonságosabb csak akkor kilépni, ha minden egészségesnek látszik?

Így a kód kapott egy őrt, ami értelmesnek tűnt:

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)

A logika: indítsd el a telepítést, várj fél másodpercet, és csak akkor kényszerítsd ki a végső app.quit() hívást, ha az updateDownloaded flag még mindig igaz — különben tartsd nyitva az appot, hogy a felhasználó ne maradjon egyedül.

A csapda egy sornyira van, a hibakezelőben. Az a kezelő beállította a this.updateDownloaded = false értéket. Szóval képzelj el egy sikertelen telepítést: az error esemény elsül, és törli a flaget. De a quitAndInstall már elkezdte a lebontást — bezárta az ablakokat, és eltávolította az app kilépési figyelőit. Aztán az 500 ms-os időzítő felébred, ellenőrzi a mostanra hamis flaget, úgy dönt, hogy „hiba észlelve, tartsd nyitva az appot", és kihagyja az app.quit() hívást.

Most, kifejezetten macOS-en, a lehető legrosszabb állapotod van. A macOS nem lép ki egy appból csak azért, mert az utolsó ablaka bezárult — ez az a window-all-closed viselkedés, amire minden Mac app támaszkodik. Így a folyamat még életben van, de nincs ablaka, nincs menüsor-útvonala, és a kilépési figyelőit kiszakították. Nincs mire kattintani. A Cmd-Q-nak nincs mivel beszélnie. Az egyetlen kiút a Force Quit az Activity Monitor-ból. A „biztonsági" ellenőrzés egy sikertelen frissítést — egy helyreállítható bosszúságot — olyan zombivá változtatott, amit nem tudtál megölni.

A megoldás: a lebontásnak feltétel nélkülinek kell lennie

A javítás a v1.5.18-ban szinte agresszíven unalmas, és pontosan ez a lényeg. Eltávolítja az ügyeskedést:

  1. Távolítsd el a window-all-closed és before-quit figyelőket, amelyek beavatkozhatnának.
  2. Semmisíts meg minden ablakot — window.destroy(), nem window.close(). A bezárást egy kezelő megvétózhatja; a megsemmisítést nem. Amikor elkötelezed magad a leállás mellett, nem kérsz udvariasan.
  3. Hívd meg a quitAndInstall függvényt.
  4. Hívd meg az app.quit() függvényt feltétel nélkül.

Semmi flag, semmi időzítő, semmi „hagyd nyitva a biztonság kedvéért". Mert az igazság egy leállási útvonalról az, hogy egy félbehagyott leállás rosszabb, mint bármelyik kimenet. A teljes kilépés rendben van. A teljes nyitva maradás rendben van. Az egyetlen állapot, amit soha nem szabad elérned, a lebontott, de még futó — és pontosan ez az az állapot, amelyben egy feltételes kilépés otthagyhat.

Két további tanulság, amit ugyanaz a hét megtanított

A nem leállítható app-hiba a főcím, de a hat kiadásos kapkodás két másik szokást is megerősített, amit érdemes ellopni.

Szűrd az összeomlás-telemetriádat pontos szignatúrák szerint, ne tág kulcsszavak szerint. A kapkodás közepén rájöttünk, hogy a hibajelentésünk úgy volt konfigurálva, hogy eldobjon bármit, ami olyan szavakat tartalmaz, mint permission, token vagy microphone — egy kísérlet a zaj csökkentésére, amely csendben lenyelte a valódi összeomlásokat, amelyek történetesen megemlítették ezeket a szavakat. Kiszedtük a takaró-szűrőket, és pontos elutasítási szövegekkel helyettesítettük őket (a konkrét üzenet, amit a macOS kibocsát, amikor egy engedélyt elutasítanak) és konkrét átmeneti hálózati kódokkal, mint az ERR_NETWORK_CHANGED. A zajcsökkentés és a hibaelrejtés ugyanaz a gomb, ellenkező irányba tekerve; ha érzés alapján szűrsz, kiszűröd azt, amit látnod kellett volna.

Minden automatikus útvonalnak szüksége van egy kézi vészkijáratra. Az automatikus frissítés természeténél fogva legjobb-szándékú — a hálózatok akadoznak, a telepítések hibáznak. Így minden hibamód kapott egy emberi tartalékot: a kézi „Check for Updates" gombot, exponenciális visszalépéses újrapróbálkozásokat, egy újraellenőrzési időzítőt, és — kifejezetten arra az esetre fenntartva, amikor egy felhasználó kézi próbálkozása sikertelen volt — egy közérthető „töröld az appot, és telepítsd újra a weboldalról" üzenetet. Az automatikus útvonal egy kényelmi funkció; a kézi útvonal a garancia.

A tanulság

  1. Egy őr egy visszafordíthatatlan művelet körül veszélyesebb, mint maga a művelet. A feltételes kilépés megpróbálta megakadályozni, hogy egy rossz frissítés kiléptesse az appot, és ehelyett egy olyan állapotot teremtett, amely rosszabb, mint akár a kilépés, akár a nem kilépés. A leállási és telepítési útvonalaknak feltétel nélkülinek és idempotensnek kell lenniük — soha nem szabad egy módosítható flaghez kötni, amit egy másik kezelő kicsúsztathat a lábad alól.
  2. macOS-en a „nincs ablak" nem azt jelenti, hogy „nincs app". Minden lebontási logikának számolnia kell azzal a platformmal, ahol egy ablak nélküli folyamat továbbra is fut. Teszteld a hibás útvonalat, a valódi operációs rendszeren, nem csak a boldog utat.
  3. A funkció, amit a frissítési rendszeren keresztül adsz ki, nem tesztelhető a frissítési rendszeren keresztül. Ez az aszimmetria az oka annak, hogy az automatikus frissítés paranoiás, feltétel nélküli, erősen kézzel-ellenőrzött kódot érdemel. Csak azután tudod a könnyű módon javítani, hogy már működik.

Ez a legkorábbi fejezete annak a megbízhatósági munkának, amelyből végül a GeekBye v2 lett. Hogy hová vezetett ez az út, lásd mit igényel valójában egy második verzió (v2.0.0) és a teljes ívet a szoftver tökéletességre juttatásának anatómiája című írásban.

Kapcsolódó cikkek

A tökéletesre csiszolt szoftverszállítás anatómiája: hogyan kapta el a kódellenőrzés azt, amit a tesztek nem
Steven
Steven6 perc olvasás

A tökéletesre csiszolt szoftverszállítás anatómiája: hogyan kapta el a kódellenőrzés azt, amit a tesztek nem

A GeekBye v2 sorozatban újra és újra ugyanaz történik: egy javítás átmegy minden teszten a fejlesztő gépén, majd a kódellenőrzés bebizonyítja, hogy szinte mindenki másnál elbukott volna. Ez a kilenc kiadás mögötti munkafolyamat — az ellenőrzési kapu, a javítás-előbb elkapott hibák és a szállítás-előtti tesztelési fegyelem, amely a „nálam működik” állapotot „működik” állapottá változtatja.

Mérnöki munka
Folyamat
Kódellenőrzés
A Mac-alkalmazásod megkapja a mikrofon-hozzáférést — aztán minden indításkor elfelejti
Steven
Steven5 perc olvasás

A Mac-alkalmazásod megkapja a mikrofon-hozzáférést — aztán minden indításkor elfelejti

A GeekBye mikrofonengedélyt kért, megadtad, és működött. A következő indításnál: eltűnt. És az alkalmazás egyáltalán meg sem jelent a Rendszerbeállítások → Mikrofon alatt. A bűnös egy macOS biztonsági funkció volt, amely csendben egy eltűnő elérési útról futtatta az alkalmazást — íme a diagnózis és az egyetlen kérdéssel megoldó javítás.

macOS
Engedélyek
Mérnöki munka
A nap, amikor az appunk saját magát DDoS-olta
Steven
Steven5 perc olvasás

A nap, amikor az appunk saját magát DDoS-olta

Egy backlognyi függőben lévő feltöltés, indításkor egyszerre elengedve, minden GeekBye klienst egy kis denial-of-service támadássá változtatott a saját szervereink ellen. A javítás — és a connection-liveness létra, amelynek megépítésére kényszerített — az egyik leghasznosabb dolog, amit a v2 tanított nekünk.

Megbízhatóság
Hálózatok
Mérnökség