Skip to content

Idle detection via inotify IN_ACCESS on /dev/input/ does not detect read() on character devices #121

@ncaq

Description

@ncaq

Summary

Clightd's idle detection module (src/modules/idle.c) uses inotify_add_watch() with IN_ACCESS on the /dev/input/ directory to detect user input activity. However, read() calls on character device files within /dev/input/ do not generate IN_ACCESS inotify events propagated to the parent directory watch. This causes the idle detection to never see user activity, resulting in the idle timeout always firing regardless of user input.

Environment

  • Kernel: Linux 6.12.78 (NixOS 25.11)
  • Filesystem: devtmpfs on /dev
  • Clightd version: 5.9
  • Clight version: 4.11

Observed behavior

With Clight's DPMS timeout set to 600 seconds (battery), the screen always goes blank after 600 seconds even during active keyboard/mouse use. Clight's gamma and dimmer modules are disabled, isolating the issue to DPMS/idle detection.

Reproduction

The following Python script demonstrates the problem. Run it while actively moving the mouse:

import ctypes, ctypes.util, struct, os, select, time, threading

IN_ALL_EVENTS = 0x00000FFF
event_names = {
    0x1: 'ACCESS', 0x2: 'MODIFY', 0x4: 'ATTRIB', 0x8: 'CLOSE_WRITE',
    0x10: 'CLOSE_NOWRITE', 0x20: 'OPEN',
}

libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
fd = libc.inotify_init()
wd = libc.inotify_add_watch(fd, b'/dev/input/', IN_ALL_EVENTS)

inotify_events = []

def watch_inotify(duration):
    start = time.time()
    while time.time() - start < duration:
        r, _, _ = select.select([fd], [], [], 0.1)
        if r:
            data = os.read(fd, 4096)
            offset = 0
            while offset < len(data):
                wd_ev, mask, cookie, name_len = struct.unpack_from('iIII', data, offset)
                name = data[offset+16:offset+16+name_len].rstrip(b'\x00').decode()
                evs = [n for bit, n in event_names.items() if mask & bit]
                inotify_events.append(f'{"&".join(evs)} on {name}')
                offset += 16 + name_len

watcher = threading.Thread(target=watch_inotify, args=(6,))
watcher.start()

mfd = os.open('/dev/input/mice', os.O_RDONLY)
print('Move your mouse for 5 seconds...')
reads_ok = 0
start = time.time()
while time.time() - start < 5:
    r, _, _ = select.select([mfd], [], [], 1.0)
    if r:
        os.read(mfd, 256)
        reads_ok += 1
        if reads_ok > 20:
            break

os.close(mfd)
watcher.join()
print(f'Successful reads: {reads_ok}')
print(f'Inotify events: {len(inotify_events)}')
for e in inotify_events:
    print(f'  {e}')

Result

Move your mouse for 5 seconds...
Successful reads: 21
Inotify events: 2
  OPEN on mice
  CLOSE_NOWRITE on mice

21 successful read() calls on /dev/input/mice, but zero IN_ACCESS events. Only OPEN and CLOSE_NOWRITE (from open()/close()) are propagated to the directory watch.

Analysis

In src/modules/idle.c line 257:

inot_wd = inotify_add_watch(inot_fd, "/dev/input/", IN_ACCESS);

This relies on IN_ACCESS events being propagated from child device files to the parent directory watcher via __fsnotify_parent(). While this propagation works for OPEN/CLOSE events, it does not work for ACCESS events on character device reads under devtmpfs (at least on kernel 6.12). A similar issue has been reported in fsnotify/fsnotify#182.

As a result, last_input (line 40, 97) is never updated, and every idle client always times out regardless of user activity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions