Steven
Steven6 min lukuaika

Syntyi ja korjattiin kahdessatoista minuutissa

GeekBye v1.8.10:n muutosloki sanoo että se korjasi kaatumisen näppäinoikoteitä muokatessa. Niin se teki — mutta kaatuminen esiteltiin ja korjattiin samassa pull requestissa, kahdentoista minuutin välein, eikä se koskaan tavoittanut ainuttakaan käyttäjää. Todellinen tarina on luotettavuuskaskadi joka sen tuotti: pieni, oikea muutos siihen milloin oikotiet keskeytetään, ja React-purkubugi joka putosi yhdestä rivistä jonka piti tehdä editorista turvallisempi.

Ohjelmistokehitys
React
Desktop
GeekBye-julkaisut
Syntyi ja korjattiin kahdessatoista minuutissa

Tässä on rehellisin lause jonka voin kirjoittaa GeekBye v1.8.10:stä: kaatuminen jonka korjaamisesta se on kuuluisa ei koskaan tapahtunut kenellekään. Muutosloki sanoo "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," mikä on totta ja lukee kuin bugi joka toimitettiin, puri käyttäjiä ja paikattiin jatkojulkaisussa. Ei se ollut. Kaatumisen esitteli yksi commit ja poisti toinen samassa pull requestissa, kahdentoista minuutin välein, samana perjantaiaamuna. Se eli kokonaan yhden haaran sisällä. Julkaisumuistiinpanot kuvaavat arven joka muodostui ja parani ennen kuin kukaan repon ulkopuolella saattoi nähdä sitä.

Se ei ole muutoslokin epäonnistuminen; se on iteratiivisen työn rehellinen muoto, ja se kannattaa kertoa koska nuo kaksitoista minuuttia sisältävät aidosti opettavaisen React-bugin. Mutta ymmärtääksesi kaatumisen sinun on ymmärrettävä mitä varten julkaisu oikeastaan oli — koska kaatuminen oli oheisvahinkoa jonkin muun korjaamisesta.

Mitä varten julkaisu oikeastaan oli

GeekBye antaa sinun uudelleensitoa näppäinoikotiensa asetuksissa. Sillä aikaa kun tallennat uutta yhdistelmää, sovelluksen on estettävä globaalien oikoteidensa laukeaminen — muuten Cmd+B:n painaminen sen määräämiseksi vain vaihtaisi ikkunaa sen sijaan että se kaapattaisiin. Joten editori keskeyttää globaalit oikotiet IPC:n yli: renderöijä kutsuu window.electronAPI.setShortcutsSuspended(true), ja pääprosessin ShortcutsHelper vastaa kutsumalla unregisterAll(), pudottaen jokaisen globalShortcut-rekisteröinnin niin että näppäinpainallus tavoittaa tallennussyötteen sen sijaan että laukaisisi toiminnon. Jatkaessa se kutsuu registerGlobalShortcuts() laittaakseen ne takaisin.

Bugi joka aloitti tämän kaiken (ongelma #233) koski tuon keskeytyksen laajuutta. Vanha koodi keskeytti oikotiet koko sen ajaksi kun oikotieasetussivu oli auki — ei vain silloin kun aktiivisesti tallensit näppäintä. Suurimman osan ajasta se on näkymätöntä. Mutta jos avasit sivun, aloit tehdä jotain muuta ja navigoit pois muokkausta koskaan lopettamatta, sovellus saattoi jäädä kaikki globaalit oikotiensa rekisteröimättöminä — hiljaa kuolleina — kunnes palasit takaisin. Commit joka sen korjaa on avoin oireesta: liian laaja keskeytys uhkasi "making the app appear broken if the user forgot they had the page open."

Korjaus on yhden rivin kavennus: keskeytä vain kun riviä oikeasti muokataan — avaimena !!editingShortcut, hook-tila joka pitää tallennettavan rivin id:tä — sen sijaan että koko käynnin ajaksi. Hyvä muutos. Mutta "keskeytä vain muokkauksen aikana" nostaa välittömän kysymyksen: mikä lasketaan muokkauksen lopettamiseksi? Näppäimen painaminen lopettaa sen. Escape:n painaminen peruuttaa sen. Mutta entä jos vain… klikkaat pois tai vaihdat kokonaan toiseen sovellukseen kesken tallennuksen? Jos mikään ei peruuta muokkausta, jäät tallentamaan oikotietä ikkunaan joka ei ole fokuksessa, ja oikotiet pysyvät keskeytettyinä. Joten sama PR lisäsi turvaventtiilin: peruuta muokkaus kun fokus lähtee. Ja se on rivi joka kaatui.

Kahdentoista minuutin bugi

Turvaventtiili oli useEffect tiedostossa ShortcutsSettings.tsx joka, sillä aikaa kun riviä muokattiin, kuunteli blur:ia:

const handleBlur = () => cancelEditing()
window.addEventListener('blur', handleBlur)
inputRef.current?.addEventListener('blur', handleBlur) // the landmine

Kaksi kuuntelijaa. Yksi window:ssa — laukeaa kun vaihdat toiseen sovellukseen. Yksi kiinnitetty suoraan, raakana DOM-kuuntelijana, tallennus-<input>:iin — tarkoitettu laukeamaan kun klikkaat pois toiseen elementtiin. Ne näyttävät tarpeettomilta ja vaarattomilta. Eivät ole.

Kävele läpi sekvenssi "switching windows mid-edit," mikä on täsmälleen muutoslokin ilmaus:

  1. Tallennat oikotietä; <input> on fokuksessa. Painat Cmd+Tab toiseen sovellukseen.
  2. Ikkunan blur laukeaa. handleBlur ajaa cancelEditing():n, joka asettaa editingShortcut:n takaisin null:iksi.
  3. Tuon tilan asettaminen irrottaa <input>:n — rivi poistuu muokkaustilasta, joten syöte poistetaan DOM:sta.
  4. Fokuksessa olevan elementin poistaminen DOM:sta lähettää synkronisesti blur-tapahtuman siihen elementtiin. Raaka inputRef-kuuntelija nappaa sen ja kutsuu cancelEditing():n uudelleen — tällä kertaa keskellä React:n commit-vaihetta, sillä aikaa kun komponenttipuuta puretaan.
  5. Tilan asettajan kutsuminen fiber:issä joka on kesken purkua kompastuu yhteen React:n sisäisistä invarianteista, ja se heittää: "Should have a queue". Asetusnäkymä kaatuu.

Bugi ei ole ikkunakuuntelija eikä se oikeastaan ole "peruuta blur:ssa" -konsepti. Se on se että toinen kahdesta kuuntelijasta oli raaka addEventListener React:n hallitsemassa elementissä. React ei tiedä siitä kuuntelijasta, ei siivoa sitä irrotuksessa, ja — ratkaisevasti — selain laukaisee blur:n juuri sen DOM-poiston aikana jota React suorittaa, joten käsittelijä palaa tilalogiikkaasi pahimmalla mahdollisella hetkellä. Korjaus-commitin viesti sanoo sen tarkemmin kuin minä osaan: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle."

Korjaus

bf28a50, kaksitoista minuuttia kaatumisen syntymän jälkeen, tekee kaksi pientä asiaa.

Ensiksi, se poistaa raa'an syötekuuntelijan ja pitää vain ikkunan, joka on ainoa joka ylipäätään tarvitsi olla manuaalinen DOM-kuuntelija (ei ole React:n onBlur:ia sille että "the whole window lost focus"):

const handleWindowBlur = () => cancelEditing()
window.addEventListener('blur', handleWindowBlur)
return () => {
  window.removeEventListener('blur', handleWindowBlur)
}

Toiseksi, se siirtää klikkaa-pois-tapauksen — "user clicked another element" — React:n omaan onBlur-propiin syötteessä, ja lykkää sitä tikin verran:

onBlur={() => {
  // Deferred to avoid state updates during React's render cycle.
  setTimeout(() => cancelEditing(), 0)
}}

Molemmat osat ovat tärkeitä. React:n onBlur-propin käyttäminen raa'an kuuntelijan sijaan tarkoittaa että React omistaa käsittelijän elinkaaren eikä jätä sitä roikkumaan. Ja setTimeout(…, 0) takaa että vaikka tämän blur:n laukaiseekin irrotus, cancelEditing()-kutsu laskeutuu seuraavalle tikille — nykyisen renderöinnin ja commitin päätyttyä — joten se ei voi koskaan päivittää tilaa komponentissa joka on yhä purkautumassa. Jos komponentti on siihen mennessä jo poissa, lykätty kutsu on vaaraton no-op.

Siinä on koko korjaus. Poista kuuntelija jota React ei nähnyt; anna React:n ajastaa se joka jäi.

Miksi tämä on sitä hyvää tylsyyttä

On houkutus arkistoida tämä "triviaaliksi" — setTimeout, poistettu rivi — ja jatkaa eteenpäin. Mutta sen muoto on yksi yleisimmistä tavoista joilla React-työpöytäsovellukset kaatuvat, ja se kannattaa sisäistää:

  • Kaatuminen ilmestyy vain elinkaaren rajalla (irrotus), laukaistuna tapahtumasta jota kehys ei ajasta (natiivi blur DOM-poiston aikana). Se ei näy klikkaustestissä; se tarvitsee tarkan "fokus lähtee muokkauksen aikana" -polun.
  • Juurisyy on kahden omistusmallin sekoittaminen — raa'at DOM-tapahtumat ja React:n synteettiset, ajastetut tapahtumat — samassa elementissä. Sillä hetkellä kun käsittelijäsi muuttaa tilaa joka voi irrottaa sen elementin, raaka polku muuttuu uudelleensyötyväksi.
  • Korjaus ei ole "lisää vartijalippu" (vaikka isMounted-ref olisi peittänyt sen); se on raa'an kuuntelijan poistaminen kokonaan, mikä on pienempi muutos ja oikea. Vähennystä taas.

Ja meta-oppitunti, se jonka ympärillä koko tämä sarja pyörii: lue diff, älä julkaisumuistiinpanoa. "Fixed a crash while editing keyboard shortcuts" on tarkka, mutta se piilottaa että kaatuminen oli itseaiheutettu, saman aamun ja ei koskaan toimitettu — ja että todellinen julkaisu on hiljainen oikeellisuusvoitto siitä milloin globaali oikotie keskeytetään.

Mitä muuta toimitettiin

Kaksi asiaa lisää ratsasti mukana v1.8.10:ssä, molemmat rivin arvoisia. Oikotiejärjestelmä sai myös itsekorjautumisen unen/heräämisen yli: macOS voi hiljaa pudottaa sovelluksen globaalien oikoteiden rekisteröinnit kun kone nukkuu, joten jatkokäsittelijä nyt tarkistaa ja rekisteröi ne uudelleen, mikä tarkoittaa että oikotiesi toimivat yhä läppärin herättyä ilman uudelleenkäynnistystä. Ja AI-puolella asiakas tuli fiksummaksi käyttörajojen suhteen — suoratoistopyynnöt jotka osuvat 429:ään perääntyvät nyt eksponentiaalisesti ja nollaavat laskurinsa kun lopetat tallennuksen, sen sijaan että odottaisivat ulos kiinteän viiveen. Kolme luotettavuuskorjausta eri alijärjestelmiin, joista yksi on kaatuminen joka ei koskaan ollut kaatuminen.

Edellinen luku v1-tarinassa: kokouksen tulostaminen PDF:ksi ilman PDF-kirjastoa (v1.8.9); ja koko kaarelle, ohjelmiston toimittamisen anatomia täydellisyyteen asti.

Aiheeseen liittyvät artikkelit

Kokouksen tulostaminen PDF:ksi ilman PDF-kirjastoa
Steven
Steven7 min lukuaika

Kokouksen tulostaminen PDF:ksi ilman PDF-kirjastoa

GeekBye vie kokouksen PDF:nä, eikä koodissa ole missään PDF-kirjastoa. Se renderöi HTML:n näkymättömässä selainikkunassa ja tulostaa sen. Tuo valinta on koko tarina: se teki ominaisuudesta helpon rakentaa ja antoi sille jokaisen vian joka oikealla selaimella on — valkoisen välähdyksen, URL:n pituusrajan ja sivunvaihdon joka leikkasi kuvakaappaukset kahtia. Rumimman bugin korjaus oli yksi rivi CSS:ää.

Ohjelmistokehitys
Electron
Desktop
OCR:stä pikseleihin vararatkaisua menettämättä
Steven
Steven6 min lukuaika

OCR:stä pikseleihin vararatkaisua menettämättä

GeekBye lakkasi lukemasta kuvakaappauksiasi OCR:llä ja alkoi lähettää itse kuvan näkömalleille — mikä on se helppo osa. Opettavainen osa on optimointi joka ohitti OCR:n kokonaan näkökäyttäjiltä, rikkoi hiljaa vararatkaisun ei-näkömalleihin, ja jouduttiin perumaan versioon jossa turvaverkko pyörii rinnakkain eikä maksa mitään.

Ohjelmistokehitys
AI
Desktop
Läpiklikattavan peittoikkunan kaksi vikatilaa
Steven
Steven7 min lukuaika

Läpiklikattavan peittoikkunan kaksi vikatilaa

GeekByen ikkuna leijuu kaiken päällä ja päästää klikkauksesi lävitseen — paitsi siellä missä sillä on painikkeita. Se on kaksipuolinen sopimus, ja v1.8.5 ja v1.8.14 näyttävät miltä näyttää kun kumpikin puoli hajoaa: yksi julkaisu jossa peittoikkuna nielaisi järjestelmädialogin, yksi jossa se varasti näppäinpainalluksesi. Toisen voittava korjaus oli koodin poistaminen.

Ohjelmistokehitys
Electron
Desktop