Steven
Steven6 мин четене

Проверката за безопасност, която направи приложението ни невъзможно за спиране

Автоматичното обновяване беше най-трудната функция, която някога сме пускали — шест издания за четири дни, за да спрем то да убива приложението. Най-лошата грешка беше тази, която сами въведохме, опитвайки се да сме внимателни: 500-милисекундна "предпазна" проверка, която превърна неуспешно обновяване в процес, който буквално не можеше да бъде спрян.

Инженерство
Electron
Надеждност
Издания на GeekBye
Проверката за безопасност, която направи приложението ни невъзможно за спиране

Всеки разработчик на настолни приложения подценява автоматичното обновяване точно веднъж. Изглежда като решен проблем — библиотека изтегля нова версия и рестартира приложението ти. После го пускаш и научаваш, че "рестартирай приложението си" е едно от най-опасните неща, за които една програма може да бъде помолена, защото се случва в точния момент, в който приложението ти се разрушава и има най-малко резерв за грешки.

Автоматичното обновяване на GeekBye отне шест издания за четири дни — от v1.5.14 до v1.5.19 — за да се стабилизира. Това е историята на най-лошата грешка в този период, която сами причинихме, опитвайки се да сме внимателни.

Шест издания и това, което имаше значение

Всичко започна обикновено. v1.5.14 поправи неудобна грешка от класа на печатните: емисията за обновяване сочеше към име на GitHub хранилище, което не съществуваше, така че обновителят проверяваше 404. v1.5.15 добави ръчен бутон "Check for Updates" и истинско съобщение за грешка. После започнаха грешките с quitAndInstall и изданията заваляха бързо — защото когато механизмът ти за обновяване е счупен, не можеш да пуснеш поправката за него чрез механизма за обновяване. Всяка итерация е залог за ръчна преинсталация.

Това, което има значение, е v1.5.18. Цялото ѝ съдържание беше един-единствен комит със заглавие, което все още ме кара да трепна: restore original quitAndInstall behavior to prevent unkillable app.

Как "внимателността" направи приложението неизползваемо

Ето постановката. Когато обновяване бъде изтеглено, quitAndInstall на Electron би трябвало да затвори приложението и да го смени с новата версия. В по-ранно издание някой — резонно — се притесни, че спирането безусловно е рисковано. Ами ако инсталацията даде грешка? Нямаше ли да е по-безопасно да се спре само ако всичко изглежда наред?

Затова кодът се сдоби със защита, която изглеждаше разумна:

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)

Логиката: задей инсталацията, изчакай половин секунда и наложи окончателното app.quit() само ако флагът updateDownloaded все още е истина — иначе остави приложението отворено, за да не остане потребителят блокиран.

Капанът е на един ред разстояние, в обработчика на грешки. Този обработчик задаваше this.updateDownloaded = false. Затова си представи неуспешна инсталация: събитието error се задейства и изчиства флага. Но quitAndInstall вече беше започнал разрушаването — беше затворил прозорците и премахнал слушателите за спиране на приложението. После 500ms таймерът се събужда, проверява вече-неистинния флаг, решава "открита е грешка, остави приложението отворено" и пропуска app.quit().

Сега имаш, конкретно на macOS, най-лошото възможно състояние. macOS не спира приложение само защото последният му прозорец се е затворил — това е поведението window-all-closed, на което разчита всяко Mac приложение. Така че процесът е все още жив, но има никакъв прозорец, никакъв път през лентата с менюта и премахнати слушатели за спиране. Няма върху какво да се щракне. Cmd-Q няма с какво да говори. Единственият изход е Force Quit от Activity Monitor. "Предпазната" проверка беше превърнала неуспешно обновяване — досадна, но поправима ситуация — в зомби, което не можеш да убиеш.

Поправката: разрушаването трябва да е безусловно

Корекцията във v1.5.18 е почти агресивно скучна, което е и целта. Тя премахва мъдруването:

  1. Премахни слушателите window-all-closed и before-quit, които биха могли да пречат.
  2. Унищожи всеки прозорец — window.destroy(), не window.close(). Close може да бъде отхвърлено от обработчик; destroy не може. Когато си се ангажирал да спираш, не питаш учтиво.
  3. Извикай quitAndInstall.
  4. Извикай app.quit() безусловно.

Никакъв флаг, никакъв таймер, никакво "остави го отворено за всеки случай." Защото истината за пътя на спиране е, че наполовина завършено спиране е по-лошо от която и да е от двете крайности. Пълното спиране е наред. Пълното оставане отворено е наред. Единственото състояние, до което никога не бива да достигаш, е разрушено, но все още работещо — и точно това е състоянието, в което условното спиране може да те остави блокиран.

Още два урока, на които същата седмица научи

Невъзможната за спиране грешка е водеща, но паническото препускане през шест издания затвърди два други навика, които си струва да откраднеш.

Филтрирай телеметрията си за сривове по точни сигнатури, а не по широки ключови думи. По средата на препускането открихме, че отчитането ни на грешки беше конфигурирано да отхвърля всичко, съдържащо думи като permission, token или microphone — опит да се реже шум, който тихо поглъщаше реални сривове, случайно споменаващи тези думи. Изтръгнахме одеалните филтри и ги заменихме с точни низове за отказ (конкретното съобщение, което macOS издава при отказано разрешение) и специфични временни мрежови кодове като ERR_NETWORK_CHANGED. Намаляването на шума и скриването на грешки са едно и също копче, завъртяно в противоположни посоки; ако филтрираш по усещане, ще филтрираш точно нещото, което е трябвало да видиш.

Всеки автоматичен път се нуждае от ръчен авариен изход. Автоматичното обновяване по природа е "най-добро усилие" — мрежите се дъним, инсталациите се провалят. Затова всеки режим на провал получи човешки резервен вариант: ръчният бутон "Check for Updates", повторни опити с експоненциално отдръпване, таймер за повторна проверка и — запазено конкретно за случая, когато ръчният опит на потребителя се провали — съобщение на прост език "изтрий приложението и преинсталирай от уебсайта". Автоматичният път е удобство; ръчният път е гаранцията.

Изводът

  1. Защита около необратимо действие е по-опасна от самото действие. Условното спиране се опитваше да предотврати неуспешно обновяване да спре приложението и вместо това създаде състояние, по-лошо и от спирането, и от неспирането. Пътищата за спиране и инсталация трябва да бъдат безусловни и идемпотентни — никога зависими от променлив флаг, който друг обработчик може да обърне изпод теб.
  2. На macOS "без прозорци" не значи "без приложение." Всяка логика за разрушаване трябва да отчита платформата, където процес без прозорци продължава да работи. Тествай пътя на провала, на реалната ОС, а не само щастливия път.
  3. Функцията, която пускаш чрез системата за обновяване, не може да бъде тествана чрез системата за обновяване. Тази асиметрия е причината автоматичното обновяване да заслужава параноичен, безусловен, силно ръчно проверен код. Получаваш възможността да го поправиш по лесния начин чак след като то вече работи.

Това е най-ранната глава от работата по надеждността, която в крайна сметка стана GeekBye v2. За това докъде доведе този път, виж какво всъщност изисква една втора версия (v2.0.0) и цялата дъга в анатомията на пускането на софтуер до съвършенство.

Свързани статии

Тишината беше носеща
Steven
Steven7 мин четене

Тишината беше носеща

Последните две издания на GeekBye v1 са за една и съща неудобна истина: транскрипцията в реално време през реална мрежа не е без загуби, а честният ход е да спреш да се преструваш, че е. v1.8.20 запази на диска копие на всеки аудио фрагмент, преди да го изхвърли по време на повторно свързване, и започна да маркира на глас пропуските в транскрипцията. v1.9.0 спря да изпраща тишина, за да пести честотна лента — и откри, че тишината беше точно сигналът, който транскрайбърът използваше, за да знае, че изречението е свършило. Две издания за цената на изхвърлянето на неща.

Инженерство
Audio
Надеждност
Отпечатване на среща в PDF без PDF библиотека
Steven
Steven8 мин четене

Отпечатване на среща в PDF без PDF библиотека

GeekBye експортира среща като PDF, а никъде в кода няма PDF библиотека. Той рендира HTML в невидим browser прозорец и го отпечатва. Този избор е цялата история: направи функцията лесна за изграждане и ѝ даде всеки провал, който има истински browser — бял проблясък, ограничение за дължината на URL и прекъсване на страница, което разрязваше скрийншотите наполовина. Fix-ът за най-грозния бъг беше един ред CSS.

Инженерство
Electron
Desktop
Двата режима на отказ на click-through overlay
Steven
Steven8 мин четене

Двата режима на отказ на click-through overlay

Прозорецът на GeekBye плава над всичко и оставя кликовете ти да минават през него — освен там, където има бутони. Това е двустранен договор, а v1.8.5 и v1.8.14 показват как изглежда, когато всяка от страните се счупи: едно издание, в което overlay-ът глътна системен диалог, и едно, в което открадна натисканията на клавишите ти. Печелившият fix за второто беше изтриване на код.

Инженерство
Electron
Desktop