
Verificarea de siguranță care a făcut aplicația noastră imposibil de închis
Auto-update a fost cea mai grea funcție pe care am livrat-o vreodată — șase lansări în patru zile ca s-o oprim din a strica aplicația. Cel mai urât bug a fost unul pe care l-am introdus chiar noi încercând să fim precauți: o verificare de „siguranță" de 500 de milisecunde care transforma un update eșuat într-un proces pe care pur și simplu nu-l puteai închide.
Fiecare dezvoltator de aplicații desktop subestimează auto-update-ul exact o singură dată. Pare o problemă rezolvată — o bibliotecă descarcă o versiune nouă și repornește aplicația. Apoi o livrezi și afli că „repornește aplicația" este unul dintre cele mai periculoase lucruri care i se pot cere unui program, pentru că se întâmplă exact în momentul în care aplicația se demontează pe sine și are cea mai mică marjă de eroare.
Auto-update-ul de la GeekBye a avut nevoie de șase lansări în patru zile — v1.5.14 până la v1.5.19 — ca să se stabilizeze. Aceasta este povestea celui mai urât bug din acel interval, pe care ni l-am provocat singuri încercând să fim precauți.
Șase lansări, și cea care a contat
Arcul a început banal. v1.5.14 a reparat un bug de tip greșeală de tipar, jenant: feed-ul de update indica spre un nume de repo GitHub care nu exista, așa că updater-ul verifica un 404. v1.5.15 a adăugat un buton manual „Check for Updates" și un mesaj de eroare real. Apoi au început bug-urile de quitAndInstall, iar lansările veneau rapid — pentru că, atunci când mecanismul tău de update este stricat, nu poți livra reparația pentru el prin mecanismul de update. Fiecare iterație este un pariu cu reinstalarea manuală.
Cea care contează este v1.5.18. Întregul ei conținut era un singur commit cu un titlu care încă mă face să tresar: restore original quitAndInstall behavior to prevent unkillable app.
Cum a stricat aplicația faptul că „am fost precauți"
Iată contextul. Când un update este descărcat, quitAndInstall de la Electron ar trebui să închidă aplicația și să pună în loc versiunea nouă. Într-o lansare anterioară, cineva — pe bună dreptate — s-a îngrijorat că închiderea necondiționată era riscantă. Dacă instalarea eșua? Nu ar fi mai sigur să închidem doar dacă totul pare sănătos?
Așa că a apărut în cod o barieră care părea rezonabilă:
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)
Logica: pornește instalarea, așteaptă o jumătate de secundă și forțează app.quit()-ul final doar dacă flag-ul updateDownloaded este încă true — altfel ține aplicația deschisă ca utilizatorul să nu rămână blocat.
Capcana este la o linie distanță, în handler-ul de eroare. Acel handler seta this.updateDownloaded = false. Așa că imaginează-ți o instalare eșuată: evenimentul error se declanșează și curăță flag-ul. Dar quitAndInstall deja începuse demontarea — închisese ferestrele și eliminase listener-ele de închidere ale aplicației. Apoi timer-ul de 500ms se trezește, verifică flag-ul acum-false, decide „error detected, keep the app open" și sare peste app.quit().
Acum ai, pe macOS în special, cea mai proastă stare posibilă. macOS nu închide o aplicație doar pentru că ultima ei fereastră s-a închis — acesta este comportamentul window-all-closed pe care se bazează fiecare aplicație Mac. Așa că procesul este încă viu, dar nu are nicio fereastră, nicio cale prin bara de meniu și listener-ele lui de închidere au fost smulse. Nu există nimic pe care să dai click. Cmd-Q nu are cu cine să vorbească. Singura ieșire este Force Quit din Activity Monitor. Verificarea de „siguranță" transformase un update eșuat — o pacoste recuperabilă — într-un zombi pe care nu-l puteai omorî.
Soluția: demontarea trebuie să fie necondiționată
Corecția din v1.5.18 este aproape agresiv de plictisitoare, ceea ce este exact ideea. Elimină deșteptăciunea:
- Elimină listener-ele
window-all-closedșibefore-quitcare ar putea interfera. - Distruge fiecare fereastră —
window.destroy(), nuwindow.close(). Close poate fi anulat de un handler; destroy nu poate. Când te-ai angajat să închizi, nu întrebi politicos. - Apelează
quitAndInstall. - Apelează
app.quit()necondiționat.
Fără flag, fără timer, fără „ține-o deschisă pentru orice eventualitate". Pentru că adevărul despre o cale de închidere este că o închidere pe jumătate terminată este mai rea decât oricare dintre rezultate. Închiderea completă e în regulă. Rămânerea complet deschisă e în regulă. Singura stare la care nu trebuie să ajungi niciodată este demontat, dar încă rulând — și exact în această stare te poate lăsa blocat o închidere condiționată.
Alte două lecții predate în aceeași săptămână
Bug-ul imposibil de închis este vedeta, dar zbaterea celor șase lansări a întărit alte două obiceiuri care merită furate.
Filtrează-ți telemetria de crash-uri pe semnături exacte, nu pe cuvinte-cheie largi. În mijlocul zbaterii am descoperit că raportarea noastră de erori era configurată să elimine orice conținea cuvinte precum permission, token sau microphone — o încercare de a tăia zgomotul care înghițea în tăcere crash-uri reale care se întâmplau să menționeze acele cuvinte. Am smuls filtrele-pătură și le-am înlocuit cu șiruri exacte de refuz (mesajul specific pe care macOS îl emite când o permisiune este refuzată) și coduri specifice de rețea tranzitorie precum ERR_NETWORK_CHANGED. Reducerea zgomotului și ascunderea bug-urilor sunt aceeași manetă răsucită în sensuri opuse; dacă filtrezi după feeling, vei filtra afară exact lucrul pe care aveai nevoie să-l vezi.
Fiecare cale automată are nevoie de o portiță manuală de scăpare. Auto-update-ul este prin natura sa best-effort — rețelele au sincope, instalările eșuează. Așa că fiecare mod de eșec a primit un fallback uman: butonul manual „Check for Updates", reîncercări cu exponential-backoff, un timer de re-verificare și — rezervat special pentru cazul în care încercarea manuală a unui utilizator eșuase — un mesaj în limbaj simplu, „șterge aplicația și reinstalează de pe site". Calea automată este o comoditate; calea manuală este garanția.
Concluzia
- O barieră în jurul unei acțiuni ireversibile este mai periculoasă decât acțiunea în sine. Închiderea condiționată încerca să împiedice un update prost să închidă aplicația și, în schimb, a creat o stare mai rea decât fie închiderea, fie neînchiderea. Căile de închidere și instalare ar trebui să fie necondiționate și idempotente — niciodată condiționate de un flag mutabil pe care un alt handler îl poate schimba de sub tine.
- Pe macOS, „fără ferestre" nu înseamnă „fără aplicație". Orice logică de demontare trebuie să țină cont de platforma unde un proces fără ferestre continuă să ruleze. Testează calea de eșec, pe sistemul de operare real, nu doar calea fericită.
- Funcția pe care o livrezi prin sistemul de update nu poate fi testată prin sistemul de update. Această asimetrie este motivul pentru care auto-update-ul merită cod paranoic, necondiționat și verificat manual intens. Ajungi să-l repari pe calea ușoară abia după ce deja funcționează.
Acesta este cel mai timpuriu capitol al muncii de fiabilitate care a devenit în cele din urmă GeekBye v2. Pentru încotro a dus acel drum, vezi ce presupune de fapt o versiune 2 (v2.0.0) și întregul arc în anatomia livrării software-ului la perfecțiune.