
Du paspaudimus praleidžiančios perdangos gedimo režimai
GeekBye langas plaukioja virš visko ir praleidžia tavo paspaudimus kiaurai — išskyrus ten, kur turi mygtukus. Tai dvipusė sutartis, o v1.8.5 ir v1.8.14 rodo, kaip atrodo, kai lūžta kiekviena pusė: vienas leidimas, kur perdanga prarijo sisteminį dialogą, ir kitas, kur ji pavogė tavo klavišų paspaudimus. Nugalinčiu vaistu antrajam tapo kodo ištrynimas.
Pagrindinis GeekBye langas nėra normalus langas. Jis be rėmelio, permatomas ir visada viršuje — stiklo lakštas, plaukiojantis virš to, ką darai, su keliais valdikliais, užpieštais ant jo. Įdomi inžinerija ne tame, kad jis plaukioja; ji tame, kad tavo pelė turi pereiti tiesiai kiaurai stiklą visur, išskyrus valdiklius. Spustelk mygtuką ant perdangos — ir paspaudimą gauna perdanga. Spustelk tuščią vietą šalia jo — ir paspaudimas nusileidžia ant tavo susitikimų programos apačioje, tarsi perdangos ten išvis nebūtų.
Tai sutartis su dviem pusėmis: priimk įvestį būtent ten, kur yra UI, ir būk visiškai nematoma įvesčiai visur kitur. Abi pusės turi galioti kiekvieną akimirką, o kiekvienas bug šioje programos dalyje yra vienos ar kitos pusės pažeidimas. Du leidimai, dviejų mėnesių tarpu, yra švariausia įmanoma iliustracija — nes kiekvienas sulaužė kitą pusę.
Sutartis, mechaniškai
Realizacija — tai hook, useMousePassthrough, ir jis mažas. Langas pagal numatymą praleidžia paspaudimus. Globalus mousemove klausytojas kiekvieno judesio metu užduoda vieną klausimą: ar kursorius virš valdiklio? Valdikliai pažymėti data-ui-region atributu, tad patikra tiesiogine prasme yra target.closest('[data-ui-region]'). Virš valdiklio jis kviečia setIgnoreMouseEvents(false) ir langas perima paspaudimus. Virš permatomos erdvės jis kviečia setIgnoreMouseEvents(true, { forward: true }) ir paspaudimai prakrenta.
Tas forward: true yra laikančioji detalė. Kai langas ima ignoruoti pelės įvykius, jis paprastai nustoja juos gauti apskritai — įskaitant mousemove įvykius, kurių hook reikia, kad pastebėtų, jog kursorius grįžta atgal virš valdiklio. forward: true liepia Electron vis tiek toliau persiųsti judesius ignoruojančiam langui, kad šis galėtų vėl pasiruošti. Be jo perdanga vieną kartą pereitų į praleidimo režimą ir niekada nebegrįžtų. Be to, hook kerta IPC ribą tik būsenos perėjimuose — kai kursorius kerta ribą tarp UI ir stiklo — o ne kiekviename judesio pikselyje. Įsimink tą optimizaciją; joje yra skylė, ir v1.8.5 į ją įkrito.
v1.8.5: kai lūžta permatomumo pusė
Permatomumo sutarties pusė yra „būk nematoma įvesčiai visur, kur nėra UI". v1.8.5 rodo, kaip atrodo pernelyg esanti perdanga — kai ji perima ar blokuoja įvestį, kurią turėjo praleisti. Postūmis buvo macOS leidimų dialogai, ir ten buvo dvi atskiros nesėkmės, sukrautos viena ant kitos.
Perdanga buvo fiziškai virš dialogo. Visada viršuje esantis langas turi lygį, o GeekBye perdanga sėdėjo aukščiausiame, kokį macOS siūlo — screen-saver. Tai virš beveik visko, įskaitant pačios sistemos Microphone ir Screen Recording leidimų dialogus. Tad onboardingo metu, būtent tuo momentu, kai macOS prašė leidimo, dialogas galėjo atsivaizduoti po perdanga ir būti nematomas. Pataisa, setPermissionMode, yra tik macOS ir daro neintuityvų dalyką: ji nuleidžia perdangą į floating lygį leidimo užklausos laikui, o paskui ją atkuria. O atkūrimui reikėjo konkretaus triuko, dokumentuoto tiesiai komite: macOS „doesn't always visually re-raise a window when setting the same level it was previously at", tad kodas praveda langą per tarpinį lygį — floating → status → screen-saver — kad priverstų langų serverį perskaičiuoti klojimo tvarką. Nenustatai lygio, kurio nori; prieini prie jo žingsnis po žingsnio.
Perdanga ėdė dialogo paspaudimus. Onboardingas piešia tamsų foną per visą langą, ir praleidimo hook traktuoja jį kaip ypatingą atvejį: kol tas fonas pakeltas, jis priverstinai įjungia paspaudimų perėmimą visame lange, kad vedlys būtų visiškai interaktyvus. Bet „perimk kiekvieną paspaudimą" yra būtent neteisinga, kai sisteminis dialogas bando vieną surinkti. Pataisa nuima fono žymeklio atributą leidimo užklausos laikui, tad hook iškrenta iš perėmimo režimo ir dialogo mygtukai gauna savo paspaudimus. Perdanga išlaiko savo pačios valdiklius interaktyvius; ji tiesiog nustoja apsimetinėti, kad tuščias stiklas yra mygtukas.
Skylė optimizacijoje. Prisimink, kad hook veikia tik perėjimuose. Kai langas paliko perėmimo režimą, jis iš naujo paleido užvedimo patikrą — bet jei tavo kursorius jau sėdėjo ant valdiklio tuo momentu, atsakymas į klausimą „ar mes virš UI?" jau buvo „taip", tad joks perėjimas neįvyko ir paspaudimų praleidimas taip ir nebuvo išjungtas. Valdiklis atrodė gyvas, o buvo miręs. Pataisa (1b6eb20, cituojama) nustoja pasitikėti perėjimu: „when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call". Kai keiti režimą, nelauki, kol kitas judesys pasakys tau, kur kursorius — pažiūri.
Ir padaryk užleidimą atšaukiamą. Subtilus rizikos taškas visame tame yra tas, kad perdanga dabar laikinai atiduoda savo z-order ir savo paspaudimų perėmimą, o kažkas turi juos atiduoti atgal. Jei atkūrimo iškvietimas mes išimtį — IPC žagsulys, langas nugriautas užklausos viduryje — programa galėtų įstrigti su isRequestingPermission, įstrigusiu ties true, ir tamsiu fonu, slepiančiu visą tavo ekraną, visam laikui. Tad išėjimo kelias buvo apvyniotas taip, kad būsenos atstata „visada įvyktų", plius kelių minučių apsauginis timeout ir išvalymas naikinant langą. Taisyklė, kuri iš to išplaukia: kaskart, kai perdanga užleidžia įvestį, to užleidimo atšaukimas turi būti finally, o ne laimingame kelyje.
v1.8.14: kai lūžta perėmimo pusė
Kita sutarties pusė yra „priimk įvestį ten, kur yra UI". v1.8.14 rodo, kaip atrodo pernelyg griebli perdanga — kai sąveika su ja kainuoja tau programos, kuria iš tikrųjų rūpiniesi, įvesties fokusą.
Simptomas changelog yra fokuso vagystė, ir štai sąžininga versija, nes git istorija pamokomesnė nei pastaba apie leidimą. Perdangos spartiųjų klavišų tvarkytuvės — matomumo perjungimas per Cmd+B, pokalbio lango atidarymas, jo slinkimas — kiekviena anksčiau kvietė mainWindow.focus(), o macOS — app.focus({ steal: true }), kad perdanga būtų priekyje, kai ją paleidi. Problema ta, kad perdangos fokusavimas deaktyvuoja tai, kas buvo priekyje anksčiau — tavo susitikimą, tavo redaktorių — tad klavišo paspaudimas, skirtas jiems, galėjo nusileisti į niekur arba ne ten. Perdanga griebė dalyką, kurio iš tikrųjų nereikėjo.
Ta paskutinė dalis ir yra visa pamoka, o komanda priėjo ją sunkiu keliu. Šaka, tapusi v1.8.14, pirma bandė išspręsti konkrečiai tempimą — nes tempimas turi savo gražią šio bug versiją: kai tempi viso ekrano perdangą, langas juda po nejudančiu kursoriumi, tad užvedimo modelis mano, kad rodyklė „paliko" valdiklį, ir persijungia į praleidimo režimą tempimo viduryje, numesdamas tempimą. Tad jie pridėjo dragLock. Paskui bandė padaryti visą langą nefokusuojamą — kas sulaužė rašymą ir tempimą, tad jie atkūrė fokusuojamumą, paskui susiaurino nefokusuojamumą tik iki teksto laukų, paskui tą perjungiklį išplėšė lauk. Jie sukūrė kastominį tempimo IPC, kad pakeistų natyvų Electron tempimą. Jie pridėjo suliejimą-prie-netikėto-fokuso. Savaitė tokio.
Paskui jie ištrynė beveik viską iš to. Komitas, kuris išsiuntė, atsuko kastominį tempimą, fokusuojamumo perjungiklį ir suliejimo tvarkytuvę ir išlaikė tiksliai vieną idėją: nustoti kviesti focus(). Suvokimas, įrašytas į kodo komentarą, yra tas, kad perdangai nuo pat pradžių niekada nereikėjo OS fokuso — „IPC messages (webContents.send) work without window focus". Perdanga valdo savo pačios rendererį tiesiogiai; fokusas buvo gryna šalutinė žala. Siųsti tinkamas vaistas nebuvo gudrus kastominio tempimo perrašymas, sukurtas per dieną. Tai buvo fokuso iškvietimų, kurių ten neturėjo būti, atėmimas. (Verta pasakyti tiesiai: changelog sako „while dragging it", bet tai, kas iš tikrųjų išsiuntė, buvo fokuso vagystės pašalinimas iš klaviatūros spartiųjų klavišų tvarkytuvių — tempimui specifinė mašinerija buvo ištirta ir atsukta tą pačią savaitę.)
Kas pastatyta ant kortos, tiesiogine prasme
Jei nori įrodymo, kad ši sutartis nėra akademinė, pažiūrėk, kas nutiko po mėnesių, kai perdanga įstrigo neteisingoje jos pusėje. Kur kas vėlesniame leidime rendererio procesas nulūžo, kol perdanga buvo perėmimo režime — setIgnoreMouseEvents(false), langas griebia visus paspaudimus — ir, neturint gyvo UI, kuris jį paleistų, nematomas viso darbalaukio langas sėdėjo ten ėsdamas kiekvieną paspaudimą naudotojo mašinoje, kol šis priverstinai neužbaigė. Permatomas langas, įstrigęs perėmimo režime, nėra miręs mygtukas; tai viso darbalaukio paspaudimų spąstai. (Tas gavo savo diagnozę — žr. kai tavo DI konspektuotojas nustoja įrašinėti susitikimo viduryje, kuri jį sugretina su visai kitu to paties leidimo bug.) Tai aštriausias visos problemos suformulavimas: užstrigink perjungiklį viena kryptimi — ir tavo UI miręs; užstrigęs kita — ir miręs visas tavo darbalaukis.
Trys dalykai, kurių mus išmokė šie leidimai
- Paspaudimus praleidžianti perdanga yra dvipusė sutartis, ir abi pusės gęsta skirtingai. Pernelyg permatoma — ir tavo pačios valdikliai numiršta; pernelyg griebli — ir tu praryji kitų programų įvestį. Dauguma perdangos bug yra tiesiog vienas iš tų dviejų, ir įvardyti, kurioje pusėje esi, pasako tau, kur ieškoti.
- Aukščiausias always-on-top lygis nėra prizas. Sėdėti virš visko reiškia sėdėti virš sisteminių dialogų, kuriuos reikia, kad naudotojas pamatytų. Projektuok tam momentui, kai tavo perdanga turi mandagiai pasitraukti — ir padaryk grįžimą aukštyn atšaukiamą
finally, nes iki pusės atliktas užleidimas yra blogiau nei neužleisti išvis. - Atėmimas įveikia gudrumą. Nugalinti v1.8.14 pataisa buvo
focus()iškvietimų, kurių perdangai niekada nereikėjo, ištrynimas, rastas tik po savaitės kastominio tempimo tvarkymo statymo ir atsukimo. Kai funkcija vis kovoja su platforma, pirmas klausimas — ar gali pašalinti tą, kas tą kovą veda.
Ankstesniam v1 istorijos skyriui — leidimų perkėlimas į CI, dukart (v1.8.4); o visai istorijos linijai — programinės įrangos tobulybės pristatymo anatomija.