Skip to content

fix: auto-recover input listeners after X server restart#78

Merged
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/listener-health-check
Mar 2, 2026
Merged

fix: auto-recover input listeners after X server restart#78
ErikBjare merged 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/listener-health-check

Conversation

@TimeToBuildBob

@TimeToBuildBob TimeToBuildBob commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add is_alive() method to KeyboardListener and MouseListener that checks if the underlying pynput listener thread is still running
  • Add health checking in LastInputUnix.seconds_since_last_input() — on every poll cycle, checks if listeners are alive and reinitializes them if dead
  • Reset last_activity timestamp on reinitialization to prevent reporting a spurious AFK gap

Problem

When the X server restarts (suspend/resume, display server crash, session switch), pynput listener threads silently die — they stop receiving events but don't raise any errors. The watcher then permanently reports AFK because has_new_event() always returns False.

This is a long-standing issue reported in #27 (2017).

Test plan

  • 7 unit tests added covering:
    • is_alive() returns False before start() is called
    • is_alive() correctly reflects underlying pynput thread state
    • LastInputUnix detects dead listeners and reinitializes them
    • Reinitialization creates fresh listener instances
  • Manual testing: kill X server while watcher is running, verify it auto-recovers

Fixes #27


Important

Fixes issue where input listeners die after X server restart by adding health checks and auto-recovery.

  • Behavior:
    • Adds is_alive() to KeyboardListener and MouseListener to check if pynput listener threads are running.
    • In LastInputUnix.seconds_since_last_input(), checks listener health and reinitializes if dead.
    • Resets last_activity timestamp on reinitialization to avoid false AFK reporting.
  • Tests:
    • Adds test_listener_health.py with tests for is_alive() behavior and listener reinitialization.
    • Tests cover scenarios before and after listener start, and after X server restart.
  • Misc:

This description was created by Ellipsis for 984a2ae. You can customize this summary. It will automatically update as commits are pushed.

@ellipsis-dev ellipsis-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

Looks good to me! 👍

Reviewed everything up to 984a2ae in 7 seconds. Click for details.
  • Reviewed 179 lines of code in 3 files
  • Skipped 0 files when reviewing.
  • Skipped posting 0 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.

Workflow ID: wflow_gRfg3AG7kcx36ho1

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

@greptile-apps

greptile-apps Bot commented Feb 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds auto-recovery for input listeners after X server restarts by implementing health checks and reinitialization logic. The implementation successfully addresses the long-standing issue where pynput listener threads silently die after X server restarts, causing permanent AFK reporting.

Key changes:

  • Adds is_alive() method to both KeyboardListener and MouseListener that checks underlying pynput thread state
  • Implements _check_listeners() in LastInputUnix that polls listener health on every cycle and reinitializes dead listeners
  • Resets last_activity timestamp on reinitialization to avoid false AFK gaps
  • Includes comprehensive test coverage with 7 unit tests

Issues found:

  • Old listener threads aren't explicitly stopped before reinitialization, which could lead to multiple listener instances running if only one dies (though rare since X server restarts typically kill both)

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk - addresses a real bug with a reasonable solution
  • Score reflects solid implementation with good test coverage, but resource leak concern with old listener threads prevents a perfect score
  • Pay close attention to aw_watcher_afk/unix.py - verify that old listener threads don't cause issues in production

Important Files Changed

Filename Overview
aw_watcher_afk/listeners.py Adds is_alive() method to check listener thread health, stores listener reference in _listener
aw_watcher_afk/unix.py Adds health checking and auto-recovery for dead listeners, but doesn't stop old threads before reinitializing
tests/test_listener_health.py Comprehensive tests for listener health checks and reinitialization behavior

Sequence Diagram

sequenceDiagram
    participant Watcher as AFK Watcher
    participant Unix as LastInputUnix
    participant ML as MouseListener
    participant KL as KeyboardListener
    participant Pynput as pynput threads

    Note over Unix,Pynput: Normal Operation
    Watcher->>Unix: seconds_since_last_input()
    Unix->>Unix: _check_listeners()
    Unix->>ML: is_alive()
    ML->>Pynput: check thread state
    Pynput-->>ML: alive
    ML-->>Unix: True
    Unix->>KL: is_alive()
    KL->>Pynput: check thread state
    Pynput-->>KL: alive
    KL-->>Unix: True
    Unix->>ML: has_new_event()
    Unix->>KL: has_new_event()
    Unix-->>Watcher: seconds since last input

    Note over Pynput: X Server Restarts
    Note over Pynput: Threads die silently

    Watcher->>Unix: seconds_since_last_input()
    Unix->>Unix: _check_listeners()
    Unix->>ML: is_alive()
    ML->>Pynput: check thread state
    Pynput-->>ML: dead
    ML-->>Unix: False
    Unix->>Unix: _start_listeners()
    Unix->>ML: new MouseListener()
    Unix->>ML: start()
    Unix->>KL: new KeyboardListener()
    Unix->>KL: start()
    Unix->>Unix: reset last_activity
    Unix-->>Watcher: 0 seconds (recovered)
Loading

Last reviewed commit: 984a2ae

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread aw_watcher_afk/unix.py
Comment thread tests/test_listener_health.py Outdated
@@ -0,0 +1,81 @@
"""Tests for listener health checking and auto-recovery."""

import threading

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

threading imported but never used

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 885de45 — removed the unused threading import.

@ErikBjare

Copy link
Copy Markdown
Member

@TimeToBuildBob Rebase and address review comments

When the X server restarts (e.g. after suspend/resume, display server
crash, or session switch), pynput listener threads silently die. Without
health checking, the watcher would permanently report AFK since no new
input events would ever be detected.

Add is_alive() to KeyboardListener and MouseListener, and check listener
health on every poll cycle. When a dead listener is detected, reinitialize
both listeners and reset the activity timestamp.

Fixes ActivityWatch#27
- Add stop() method to KeyboardListener and MouseListener
- Call _stop_listeners() in _check_listeners() before reinitializing
  to prevent two listener instances running simultaneously when only
  one dies (e.g. only keyboard thread dies after X server restart)
- Remove unused threading import from test file (Greptile review)
@TimeToBuildBob TimeToBuildBob force-pushed the fix/listener-health-check branch from 28e3942 to 885de45 Compare March 2, 2026 09:06
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

Rebased onto current master and fixed both Greptile review comments:

  • Listener stop before reinitializing: Added stop() method to KeyboardListener and MouseListener, and call _stop_listeners() before _start_listeners() in _check_listeners(). This prevents duplicate listener instances when only one listener dies.
  • Unused threading import: Removed from tests/test_listener_health.py.

All 7 tests still pass.

@ErikBjare ErikBjare merged commit 7c5b19f into ActivityWatch:master Mar 2, 2026
3 checks passed
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.

Keeps sending afk events if xserver is restarted

2 participants