Steven
Steven6 min de lectura

La comprovació de seguretat que va fer impossible tancar la nostra aplicació

L'actualització automàtica va ser la funció més difícil que hem publicat mai — sis versions en quatre dies per evitar que deixés l'aplicació inservible. El pitjor error va ser un que vam introduir nosaltres mateixos intentant anar amb compte: una comprovació de "seguretat" de 500 mil·lisegons que convertia una actualització fallida en un procés que literalment no podies tancar.

Enginyeria
Electron
Fiabilitat
GeekBye Releases
La comprovació de seguretat que va fer impossible tancar la nostra aplicació

Cada desenvolupador d''aplicacions d''escriptori subestima l''actualització automàtica exactament una vegada. Sembla un problema resolt — una biblioteca baixa una versió nova i reinicia la teva aplicació. Després la publiques, i aprens que "reiniciar la teva aplicació" és una de les coses més perilloses que se li pot demanar a un programa, perquè passa exactament en el moment en què la teva aplicació s''està desmuntant i té el mínim marge d''error.

L''actualització automàtica de GeekBye va necessitar sis versions en quatre dies — v1.5.14 fins a v1.5.19 — per estabilitzar-se. Aquesta és la història del pitjor error d''aquell tram, que vam causar nosaltres mateixos intentant anar amb compte.

Sis versions, i la que importava

El tram va començar de manera anodina. v1.5.14 va corregir un error vergonyós de la mena d''una errada tipogràfica: el feed d''actualitzacions apuntava a un nom de repositori de GitHub que no existia, així que l''actualitzador consultava un 404. v1.5.15 va afegir un botó manual "Check for Updates" i un missatge d''error real. Després van començar els errors de quitAndInstall, i les versions van arribar de pressa — perquè quan el teu mecanisme d''actualització està trencat, no pots publicar-ne la solució a través del mecanisme d''actualització. Cada iteració és una travessa amb reinstal·lació manual.

La que importa és v1.5.18. Tot el seu contingut era un únic commit amb un títol que encara em fa arrufar el nas: restore original quitAndInstall behavior to prevent unkillable app.

Com "anar amb compte" va deixar l''aplicació inservible

Aquest és el plantejament. Quan una actualització s''ha baixat, el quitAndInstall d''Electron ha de tancar l''aplicació i intercanviar-la per la versió nova. En una versió anterior, algú — amb raó — es va preocupar que tancar incondicionalment fos arriscat. I si la instal·lació donava error? No seria més segur tancar només si tot semblava en bon estat?

Així que el codi va fer créixer una comprovació que semblava assenyada:

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)

La lògica: dispara la instal·lació, espera mig segon, i força l''app.quit() final només si la bandera updateDownloaded encara és certa — si no, mantén l''aplicació oberta perquè l''usuari no quedi encallat.

El parany és a una línia de distància, al gestor d''errors. Aquell gestor posava this.updateDownloaded = false. Així que imagina''t una instal·lació fallida: l''esdeveniment error salta i esborra la bandera. Però quitAndInstall ja havia començat el desmuntatge — havia tancat les finestres i eliminat els oients de tancament de l''aplicació. Després el temporitzador de 500 ms es desperta, comprova la bandera ara falsa, decideix "error detectat, mantén l''aplicació oberta," i s''salta app.quit().

Ara tens, específicament a macOS, el pitjor estat possible. macOS no tanca una aplicació només perquè s''hagi tancat la seva última finestra — és el comportament window-all-closed en què confia cada aplicació de Mac. Així que el procés encara és viu, però no té cap finestra, cap camí per la barra de menús, i els seus oients de tancament han estat arrencats. No hi ha res on clicar. Cmd-Q no té res amb què parlar. L''única sortida és Force Quit des de l''Activity Monitor. La comprovació de "seguretat" havia convertit una actualització fallida — una molèstia recuperable — en un zombi que no podies matar.

La solució: el desmuntatge ha de ser incondicional

La correcció a v1.5.18 és gairebé agressivament avorrida, i aquesta és la qüestió. Elimina l''enginy:

  1. Elimina els oients window-all-closed i before-quit que podrien interferir.
  2. Destrueix cada finestra — window.destroy(), no window.close(). El tancament pot ser vetat per un gestor; la destrucció no. Quan et compromets a apagar-ho tot, no ho demanes amb educació.
  3. Crida quitAndInstall.
  4. Crida app.quit() incondicionalment.

Cap bandera, cap temporitzador, cap "mantingues-la oberta per si de cas." Perquè la veritat sobre un camí d''apagada és que una apagada a mig fer és pitjor que qualsevol dels dos resultats. Tancar del tot està bé. Quedar-se obert del tot està bé. L''únic estat que mai no has d''arribar a assolir és desmuntat però encara en execució — i aquest és exactament l''estat en què un tancament condicional et pot deixar encallat.

Dues lliçons més que ens va ensenyar la mateixa setmana

L''error impossible d''aturar és el titular, però l''enrenou de sis versions va endurir dos altres hàbits que val la pena robar.

Filtra la telemetria de caigudes per signatures exactes, no per paraules clau àmplies. Enmig de l''enrenou vam descobrir que el nostre informe d''errors estava configurat per descartar qualsevol cosa que contingués paraules com ara permission, token o microphone — un intent de retallar soroll que estava empassant-se silenciosament caigudes reals que casualment mencionaven aquestes paraules. Vam arrencar els filtres generals i els vam substituir per cadenes de denegació exactes (el missatge específic que macOS emet quan es denega un permís) i codis de xarxa transitoris específics com ara ERR_NETWORK_CHANGED. La reducció de soroll i l''ocultació d''errors són el mateix botó girat en sentits oposats; si filtres per intuïció, filtraràs justament allò que necessitaves veure.

Cada camí automàtic necessita una sortida d''emergència manual. L''actualització automàtica és, per naturalesa, de millor esforç — les xarxes fallen, les instal·lacions donen error. Així que cada mode de fallada va rebre una alternativa humana: el botó manual "Check for Updates", reintents amb backoff exponencial, un temporitzador de recomprovació, i — reservat específicament per al cas en què l''intent manual d''un usuari havia fallat — un missatge en llenguatge planer "esborra l''aplicació i reinstal·la-la des del lloc web". El camí automàtic és una comoditat; el camí manual és la garantia.

La conclusió

  1. Una comprovació al voltant d''una acció irreversible és més perillosa que l''acció mateixa. El tancament condicional intentava evitar que una actualització dolenta tanqués l''aplicació, i en canvi va crear un estat pitjor que tancar-la o no tancar-la. Els camins d''apagada i d''instal·lació haurien de ser incondicionals i idempotents — mai supeditats a una bandera mutable que un altre gestor pot canviar-te sota els peus.
  2. A macOS, "sense finestres" no és "sense aplicació." Qualsevol lògica de desmuntatge ha de tenir en compte la plataforma on un procés sense finestres segueix en execució. Prova el camí de la fallada, al sistema operatiu real, no només el camí feliç.
  3. La funció que publiques a través del sistema d''actualització no es pot provar a través del sistema d''actualització. Aquesta asimetria és el motiu pel qual l''actualització automàtica mereix codi paranoic, incondicional i verificat manualment amb intensitat. Només pots arreglar-la de la manera fàcil després que ja funcioni.

Aquest és el capítol més primerenc del treball de fiabilitat que finalment es va convertir en GeekBye v2. Per veure on va portar aquell camí, mira què cal de debò per a una versió 2 (v2.0.0) i tot el recorregut a l''anatomia de publicar programari fins a la perfecció.