Steven
Steven7 minit bacaan

Lahir dan Dibetulkan dalam Dua Belas Minit

Changelog GeekBye v1.8.10 berkata ia membetulkan satu crash semasa menyunting pintasan papan kekunci. Memang begitu — tetapi crash itu diperkenalkan dan dibetulkan dalam pull request yang sama, berselang dua belas minit, dan tidak pernah sampai kepada seorang pun pengguna. Kisah sebenarnya ialah lata kebolehpercayaan yang menghasilkannya: satu perubahan kecil dan betul tentang bila pintasan digantung, dan bug teardown React yang jatuh daripada satu baris yang bertujuan menjadikan editor lebih selamat.

Kejuruteraan
React
Desktop
GeekBye Releases
Lahir dan Dibetulkan dalam Dua Belas Minit

Inilah ayat paling jujur yang boleh saya tulis tentang GeekBye v1.8.10: crash yang ia terkenal kerana membetulkannya sebenarnya tidak pernah berlaku kepada sesiapa pun. Changelog berkata "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," yang benar, dan terbaca seperti satu bug yang dikeluarkan, menggigit pengguna, dan ditampal dalam susulan. Ia bukan begitu. Crash itu diperkenalkan oleh satu commit dan dibuang oleh satu lagi dalam pull request yang sama, berselang dua belas minit, pada pagi Jumaat yang sama. Ia hidup sepenuhnya di dalam satu branch. Nota keluaran menggambarkan satu parut yang terbentuk dan sembuh sebelum sesiapa di luar repo dapat melihatnya.

Itu bukan satu kegagalan changelog; ia bentuk jujur bagi kerja berulang, dan ia berbaloi diceritakan kerana dua belas minit itu mengandungi satu bug React yang benar-benar mendidik. Tetapi untuk memahami crash itu anda perlu memahami untuk apa keluaran ini sebenarnya — kerana crash itu ialah kerosakan cagaran daripada membetulkan sesuatu yang lain.

Tentang apa keluaran ini sebenarnya

GeekBye membenarkan anda mengikat semula pintasan papan kekuncinya di Tetapan. Semasa anda merakam satu kombinasi baru, aplikasi mesti menghentikan pintasan global-nya daripada tercetus — jika tidak, menekan Cmd+B untuk menetapkannya hanya akan menogol tetingkap dan bukan ditangkap. Jadi editor menggantung pintasan global melalui IPC: renderer memanggil window.electronAPI.setShortcutsSuspended(true), dan ShortcutsHelper milik proses utama membalas dengan memanggil unregisterAll(), menggugurkan setiap pendaftaran globalShortcut supaya tekanan kekunci sampai ke input penangkap dan bukan mencetuskan satu tindakan. Semasa menyambung semula ia memanggil registerGlobalShortcuts() untuk mengembalikannya.

Bug yang memulakan semua ini (issue #233) ialah tentang skop penggantungan itu. Kod lama menggantung pintasan sepanjang masa halaman tetapan pintasan terbuka — bukan hanya semasa anda aktif merakam satu kekunci. Kebanyakan masa itu tidak kelihatan. Tetapi jika anda membuka halaman itu, mula melakukan sesuatu yang lain, dan menavigasi menjauh tanpa pernah menyelesaikan satu suntingan, aplikasi boleh tertinggal dengan semua pintasan globalnya tidak berdaftar — mati secara senyap — sehingga anda kembali. Commit yang membetulkannya berterus terang tentang gejalanya: menggantung terlalu luas berisiko "making the app appear broken if the user forgot they had the page open."

Pembetulannya ialah satu penyempitan satu baris: gantung hanya apabila satu baris benar-benar sedang disunting — dikunci pada !!editingShortcut, state hook yang memegang id baris yang anda rakam — dan bukan sepanjang lawatan. Perubahan yang baik. Tetapi "hanya gantung semasa menyunting" menimbulkan satu soalan segera: apa yang dikira sebagai menyelesaikan satu suntingan? Menekan satu kekunci menyelesaikannya. Menekan Escape membatalkannya. Tetapi bagaimana jika anda cuma… klik menjauh, atau bertukar ke aplikasi lain sepenuhnya, di tengah penangkapan? Jika tiada apa yang membatalkan suntingan, anda tertinggal merakam satu pintasan ke dalam satu tetingkap yang tidak berfokus, dan pintasan kekal digantung. Jadi PR yang sama menambah satu injap keselamatan: batalkan suntingan apabila fokus beredar. Dan itulah baris yang crash.

Bug dua belas minit

Injap keselamatan itu ialah satu useEffect di ShortcutsSettings.tsx yang, semasa satu baris sedang disunting, mendengar blur:

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

Dua listener. Satu pada window — tercetus apabila anda bertukar ke aplikasi lain. Satu dilampirkan terus, sebagai satu listener DOM mentah, ke <input> penangkap — bertujuan tercetus apabila anda klik menjauh ke satu elemen lain. Ia kelihatan berlebihan dan tidak berbahaya. Ia bukan.

Susuri urutan bagi "switching windows mid-edit," yang tepat-tepat frasa changelog:

  1. Anda sedang merakam satu pintasan; <input> berfokus. Anda Cmd+Tab ke satu aplikasi lain.
  2. Blur tetingkap tercetus. handleBlur menjalankan cancelEditing(), yang menetapkan semula editingShortcut kepada null.
  3. Menetapkan state itu meng-unmount <input> — baris itu keluar daripada mod sunting, jadi input dibuang daripada DOM.
  4. Membuang satu elemen yang berfokus daripada DOM secara segerak menghantar satu event blur pada elemen itu. Listener inputRef mentah menangkapnya dan memanggil cancelEditing() sekali lagi — kali ini di tengah fasa commit React, semasa pepohon komponen sedang dirobohkan.
  5. Memanggil satu state setter pada satu fiber yang berada di tengah teardown tersandung salah satu invarian dalaman React, dan ia melontar: "Should have a queue". Paparan tetapan crash.

Bug itu bukan listener tetingkap dan bukan juga sebenarnya konsep "batal semasa blur." Ia adalah bahawa salah satu daripada dua listener itu ialah satu addEventListener mentah pada satu elemen yang diurus React. React tidak tahu tentang listener itu, tidak membersihkannya semasa unmount, dan — yang penting — pelayar mencetuskan blur semasa pembuangan DOM yang sedang dilakukan React itu sendiri, jadi handler masuk semula ke logik state anda pada saat paling buruk yang mungkin. Mesej commit pembetulan itu mengatakannya lebih tepat daripada yang saya boleh: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle."

Pembetulannya

bf28a50, dua belas minit selepas crash itu lahir, melakukan dua perkara kecil.

Pertama, ia membuang listener input mentah dan menyimpan hanya yang tetingkap, iaitu satu-satunya yang memang perlu menjadi satu listener DOM manual (tiada React onBlur untuk "seluruh tetingkap hilang fokus"):

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

Kedua, ia memindahkan kes klik-menjauh — "pengguna klik satu elemen lain" — ke prop onBlur milik React sendiri pada input, dan menangguhkannya satu detik:

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

Kedua-dua bahagian itu penting. Menggunakan prop onBlur React dan bukan satu listener mentah bermakna React memiliki kitaran hayat handler dan tidak akan membiarkannya terjuntai. Dan setTimeout(…, 0) menjamin bahawa walaupun blur ini memang dicetuskan oleh satu unmount, panggilan cancelEditing() mendarat pada detik berikutnya — selepas render dan commit semasa selesai — supaya ia tidak pernah boleh mengemas kini state pada satu komponen yang masih dirobohkan. Jika komponen itu sudah hilang menjelang ketika itu, panggilan tertangguh itu ialah satu no-op yang tidak berbahaya.

Itulah keseluruhan pembetulan. Buang listener yang React tidak dapat lihat; biar React menjadualkan yang tinggal.

Mengapa ini jenis membosankan yang baik

Ada godaan untuk memfailkan ini sebagai "remeh" — satu setTimeout, satu baris dibuang — dan teruskan. Tetapi bentuknya ialah salah satu cara paling lazim aplikasi desktop React crash, dan ia berbaloi dihayati:

  • Crash hanya muncul pada satu sempadan kitaran hayat (unmount), dicetuskan oleh satu event yang tidak dijadualkan framework (satu blur natif semasa pembuangan DOM). Ia tidak akan muncul dalam satu ujian klik-lalu; ia memerlukan laluan khusus "fokus beredar semasa menyunting."
  • Punca akarnya ialah mencampur dua model pemilikan — event DOM mentah dan event sintetik, terjadual milik React — pada elemen yang sama. Pada saat handler anda memutasikan state yang boleh meng-unmount elemen itu, laluan mentah menjadi re-entrant.
  • Pembetulannya bukan "tambah satu bendera pengawal" (walaupun satu ref isMounted akan menyelubunginya); ia membuang listener mentah sepenuhnya, iaitu perubahan lebih kecil dan yang betul. Penolakan sekali lagi.

Dan meta-pengajarannya, yang seluruh siri ini terus mengelilinginya: baca diff-nya, bukan nota keluaran. "Fixed a crash while editing keyboard shortcuts" adalah tepat, tetapi ia menyembunyikan bahawa crash itu ditimbulkan sendiri, pada pagi yang sama, dan tidak pernah dikeluarkan — dan bahawa keluaran yang sebenar ialah satu kemenangan ketepatan yang senyap tentang bila hendak menggantung satu pintasan global.

Apa lagi yang dikeluarkan

Dua perkara lagi menumpang dalam v1.8.10, kedua-duanya berbaloi satu baris. Sistem pintasan juga memperoleh pemulihan-kendiri merentasi tidur/bangun: macOS boleh menggugurkan secara senyap pendaftaran pintasan global satu aplikasi apabila mesin tidur, jadi satu handler sambung semula kini mengesahkan dan mendaftarkannya semula, bermakna pintasan anda masih berfungsi selepas komputer riba bangun tanpa pelancaran semula. Dan di sisi AI, klien menjadi lebih pintar tentang had kadar — permintaan streaming yang melanggar satu 429 kini berundur secara eksponen dan menetapkan semula pembilangnya apabila anda berhenti merakam, dan bukan menunggu satu kelewatan tetap. Tiga pembetulan kebolehpercayaan pada subsistem berbeza, salah satunya ialah crash yang tidak pernah menjadi crash.

Untuk bab sebelumnya dalam kisah v1, mencetak mesyuarat ke PDF tanpa pustaka PDF (v1.8.9); dan untuk keseluruhan lengkungnya, anatomi mengeluarkan perisian hingga ke kesempurnaan.

Artikel Berkaitan

Mencetak Mesyuarat ke PDF Tanpa Pustaka PDF
Steven
Steven9 minit bacaan

Mencetak Mesyuarat ke PDF Tanpa Pustaka PDF

GeekBye mengeksport sebuah mesyuarat sebagai PDF, dan tiada pustaka PDF di mana-mana dalam kodnya. Ia me-render HTML dalam satu tetingkap pelayar yang tidak kelihatan dan mencetaknya. Pilihan itu ialah keseluruhan ceritanya: ia menjadikan ciri ini mudah dibina dan memberinya setiap kegagalan yang dimiliki sebuah pelayar sebenar — satu kilatan putih, satu had panjang URL, dan satu page break yang memotong screenshot menjadi dua. Penyelesaian untuk bug paling hodoh ialah satu baris CSS.

Kejuruteraan
Electron
Desktop
Daripada OCR ke Piksel Tanpa Kehilangan Fallback
Steven
Steven7 minit bacaan

Daripada OCR ke Piksel Tanpa Kehilangan Fallback

GeekBye berhenti membaca screenshot anda dengan OCR dan mula menghantar imej sebenar ke model vision — yang merupakan bahagian mudahnya. Bahagian yang memberi pengajaran ialah pengoptimuman yang melangkau OCR sepenuhnya untuk pengguna vision, secara senyap mematahkan fallback ke model bukan-vision, dan terpaksa ditarik balik ke satu versi yang jaring keselamatannya berjalan selari dan tidak berkos apa-apa.

Kejuruteraan
AI
Desktop
Dua Mod Kegagalan Sebuah Overlay Click-Through
Steven
Steven8 minit bacaan

Dua Mod Kegagalan Sebuah Overlay Click-Through

Tetingkap GeekBye terapung di atas segala-galanya dan membenarkan klik anda menembusinya — kecuali di tempat ia mempunyai butang. Itu satu kontrak dua sisi, dan v1.8.5 serta v1.8.14 ialah wajahnya apabila setiap sisi patah: satu release yang overlay-nya menelan sebuah dialog sistem, satu lagi yang ia mencuri ketukan kekunci anda. Penyelesaian yang menang bagi yang kedua ialah memadamkan kod.

Kejuruteraan
Electron
Desktop