Skip to content

fix(input-monitor): harden event tap to avoid IME conflict (#103)#106

Open
debugtheworldbot wants to merge 4 commits intomainfrom
fix/issue-103
Open

fix(input-monitor): harden event tap to avoid IME conflict (#103)#106
debugtheworldbot wants to merge 4 commits intomainfrom
fix/issue-103

Conversation

@debugtheworldbot
Copy link
Copy Markdown
Owner

Summary

  • 修复 CGEventTap 被系统禁用(tapDisabledByTimeout / tapDisabledByUserInput)后一直处于半死状态,可能干扰输入法事件链路的问题。
  • 把事件 tap 回调线程上对 NSRunningApplication(processIdentifier:) 的同步查询移到后台队列,避免回调超时反过来触发 tap 被禁用。
  • 推测这是 [Report]: 与微信输入法存在冲突 #103 中 KeyStats 与微信输入法在 macOS 15.7.4 下概率性冲突(重启后 IME 无法输入中文)的成因。

Changes

Fix

  • InputMonitor.swift:事件回调收到 tap-disable 信号时立即重新启用 tap。
  • AppActivityTracker.swift:改写为 Cocoa 薄封装,内部使用新的 AppIdentityCache
  • AppStats.swift:新增 AppIdentity / AppIdentityCache(纯 Swift,PID 缓存 + 异步解析 + 同 PID 并发合并)。
  • StatsModels.swift:抽出 isTapDisabledSignal(_:) 供回调使用与测试覆盖。
  • 前台 App 切换时预填 PID → bundleId 缓存,避免前台 App 事件走异步解析。

Test

  • 新增 KeyStatsTests/AppIdentityCacheTests.swift(13 个用例):
    • isTapDisabledSignal 仅对 timeout / userInput 返回 true
    • 未命中缓存立即返回 frontmost,resolver 异步执行,绝不在调用线程同步触发
    • 同 PID 的并发查询合并为单次解析
    • resolver 失败不写缓存、允许重试
    • updateFrontmost 预填 PID 让后续查询零解析开销
  • Package.swift 加入新测试文件。

Test plan

  • swift test 全部通过(39 个测试,+13 新增)
  • 在 macOS 15.7 + 微信输入法环境验证重启后能正常拼音输入(本地无该环境,请社区帮忙验证)
  • Xcode 构建 KeyStats app target 通过

Closes #103

Re-enable the CGEventTap on tapDisabledByTimeout / tapDisabledByUserInput
instead of leaving it dormant, and move the synchronous
NSRunningApplication lookup off the event tap callback thread via a new
AppIdentityCache that resolves unknown PIDs asynchronously and coalesces
concurrent lookups. Pre-cache the frontmost app's PID so the common path
avoids the async hop entirely. These two changes keep the callback fast
and the tap healthy, reducing interference with input methods such as
WeChat IME on macOS 15.

Refs #103
Lock in the core invariants behind the IME-conflict fix:
- resolver is never called synchronously from the event-tap callback path
- concurrent lookups for the same PID coalesce into a single resolution
- isTapDisabledSignal flags only tapDisabledByTimeout / tapDisabledByUserInput

Refs #103
Copilot AI review requested due to automatic review settings April 22, 2026 04:10
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an AppIdentityCache to handle application identity resolution asynchronously, avoiding event tap delays, and adds logic to recover from disabled event taps. Technical feedback points out a potential crash in the tap callback when handling system-disabled signals, suggests optimizing lock contention within the cache, and recommends implementing negative caching for failed resolutions to prevent task queue bloat.

Comment thread KeyStats/InputMonitor.swift Outdated
Comment thread KeyStats/AppStats.swift
Comment thread KeyStats/AppStats.swift
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens KeyStats’ macOS input event tap pipeline to avoid IME conflicts by promptly re-enabling disabled event taps and moving app-identity resolution off the event-tap callback path via an asynchronous PID→bundleId cache.

Changes:

  • Add isTapDisabledSignal(_:) and use it in the event tap callback to immediately re-enable the tap when the system disables it.
  • Introduce AppIdentity + AppIdentityCache to resolve PID→(bundleId,name) asynchronously with coalescing of concurrent resolutions.
  • Add a new focused test suite for AppIdentityCache behavior and wire it into swift test.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
Package.swift Includes the new AppIdentityCacheTests.swift in the explicit SwiftPM test target sources list.
KeyStatsTests/AppIdentityCacheTests.swift Adds 13 tests covering tap-disable signal detection and AppIdentityCache async/coalescing semantics.
KeyStats/StatsModels.swift Adds isTapDisabledSignal(_:) helper for reuse + test coverage.
KeyStats/InputMonitor.swift Re-enables the event tap upon receiving .tapDisabledByTimeout / .tapDisabledByUserInput signals.
KeyStats/AppStats.swift Introduces AppIdentity, dispatcher abstraction, and the thread-safe async AppIdentityCache.
KeyStats/AppActivityTracker.swift Refactors to use AppIdentityCache and resolves NSRunningApplication(processIdentifier:) on a utility queue.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Merge the cache-hit / pending-check / frontmost-read into a single
  lock block via lookupLocked(pid:), removing two lock roundtrips from
  the uncached fast path on the event tap callback thread.
- Cache resolver failures in unresolvablePIDs so events from
  unresolvable system processes no longer spawn a new background
  resolution on every hit.
- Clear the negative-cache entry for a PID when it shows up as
  frontmost, so PID reuse after process termination is handled.

Addresses: #106 (comment)
Addresses: #106 (comment)
Apple's docs note that the event parameter is NULL when the tap callback
is invoked with a tap-disabled notification. Swift maps CGEventTapCallBack
to a non-optional CGEvent, so calling Unmanaged.passUnretained on a NULL
event is a latent crash. The disable path is purely a state signal, so
returning nil matches the notification semantics and is safe.

Addresses: #106 (comment)
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.

[Report]: 与微信输入法存在冲突

2 participants