Steven
Steven5 min. skaitymo

Saugumo patikra, dėl kurios mūsų programos nebuvo įmanoma uždaryti

Automatinis atnaujinimas buvo sunkiausia funkcija, kokią kada nors išleidome — šeši leidimai per keturias dienas, kad jis nustotų gadinti programą. Blogiausią klaidą įvėlėme patys, stengdamiesi būti atsargūs: 500 milisekundžių „saugumo" patikra, kuri nepavykusį atnaujinimą paversdavo procesu, kurio tiesiogine prasme nebuvo įmanoma uždaryti.

Inžinerija
Electron
Patikimumas
GeekBye leidimai
Saugumo patikra, dėl kurios mūsų programos nebuvo įmanoma uždaryti

Kiekvienas darbalaukio programų kūrėjas tiksliai vieną kartą neįvertina automatinio atnaujinimo. Jis atrodo kaip išspręsta problema — biblioteka atsisiunčia naują versiją ir paleidžia programą iš naujo. Paskui tai išleidi ir sužinai, kad „paleisk programą iš naujo" yra vienas pavojingiausių dalykų, kurių galima paprašyti programos, nes tai nutinka lygiai tuo momentu, kai programa ardo pati save ir turi mažiausią atsargą klaidai.

GeekBye automatiniam atnaujinimui prireikė šešių leidimų per keturias dienas — nuo v1.5.14 iki v1.5.19 — kol jis stabilizavosi. Tai istorija apie blogiausią to atkarpos klaidą, kurią patys ir sukėlėme stengdamiesi būti atsargūs.

Šeši leidimai ir tas vienas, kuris turėjo reikšmės

Lankas prasidėjo kasdieniškai. v1.5.14 ištaisė gėdingą korektūros lygio klaidą: atnaujinimų srautas rodė į GitHub saugyklos pavadinimą, kurio nebuvo, tad atnaujintuvas tikrino 404. v1.5.15 pridėjo rankinį mygtuką „Check for Updates" ir tikrą klaidos pranešimą. Paskui prasidėjo quitAndInstall klaidos, ir leidimai ėjo greitai — nes kai tavo atnaujinimo mechanizmas sugedęs, jo pataisos negali išleisti per atnaujinimo mechanizmą. Kiekviena iteracija — tai lošimas dėl rankinio įdiegimo iš naujo.

Tas, kuris turi reikšmės, yra v1.5.18. Visą jo turinį sudarė vienas commitas su pavadinimu, nuo kurio iki šiol susiraukiu: restore original quitAndInstall behavior to prevent unkillable app.

Kaip „atsargumas" sugadino programą

Štai padėtis. Kai atnaujinimas atsisiųstas, Electron quitAndInstall turi uždaryti programą ir pakeisti ją nauja versija. Ankstesniame leidime kažkas — visai pagrįstai — sunerimo, kad uždaryti besąlygiškai rizikinga. O jei diegimas suklys? Ar nebūtų saugiau uždaryti tik tada, kai viskas atrodo sveika?

Tad kodas apaugo apsauga, kuri atrodė protinga:

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: paleisk diegimą, palauk pusę sekundės ir priversk galutinį app.quit() tik tada, kai vėliavėlė updateDownloaded vis dar teisinga — kitaip palik programą atvirą, kad naudotojas neliktų nieko.

Spąstai yra viena eilute toliau, klaidos tvarkyklėje. Ta tvarkyklė nustatydavo this.updateDownloaded = false. Tad įsivaizduok nepavykusį diegimą: error įvykis įsijungia ir išvalo vėliavėlę. Bet quitAndInstall jau buvo pradėjęs ardymą — jis uždarė langus ir pašalino programos uždarymo klausytojus. Paskui pabunda 500 ms laikmatis, patikrina dabar jau klaidingą vėliavėlę, nusprendžia „aptikta klaida, palik programą atvirą" ir praleidžia app.quit().

Dabar turi, konkrečiai macOS aplinkoje, blogiausią galimą būseną. macOS neuždaro programos vien todėl, kad užsidarė paskutinis jos langas — tai window-all-closed elgsena, kuria remiasi kiekviena Mac programa. Tad procesas vis dar gyvas, bet neturi jokio lango, jokio kelio per meniu juostą, o jo uždarymo klausytojai išrauti. Nėra į ką spustelėti. Cmd-Q neturi su kuo kalbėtis. Vienintelė išeitis — Force Quit iš Activity Monitor. „Saugumo" patikra pavertė nepavykusį atnaujinimą — pataisomą nemalonumą — zombiu, kurio nebuvo įmanoma nužudyti.

Sprendimas: ardymas turi būti besąlygiškas

Pataisa v1.5.18 yra beveik agresyviai nuobodi, ir čia visa esmė. Ji pašalina gudrumą:

  1. Pašalink window-all-closed ir before-quit klausytojus, kurie galėtų trukdyti.
  2. Sunaikink kiekvieną langą — window.destroy(), o ne window.close(). Uždarymą tvarkyklė gali vetuoti; sunaikinimo — ne. Kai įsipareigoji išsijungti, mandagiai neklausi.
  3. Iškviesk quitAndInstall.
  4. Iškviesk app.quit() besąlygiškai.

Jokios vėliavėlės, jokio laikmačio, jokio „palik atvirą dėl visa ko". Nes tiesa apie išjungimo šaką yra tokia: pusiau užbaigtas išjungimas yra blogiau nei bet kuri iš baigčių. Visiškai užsidaryti — gerai. Visiškai likti atviram — gerai. Vienintelė būsena, kurios niekada negalima pasiekti, yra išardytas, bet vis dar veikiantis — ir tai lygiai ta būsena, kurioje sąlyginis uždarymas gali tave įstrigdyti.

Dar dvi tos pačios savaitės pamokos

Neuždaromos programos klaida yra antraštė, bet šešių leidimų lenktynės užgrūdino dar du įpročius, kuriuos verta pasisavinti.

Filtruok lūžių telemetriją pagal tikslias signatūras, o ne plačius raktažodžius. Lenktynių įkarštyje aptikome, kad mūsų klaidų ataskaitos buvo sukonfigūruotos atmesti viską, kas turi žodžių kaip permission, token ar microphone — bandymas nupjauti triukšmą, kuris tyliai rijo tikrus lūžius, atsitiktinai paminėjusius tuos žodžius. Išrovėme antklodinius filtrus ir pakeitėme juos tiksliomis atmetimo eilutėmis (konkrečiu pranešimu, kurį macOS pateikia atmetus leidimą) ir konkrečiais laikinaisiais tinklo kodais kaip ERR_NETWORK_CHANGED. Triukšmo mažinimas ir klaidų slėpimas yra ta pati rankenėlė, pasukta priešingomis kryptimis; jei filtruoji pagal nuojautą, atfiltruosi būtent tai, ką reikėjo pamatyti.

Kiekvienai automatinei šakai reikia rankinio avarinio išėjimo. Automatinis atnaujinimas savo prigimtimi yra geriausios pastangos — tinklai strigsta, diegimai žlunga. Tad kiekvienas gedimo režimas gavo žmogišką atsarginį variantą: rankinį mygtuką „Check for Updates", pakartojimus su eksponentiniu atidėjimu, pakartotinės patikros laikmatį ir — skirtą būtent atvejui, kai rankinis naudotojo bandymas žlugo — paprastais žodžiais suformuluotą pranešimą „ištrink programą ir įdiek iš naujo iš svetainės". Automatinė šaka yra patogumas; rankinė šaka yra garantija.

Išvada

  1. Apsauga aplink negrįžtamą veiksmą yra pavojingesnė nei pats veiksmas. Sąlyginis uždarymas mėgino neleisti blogam atnaujinimui uždaryti programos, o vietoj to sukūrė būseną, blogesnę nei uždarymas ar neuždarymas. Išjungimo ir diegimo šakos turi būti besąlygiškos ir idempotentiškos — niekada nesusietos su kintama vėliavėle, kurią kita tvarkyklė gali ištraukti tau iš po kojų.
  2. macOS aplinkoje „nėra langų" nėra „nėra programos". Bet kokia ardymo logika turi atsižvelgti į platformą, kur procesas be langų veikia toliau. Testuok gedimo šaką tikroje OS, o ne tik laimingą šaką.
  3. Funkcijos, kurią išleidi per atnaujinimo sistemą, negalima ištestuoti per atnaujinimo sistemą. Būtent ši asimetrija ir yra priežastis, kodėl automatinis atnaujinimas nusipelno paranojiško, besąlygiško, kruopščiai rankiniu būdu patikrinto kodo. Lengvą taisymo būdą gauni tik po to, kai jis jau veikia.

Tai ankstyviausias patikimumo darbo skyrius, kuris galiausiai tapo GeekBye v2. Kur nuvedė tas kelias, skaityk ko iš tiesų kainuoja antra versija (v2.0.0) ir visą lanką programinės įrangos tobulinimo iki tobulybės anatomijoje.