Steven
Steven10 मिनट पढ़ें

एक click-through overlay के दो failure modes

GeekBye की window हर चीज़ के ऊपर तैरती है और आपके clicks को अपने आर-पार जाने देती है — सिवाय वहाँ जहाँ उसके buttons हैं। यह एक दो-तरफ़ा contract है, और v1.8.5 तथा v1.8.14 वही हैं जो हर तरफ़ के टूटने पर दिखता है: एक release जहाँ overlay ने एक system dialog निगल लिया, एक जहाँ उसने आपके keystrokes चुरा लिए। दूसरे का जीतने वाला fix code मिटाना था।

इंजीनियरिंग
Electron
डेस्कटॉप
GeekBye रिलीज़
एक click-through overlay के दो failure modes

GeekBye की main window कोई आम window नहीं है। यह frameless, transparent, और always on top है — काँच की एक शीट जो आप जो भी कर रहे हों उसके ऊपर तैरती है, जिस पर कुछ controls painted हैं। दिलचस्प engineering यह नहीं कि यह तैरती है; बल्कि यह कि आपका mouse controls को छोड़कर हर जगह काँच के सीधे आर-पार निकल जाना चाहिए। overlay पर एक button click करो और click overlay को मिलता है। उसके बग़ल की ख़ाली जगह click करो और click नीचे आपके meeting app पर जा गिरता है, मानो overlay वहाँ था ही नहीं।

यह एक दो तरफ़ों वाला contract है: ठीक वहीं input लो जहाँ UI है, और बाक़ी हर जगह input के लिए पूरी तरह अदृश्य रहो। दोनों तरफ़ों को हर पल क़ायम रहना है, और app के इस हिस्से में हर bug दोनों में से किसी एक तरफ़ का उल्लंघन है। दो महीने की दूरी पर दो releases सबसे साफ़-सुथरी मिसाल हैं — क्योंकि हर एक ने अलग तरफ़ तोड़ी।

contract, तंत्र के हिसाब से

implementation एक hook है, useMousePassthrough, और वह छोटा है। window by default click-through है। एक global mousemove listener हर हरकत पर एक सवाल पूछता है: cursor किसी control के ऊपर है क्या? controls पर एक data-ui-region attribute का tag लगा होता है, तो test शब्दशः target.closest('[data-ui-region]') है। किसी control के ऊपर, यह setIgnoreMouseEvents(false) call करता है और window clicks पकड़ लेती है। transparent जगह के ऊपर, यह setIgnoreMouseEvents(true, { forward: true }) call करता है और clicks आर-पार गिर जाते हैं।

वह forward: true ही भार उठाने वाली बारीक़ी है। एक बार जब कोई window mouse events को ignore करने लगती है, तो आम तौर पर वह उन्हें पूरी तरह लेना बंद कर देती है — उन mousemove events समेत जो hook को यह भाँपने के लिए चाहिए कि cursor किसी control के ऊपर वापस आ रहा है। forward: true Electron से कहता है कि फिर भी हरकतें उस ignore कर रही window तक forward करता रह, ताकि वह ख़ुद को दोबारा तैयार कर सके। इसके बिना, overlay एक बार click-through होकर फिर कभी वापस नहीं आती। hook सिर्फ़ state के transitions पर ही — जब cursor UI और काँच के बीच की सीमा पार करता है — IPC सीमा के पार call करता है, हरकत के हर pixel पर नहीं। उस optimization को याद रखिए; उसमें एक छेद है, और v1.8.5 उसमें गिरी।

v1.8.5: जब transparency वाली तरफ़ टूटती है

contract की transparency वाली तरफ़ है "हर उस जगह input के लिए अदृश्य रहो जहाँ कोई UI नहीं।" v1.8.5 वही है जो तब दिखता है जब overlay हद से ज़्यादा हाज़िर हो — जब वह उस input को पकड़ या रोक ले जिसे उसे गुज़रने देना चाहिए था। ट्रिगर macOS के permission dialogs थे, और एक-दूसरे के ऊपर चढ़े हुए दो अलग-अलग failures थे।

overlay भौतिक रूप से dialog के ऊपर थी। एक always-on-top window का एक level होता है, और GeekBye की overlay macOS के दिए सबसे ऊँचे level पर बैठी थी — screen-saver। वह क़रीब-क़रीब हर चीज़ के ऊपर है, ख़ुद system के Microphone और Screen Recording permission dialogs समेत। तो onboarding के दौरान, ठीक उसी पल जब macOS ने permission माँगी, dialog overlay के नीचे render होकर अदृश्य हो सकता था। fix, setPermissionMode, सिर्फ़-macOS वाला है और एक ग़ैर-सहज काम करता है: एक permission request की अवधि भर के लिए यह overlay को floating level तक नीचे कर देता है, फिर उसके बाद उसे बहाल कर देता है। और उसे बहाल करने के लिए एक ख़ास तरक़ीब चाहिए थी, जो commit में ही दर्ज है: macOS "जब पहले वाले ही level को दोबारा set किया जाए तो window को हमेशा दृश्य रूप से दोबारा नहीं उठाता," तो code window को एक बीच के level से क़दम-दर-क़दम गुज़ारता है — floating → status → screen-saver — ताकि window server को stacking order दोबारा हिसाब लगाने पर मजबूर किया जा सके। आप जो level चाहते हैं वह सीधे set नहीं करते; आप उस तक चलकर पहुँचते हैं।

overlay dialog के clicks खा रही थी। onboarding एक अँधेरा पूरे-window का backdrop paint करता है, और passthrough hook उसे ख़ास मामला मानता है: जब तक वह backdrop खड़ा है, यह पूरी window पर click-capture ज़बरन लगा देता है ताकि wizard पूरी तरह interactive रहे। पर "हर click पकड़ो" ठीक तभी ग़लत है जब एक system dialog एक click इकट्ठा करने की कोशिश कर रहा हो। fix एक permission request के दौरान backdrop का marker attribute छील देता है, तो hook capture mode से बाहर निकल आता है और dialog के buttons को उनके clicks मिल जाते हैं। overlay अपने ख़ुद के controls को interactive रखती है; वह बस ख़ाली काँच के button होने का नाटक बंद कर देती है।

optimization में वह छेद। याद रखिए कि hook सिर्फ़ transitions पर काम करता है। जब window capture mode से निकली, तो उसने hover test दोबारा चलाया — पर अगर उस पल आपका cursor पहले से ही किसी control पर बैठा था, तो "क्या हम UI के ऊपर हैं?" का जवाब पहले से ही "हाँ" था, तो कोई transition नहीं छिड़ी, और click-through कभी disable ही नहीं हुआ। control ज़िंदा दिख रहा था और मरा हुआ था। fix (1b6eb20, उद्धृत) transition पर भरोसा करना छोड़ देता है: "when exiting onboarding mode, directly evaluate whether cursor is over a UI region and make the correct IPC call." जब आप modes बदलते हैं, तो आप अगली हरकत के आपको बताने का इंतज़ार नहीं करते कि cursor कहाँ है — आप देखते हैं।

और छोड़ाई को उलटाने लायक़ बनाओ। इस सबमें छिपा सूक्ष्म ख़तरा यह है कि overlay अब अस्थायी रूप से अपना z-order और अपनी click-capture छोड़ रही है, और किसी चीज़ को उन्हें वापस देना है। अगर बहाली वाली call कोई exception फेंक दे — एक IPC हिचकी, request के बीच में तोड़ी गई एक window — तो app isRequestingPermission के true पर अटके रहने और एक अँधेरे backdrop के आपकी पूरी screen को हमेशा के लिए छुपाने के साथ फँसा रह सकता है। तो exit path को इस तरह लपेटा गया कि state-reset "हमेशा execute होता है," साथ में एक कई-मिनट का safety timeout और window destroy पर एक cleanup। इससे निकलने वाला नियम: हर बार जब कोई overlay input छोड़े, तो छोड़ाई का उलटाव एक happy path पर नहीं, एक finally में होना चाहिए।

v1.8.14: जब capture वाली तरफ़ टूटती है

contract की दूसरी तरफ़ है "जहाँ UI है वहाँ input लो।" v1.8.14 वही है जो तब दिखता है जब overlay हद से ज़्यादा झपटने वाली हो — जब उससे बरतना आपको उस app का input focus गँवा दे जिसकी आप असल में परवाह करते हैं।

changelog में लक्षण focus-चुराना है, और यहाँ ईमानदार version है, क्योंकि git history release note से ज़्यादा सिखाती है। overlay के shortcut handlers — Cmd+B पर visibility toggle करना, chat window खोलना, उसे scroll करना — हर एक पहले mainWindow.focus() call करता था, और macOS पर app.focus({ steal: true }), ताकि यक़ीन हो कि जब आपने उसे trigger किया तो overlay सबसे आगे थी। दिक़्क़त यह है कि overlay को focus करना जो भी पहले सबसे आगे था उसे निष्क्रिय कर देता है — आपकी meeting, आपका editor — तो उनके लिए बनी एक keystroke कहीं न गिरे, या ग़लत जगह गिरे। overlay एक ऐसी चीज़ झपट रही थी जिसकी उसे असल में ज़रूरत ही नहीं थी।

वह आख़िरी हिस्सा ही पूरा सबक़ है, और team ने उसे मुश्किल तरीक़े से पाया। जो branch v1.8.14 बनी उसने पहले ख़ासकर dragging हल करने की कोशिश की — क्योंकि dragging के पास इस bug का अपना सुंदर version है: जब आप एक full-screen overlay drag करते हैं, तो window एक स्थिर cursor के नीचे खिसकती है, तो hover model सोचता है कि pointer ने control "छोड़ दिया" और drag के बीच में click-through में पलट जाता है, drag गिरा देता है। तो उन्होंने एक dragLock जोड़ा। फिर उन्होंने पूरी window को non-focusable बनाने की कोशिश की — जिसने typing और dragging तोड़ दी, तो उन्होंने focusability बहाल की, फिर non-focusability को घटाकर सिर्फ़ text inputs तक ले आए, फिर वह toggle उखाड़ फेंका। उन्होंने Electron के native drag की जगह एक custom drag IPC बनाया। उन्होंने ग़ैर-उम्मीद focus पर blur जोड़ा। इसका एक हफ़्ता।

फिर उन्होंने उसका क़रीब सब कुछ मिटा दिया। जो commit ship हुई उसने custom drag, focusable toggle, और blur handler को revert किया, और बिलकुल एक ही विचार रखा: focus() call करना बंद करो। एक code comment में लिखा गया वह एहसास यह है कि overlay को शुरू से OS focus की कभी ज़रूरत ही नहीं थी — "IPC messages (webContents.send) work without window focus." overlay अपने ख़ुद के renderer को सीधे चलाती है; focus शुद्ध collateral damage था। ship करने लायक़ fix एक दिन में बनाया गया वह चतुर custom-drag rewrite नहीं था। वह उन focus calls को घटाना था जिन्हें वहाँ होना ही नहीं चाहिए था। (साफ़ कह देना बनता है: changelog कहता है "while dragging it," पर असल में जो ship हुआ वह keyboard-shortcut handlers से focus-चुराना हटाना था — drag के लिए ख़ास मशीनरी को उसी हफ़्ते टटोला और revert किया गया था।)

दाँव, सचमुच के रूप में

अगर आप सबूत चाहते हैं कि यह contract किताबी नहीं है, तो देखिए कि महीनों बाद क्या हुआ जब overlay इसकी ग़लत तरफ़ पर फँस गई। एक कहीं बाद वाली release में, renderer process तब crash हुआ जब overlay capture mode में थी — setIgnoreMouseEvents(false), window सारे clicks झपटे हुए — और उसे छोड़ने के लिए कोई ज़िंदा UI न होने से, वह अदृश्य पूरे-desktop वाली window वहीं बैठी रही, user की machine पर हर click खाती रही जब तक उन्होंने force-quit नहीं किया। capture mode में फँसी एक transparent window कोई मरा हुआ button नहीं है; वह पूरे-desktop भर का एक click trap है। (उसका अपना diagnosis हुआ — देखें जब आपका AI notetaker meeting के बीच में recording रोक देता है, जो उसे उसी release के एक बहुत अलग bug के साथ जोड़ता है।) यह पूरी समस्या का सबसे तीखा बयान है: toggle को एक तरफ़ फँसा दो और आपका UI मर गया; दूसरी तरफ़ फँसा दो और आपका पूरा desktop मर गया।

इन releases ने हमें तीन बातें सिखाईं

  1. एक click-through overlay एक दो-तरफ़ा contract है, और दोनों तरफ़ें अलग-अलग तरीक़े से fail होती हैं। बहुत ज़्यादा transparent और आपके ख़ुद के controls मर जाते हैं; बहुत ज़्यादा झपटने वाली और आप दूसरी apps का input निगल लेते हैं। ज़्यादातर overlay bugs उन्हीं दोनों में से बस एक हैं, और यह नाम लेना कि आप किस तरफ़ हैं आपको बता देता है कि कहाँ देखना है।
  2. सबसे ऊँचा always-on-top level कोई इनाम नहीं है। हर चीज़ के ऊपर बैठने का मतलब है उन system dialogs के ऊपर बैठना जिन्हें आप चाहते हैं कि user देखे। इस बात के लिए design करो कि आपकी overlay को कब शालीनता से एक क़दम पीछे हटना चाहिए — और वापस एक क़दम ऊपर चढ़ने को एक finally में उलटाने लायक़ बनाओ, क्योंकि आधी-पूरी हुई छोड़ाई कभी न छोड़ने से बदतर है।
  3. घटाव चतुराई को हरा देता है। v1.8.14 का जीतने वाला fix उन focus() calls को मिटाना था जिनकी overlay को कभी ज़रूरत नहीं थी, जो custom drag handling बनाने और revert करने के एक हफ़्ते के बाद ही मिला। जब कोई feature platform से लड़ता ही रहे, तो पहला सवाल यह पूछना है कि क्या आप वह चीज़ ही हटा सकते हैं जो लड़ाई कर रही है।

v1 कहानी के पिछले अध्याय के लिए, देखें release को CI में डालना, दो बार (v1.8.4); और पूरे चाप के लिए, सॉफ़्टवेयर को परिपूर्णता तक पहुँचाकर भेजने की शारीर-रचना

संबंधित लेख

OCR से pixels तक, fallback गँवाए बिना
Steven
Steven9 मिनट पढ़ें

OCR से pixels तक, fallback गँवाए बिना

GeekBye ने आपके screenshots को OCR से पढ़ना बंद किया और असली image को vision models तक भेजना शुरू किया — और वह आसान हिस्सा है। सीखने लायक़ हिस्सा वह optimization है जिसने vision users के लिए OCR को पूरा छोड़ दिया, non-vision models तक के fallback को चुपके से तोड़ दिया, और उसे एक ऐसे version तक वापस समेटना पड़ा जहाँ safety net समानांतर में चलता है और कुछ भी ख़र्च नहीं करता।

इंजीनियरिंग
AI
डेस्कटॉप
release को CI में डालना, दो बार
Steven
Steven9 मिनट पढ़ें

release को CI में डालना, दो बार

GeekBye v1.8.4 ने macOS और Windows दोनों के release builds को CI में ले जाया। changelog जो नहीं बताता वह यह है: हमने यह एक बार पहले भी आज़माया था, runner cost की वजह से एक महीने बाद उसे मिटा दिया, और दूसरी बार में ही जाकर वह टिका — क्योंकि तब तक release script हाथ से पहले ही चल रहा था।

इंजीनियरिंग
CI/CD
डेस्कटॉप
बिना किसी safety net के तीस भाषाएँ ship करना
Steven
Steven9 मिनट पढ़ें

बिना किसी safety net के तीस भाषाएँ ship करना

GeekBye v1.8.3 ने react-i18next को जोड़ा और पूरे app को 30 भाषाओं में translate कर दिया। library वाला हिस्सा रवायती था। दिलचस्प हिस्सा वह है जो ग़ायब था: न कोई key-parity check, न कोई string extractor — इसलिए hardcoded English रिस गई, एक Danish typo ship हो गया, और changelog यह तय ही नहीं कर पाया कि app 28, 29 या 30 भाषाएँ बोलता है।

इंजीनियरिंग
i18n
डेस्कटॉप