Skip to content

fix(macOS): re-enable event tap after macOS disables it - #332

Open
duyhnynh wants to merge 1 commit into
tuyenvm:masterfrom
duyhnynh:fix/reenable-event-tap-after-macos-disables-it
Open

duyhnynh wants to merge 1 commit into
tuyenvm:masterfrom
duyhnynh:fix/reenable-event-tap-after-macos-disables-it

Conversation

@duyhnynh

@duyhnynh duyhnynh commented Aug 28, 2026 •

Copy link
Copy Markdown

The problem I ran into

I use OpenKey every day for Vietnamese typing on macOS, alongside Docker Desktop for work.

Every time I opened or restarted Docker Desktop, Vietnamese typing stopped working — in every app, not just Docker. OpenKey still looked fine: the menu bar icon still showed "V", and the switch key still toggled between V and E. But whatever I typed came out as plain keys, as if OpenKey was not there. as stayed as instead of becoming á.

The only way back was to quit OpenKey and open it again. Then it happened again the next time Docker Desktop restarted.

Restarting Docker Desktop boots a VM and restarts containers, which keeps the CPU busy for a while. That turned out to be the trigger. Docker is not doing anything to OpenKey directly — it just makes the machine slow enough to expose the bug below.

This is the same symptom as #258 ("Tất cả các ứng dụng đều không gõ được VI ... khắc phục: reboot lại OS và bật lại OpenKey").

Root cause

initEventTap creates an active event tap:

eventTap = CGEventTapCreate(kCGSessionEventTap,
                            kCGHeadInsertEventTap,
                            0,                    // active tap
                            eventMask,
                            OpenKeyCallback,
                            NULL);

macOS puts a deadline on an active tap's callback. If OpenKeyCallback does not return in time — which happens when the machine is under sustained load — the system switches the tap off and reports it back through the same callback as the pseudo event type kCGEventTapDisabledByTimeout. kCGEventTapDisabledByUserInput is the other case.

Two things then go wrong on master:

  1. OpenKeyCallback never checks type against those two values, so the notification is ignored.
  2. CGEventTapEnable is called exactly once, at the end of initEventTap.
$ grep -rn "kCGEventTapDisabled" .
(no matches)

So once macOS turns the tap off, nothing ever turns it back on. The rest of the app keeps running, which is why the menu bar and the switch key still respond and it looks like OpenKey is working.

Fix

Handle both notifications at the top of the callback and re-enable the tap. eventTap is file-static in OpenKeyManager.m, so re-enabling goes through a small C function there, in the same style as the existing cross-file extern declarations.

The normal event path is untouched — the guard returns before any existing logic runs.

Testing

  • xcodebuild -project Sources/OpenKey/macOS/OpenKey.xcodeproj -scheme OpenKey -configuration Release → BUILD SUCCEEDED (Xcode 26.1.1, macOS 26.3)
  • nm on the built binary confirms the new path is present and CGEventTapEnable now has two call sites instead of one.
  • No new warnings.

Note: MACOSX_DEPLOYMENT_TARGET in the project is 10.14, which no longer builds on current Xcode (SDK does not contain 'libarclite'). I built with an override; that is a pre-existing issue unrelated to this change and I have not touched the project file.

Verification of the disable → re-enable path

I could not force the tap to be disabled on demand myself: it needs the callback to be mid-flight during a CPU stall, so it only happens while actually typing.

@scvbuild has since reproduced it directly in this comment by stalling OpenKeyCallback for 3 seconds in a test-only build. macOS delivered type = 0xFFFFFFFE (kCGEventTapDisabledByTimeout), and with this patch CGGetEventTapList() reported OpenKey's tap as enabled=YES again on the same process, without relaunching the app.

Refs #258

@scvbuild

Copy link
Copy Markdown

Xác nhận bản vá hoạt động, kèm bằng chứng cho phần "What I did not verify"

Mình đã tái hiện được đúng khoảnh khắc macOS tắt event tap mà tác giả PR nói chưa bắt được, và xác nhận tap tự bật lại.

Môi trường: macOS 26.6.2, Apple Silicon, build arm64.

Cách build: vì MACOSX_DEPLOYMENT_TARGET = 10.14 không build được trên Xcode hiện tại, mình build bằng clang của Command Line Tools, lấy mã nguồn ở tag 2.0.5 cộng commit gỡ CFRunLoopRun() lồng nhau trong initEventTap, rồi áp PR này. Patch apply sạch, không có xung đột.

Thay đổi nhỏ so với PR: mình đổi OpenKeyReEnableEventTap(void) thành nhận thêm tham số reason và truyền type vào, chỉ để ghi os_log. Logic bật lại giữ nguyên như PR.

Cách ép lỗi xảy ra: trong một bản build riêng chỉ dùng để thử, mình chèn usleep(3000000) vào đầu OpenKeyCallback cho 2 phím đầu tiên, để callback vượt hạn mức thời gian của macOS. Phần chèn này không có trong bản dùng thật.

Kết quả (log, mốc thời gian tương đối):

+0s   TEST: co tinh treo callback 3 giay (con lai 1)
+3s   TEST: co tinh treo callback 3 giay (con lai 0)
+6s   macOS tat bay phim (reason=4294967294), dang bat lai

4294967294 là 0xFFFFFFFE, tức kCGEventTapDisabledByTimeout. Đúng như phân tích trong PR.

Ngay sau đó, CGGetEventTapList() báo tap của OpenKey enabled=YES trở lại, trên cùng process, không cần khởi động lại app. Trước khi có bản vá, tình huống này là lúc phải thoát và mở lại OpenKey.

Lưu ý: mình mới chạy bản vá trong thời gian ngắn nên chưa kết luận được gì về độ ổn định dài hạn. Phần được xác nhận ở đây là cơ chế: notification có tới, và tap bật lại được.

initEventTap creates an active event tap, so macOS puts a deadline on
OpenKeyCallback. When the callback misses that deadline (sustained CPU
load) or on kCGEventTapDisabledByUserInput, the system switches the tap
off and reports it back through the callback as a pseudo event type.

OpenKeyCallback never checked for those two types, and CGEventTapEnable
was only ever called once during setup. Once macOS turned the tap off,
nothing turned it back on: the menu bar still showed the current mode
and the switch key still worked, but no keystroke was converted in any
app until OpenKey was relaunched.

Handle both notifications in the callback and re-enable the tap. The
normal event path is unchanged.

Refs tuyenvm#258
@duyhnynh
duyhnynh force-pushed the fix/reenable-event-tap-after-macos-disables-it branch from 5a5e5fd to 21b98e3 Compare September 14, 2026 09:10
@duyhnynh

duyhnynh commented Sep 14, 2026 •

Copy link
Copy Markdown
Author

Cảm ơn bạn đã test và gửi log chi tiết.

Phía mình chưa ép được lỗi xảy ra nên phần xác minh còn thiếu. Log của bạn bổ sung đúng chỗ đó: macOS gửi kCGEventTapDisabledByTimeout (0xFFFFFFFE) và tap bật lại trên cùng process. Mình đã thêm link tới comment này vào mô tả PR.

Về việc ghi os_log kèm type: code hiện tại gần như không dùng log, nên mình tạm giữ PR gọn như bây giờ. Nếu maintainer thấy cần thì mình thêm sau.

Đồng ý là cần dùng thêm một thời gian mới nói được về độ ổn định. Nếu có gì bất thường thì mình cùng xem tiếp nhé.

jetaudio added a commit to jetaudio/Omnikey that referenced this pull request Oct 7, 2026
Adapt upstream PRs tuyenvm#324, tuyenvm#329, tuyenvm#332 and tuyenvm#333. Preserve saved shortcuts, bound Spotlight focus queries, avoid reusing backspace events, and send complete UTF-16 chunks. Cover event processing and tap lifecycle under sanitizers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants