Skip to content

feat: CGEvent tap diagnostics — conflict detection + live event monitor#276

Merged
AprilNEA merged 6 commits into
masterfrom
feat/cgevent-diagnostics
Jul 18, 2026
Merged

feat: CGEvent tap diagnostics — conflict detection + live event monitor#276
AprilNEA merged 6 commits into
masterfrom
feat/cgevent-diagnostics

Conversation

@AprilNEA

@AprilNEA AprilNEA commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Adds an in-app way to answer "is another app intercepting my mouse / what's in the CGEvent stream?" — the productized version of hand-running CGGetEventTapList.

What's in it

  • openlogi-hook: Hook::list_event_taps() enumerates every event tap in the login session (owner, location, active/listen, enabled) via a hand-bound CGGetEventTapList + proc_pidpath (read-only, no Accessibility needed). EventTapInfo::gates_input() / known_input_conflict() classify the lag-relevant ones against a curated driver list (Logi Options+, SteerMouse, BetterMouse, Mac Mouse Fix, LinearMouse, …). A list_taps example doubles as a headless diagnostic. Latency fields are dropped (uninitialised sentinel values).
  • agent (protocol v4): an opt-in live event monitor. A shared EventMonitor buffers button/scroll/interrupt events (pointer moves excluded); the freeze-sensitive hook callback pays only one relaxed atomic load while monitoring is off. New poll_event_monitor IPC method drains + implicitly enables it; an idle janitor disables it once the GUI stops polling. Bumps PROTOCOL_VERSION 3→4 and regenerates the wire goldens (agent + GUI ship together).
  • GUI: a macOS Diagnostics settings page. Release surfaces the curated conflict warning; debug builds also show the full tap list and the live monitor (polls the agent while the window is open). Five new strings translated into all 20 locales.

Verification

  • list_event_taps() output matches a reference Swift CGGetEventTapList dump byte-for-byte.
  • Unit tests (classification, monitor buffer) + regenerated wire goldens pass; GUI compiles/links, clippy-clean under -D warnings, i18n parity holds.
  • Diagnostics page rendered and visually confirmed (layout uses Axis::Vertical so the wide content wraps full-width).

Notes

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an in-app CGEvent tap diagnostics feature: Hook::list_event_taps() enumerates active taps via hand-bound CGGetEventTapList + proc_pidpath, a curated conflict list flags known competing input drivers, and an opt-in live event monitor (agent-side EventMonitor + poll_event_monitor IPC) feeds a debug panel in Settings. All three previously reported issues — Relaxed atomic ordering in poll/janitor, per-render CGGetEventTapList syscall storm, and the janitor's immediate first tick on agent restart — have been correctly addressed in the current commit.

  • event_monitor.rs: The Release/Acquire pairing on enabled is correct; interval_at delays the first janitor tick by a full IDLE_TICK to survive agent restarts.
  • macos.rs: The hand-declared CGEventTapInformation repr(C) struct reproduces the correct 48-byte Apple layout including the 4-byte padding before events_of_interest; process_name gracefully handles process exits.
  • Protocol: version bumped 8→9 with MonitorEvent variants marked append-only and wire goldens regenerated.

Confidence Score: 5/5

Safe to merge: adds a read-only diagnostic path with no new writes beyond a bounded VecDeque, and the hook hot path pays only one relaxed atomic load while monitoring is off.

All three concurrency issues from prior review rounds are correctly fixed. The repr(C) layout matches Apple's 48-byte stride. No correctness, data-safety, or security issues remain.

No files require special attention.

Important Files Changed

Filename Overview
crates/openlogi-agent-core/src/event_monitor.rs New EventMonitor with correct Release/Acquire pairing on enabled, interval_at for delayed first janitor tick, and bounded VecDeque buffer — previous thread concerns fully addressed.
crates/openlogi-hook/src/macos.rs Hand-bound CGGetEventTapList + proc_pidpath with correct repr(C) layout matching Apple's 48-byte CGEventTapInformation stride (including u64 alignment padding).
crates/openlogi-hook/src/lib.rs Adds EventTapInfo, TapLocation, gates_input(), known_input_conflict() with curated list; clean logic and well-covered by new unit tests.
crates/openlogi-agent-core/src/ipc.rs PROTOCOL_VERSION bumped 8→9 with MonitorEvent variants append-only; wire golden regenerated correctly.
crates/openlogi-gui/src/windows/settings.rs Monitor task correctly held in _monitor_task (drop-to-stop), taps cached in AppState to avoid per-frame syscalls; refresh_windows() fires unconditionally every 300ms.
crates/openlogi-gui/src/windows/settings/diagnostics.rs Diagnostics page correctly splits debug (cached taps) vs release (live taps) code paths; conflict dedup and format_monitor_event are straightforward.
crates/openlogi-agent-core/src/hook_runtime.rs monitor.record() injected before the match dispatch; single relaxed atomic load on the hot path when monitoring is off.
crates/openlogi-gui/src/ipc_client.rs PollEventMonitor command gated on cfg(all(target_os = macos, debug_assertions)); reply channel error ignored correctly with let _.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant GUI as GUI (debug)
    participant IPC as IPC Client
    participant Agent as AgentServer
    participant EM as EventMonitor
    participant Hook as Hook Callback
    participant Janitor as Idle Janitor

    Note over GUI: Settings window opens
    GUI->>IPC: PollEventMonitor(tx) every 300ms
    IPC->>Agent: poll_event_monitor RPC
    Agent->>EM: poll()
    Note over EM: polled.store(true,Relaxed) then enabled.store(true,Release)
    EM-->>Agent: Vec MonitorEvent
    Agent-->>IPC: Vec MonitorEvent
    IPC-->>GUI: reply via oneshot rx

    Hook->>EM: record(event)
    Note over EM: enabled.load(Relaxed) fast path

    Note over Janitor: every 3s via interval_at
    Janitor->>EM: enabled.load(Acquire)
    Note over EM: if enabled and not polled: disable()

    Note over GUI: Settings window closes
    Note over GUI: _monitor_task dropped, loop cancelled
    Note over Janitor: next tick disables monitoring
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant GUI as GUI (debug)
    participant IPC as IPC Client
    participant Agent as AgentServer
    participant EM as EventMonitor
    participant Hook as Hook Callback
    participant Janitor as Idle Janitor

    Note over GUI: Settings window opens
    GUI->>IPC: PollEventMonitor(tx) every 300ms
    IPC->>Agent: poll_event_monitor RPC
    Agent->>EM: poll()
    Note over EM: polled.store(true,Relaxed) then enabled.store(true,Release)
    EM-->>Agent: Vec MonitorEvent
    Agent-->>IPC: Vec MonitorEvent
    IPC-->>GUI: reply via oneshot rx

    Hook->>EM: record(event)
    Note over EM: enabled.load(Relaxed) fast path

    Note over Janitor: every 3s via interval_at
    Janitor->>EM: enabled.load(Acquire)
    Note over EM: if enabled and not polled: disable()

    Note over GUI: Settings window closes
    Note over GUI: _monitor_task dropped, loop cancelled
    Note over Janitor: next tick disables monitoring
Loading

Reviews (3): Last reviewed commit: "perf(gui): cache the diagnostics event-t..." | Re-trigger Greptile

Comment thread crates/openlogi-agent-core/src/event_monitor.rs
Comment thread crates/openlogi-gui/src/windows/settings.rs Outdated
Comment thread crates/openlogi-agent-core/src/event_monitor.rs
@AprilNEA
AprilNEA force-pushed the feat/cgevent-diagnostics branch from e7e4d10 to 7115200 Compare July 18, 2026 08:21
AprilNEA added 6 commits July 18, 2026 17:03
Enumerate every event tap in the login session via CGGetEventTapList:
owner process, tap location (HID/Session), active vs listen-only, and
enabled state. Read-only and needs no Accessibility grant, so it surfaces
input contention — a competing app holding an active HID tap is the
classic cause of pointer lag, and it also flags OpenLogi's own tap being
disabled.

The latency fields CGEventTapInformation carries are deliberately not
exposed: they hold uninitialised sentinel values that change between
samples, so they are not a trustworthy signal.

Non-macOS targets return an empty list. A list_taps example doubles as a
headless diagnostic.
EventTapInfo::gates_input flags the active+enabled+HID configuration that
can add system-wide latency; known_input_conflict matches the owner against
a curated list of third-party mouse drivers (Logi Options+, SteerMouse,
BetterMouse, Mac Mouse Fix, LinearMouse, …) so the GUI can warn about a
likely pointer-lag cause without false-flagging legitimate utilities.

Unit-tested: the gating predicate and case-insensitive owner matching.
Add an opt-in event monitor so the GUI can show what the mouse hook
observes. A shared EventMonitor buffers button/scroll/interrupt events
(pointer moves excluded as noise); the freeze-sensitive hook callback pays
only one relaxed atomic load while monitoring is off. The new
poll_event_monitor IPC method drains the buffer and implicitly enables
monitoring; an idle janitor disables it once the GUI stops polling, so a
closed panel or crashed GUI can't leave the callback buffering forever.

Bumps PROTOCOL_VERSION to 9 (poll_event_monitor appended last, MonitorEvent
added) and pins the new wire goldens.
Add a macOS Diagnostics settings page. In release it surfaces the curated
known-conflict check: if another app holds an active HID tap (Logi
Options+, SteerMouse, BetterMouse, …) it warns that the app may be causing
pointer lag. In debug builds it also dumps the full event-tap list and a
live event monitor that polls the agent's hook over IPC (poll_event_monitor)
while the window is open.

Stores polled events in AppState (debug macOS only) and adds the five
release-facing strings to all 20 locale files.
… the janitor's first tick

The enable path published `polled` and `enabled` with two Relaxed stores while
the idle janitor read them from another thread, also Relaxed. With no cross-
thread ordering the janitor could observe `enabled == true` alongside a stale
`polled == false` right after the GUI's first poll and disable monitoring for a
tick. Publish `enabled` with Release and load it with Acquire in the janitor so
observing `enabled == true` guarantees the paired `polled = true` is visible;
the `polled` accesses stay Relaxed (payload ordered by the enabled pair) and the
hot-path `record` load stays Relaxed.

Also switch the janitor to `interval_at(now + IDLE_TICK, IDLE_TICK)`: `interval`
fires immediately, so on an agent restart with monitoring enabled the first tick
would disable it before the reconnecting GUI repolls. Waiting a full idle window
lets the GUI poll first.
…g per render

`input_conflict_field` is a render fn that ran `Hook::list_event_taps` (two
`CGGetEventTapList` syscalls) on every repaint. With the live monitor open the
Diagnostics page repaints several times a second, so those syscalls fired
continuously.

In debug macOS builds, cache the tap snapshot in `AppState` and refresh it on the
same ~300ms tick that pushes monitor events; the render reads the cache. Release
builds keep the live call — that page renders only on interaction there, not on a
300ms monitor cadence, so per-render enumeration is cheap. The cache field and its
accessors are gated `cfg(all(target_os = "macos", debug_assertions))`, matching
`monitor_events`, so non-macOS builds are unaffected.
@AprilNEA
AprilNEA force-pushed the feat/cgevent-diagnostics branch from 210f278 to 79b511c Compare July 18, 2026 09:05
@AprilNEA
AprilNEA merged commit e6b8e70 into master Jul 18, 2026
12 checks passed
@AprilNEA
AprilNEA deleted the feat/cgevent-diagnostics branch July 18, 2026 09:14
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.

1 participant