lordofire
lordofire

Reputation: 155

python script to detect hot-plug event

I'm trying to use python to detect mouse and keyboard event, and tolerant the hot-plug action during the detection. I write this script to automatically detect the keyboard and mouse plug-ins in run-time and output all the keyboard and mouse events. I use evdev and pyudev packages to realize this function. I have my scripts mostly working, including keyboard and mouse event detection and plug-in detection. However, whenever I plug-out the mouse, many weird things happen and my script could not work properly. I have several confusions here.

(1) Whenever the mouse is plugged into the system, there are two files generated in /dev/input/ folder, including ./mouseX and ./eventX. I try to cat to see the output from both source and there are indeed differences, but I do not understand why linux will have ./mouseX even if ./eventX already exists?

(2) Whenever I unplug my mouse, the ./mouseX unplug event comes first, which I did not use in evdev, and this leads to the failure of the script because ./eventX(where I read the data in the script) is unplugged simultaneously but I could only detect ./eventX in the next round. I use a trick(variable i in my script) to bypass this issue, but even though I could successfully delete the mouse device, the select.select() begins endless input reading even though I did not type anything to the keyboard.

The script is listed below(modified based on answers from previous post), thanks beforehand for your attention!

#!/usr/bin/env python

import pyudev
from evdev import InputDevice, list_devices, categorize
from select import select

context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem='input')
monitor.start()

devices = map(InputDevice, list_devices())
dev_paths = []
finalizers = []

for dev in devices:
    if "keyboard" in dev.name.lower():
        dev_paths.append(dev.fn)
    elif "mouse" in dev.name.lower():
        dev_paths.append(dev.fn)

devices = map(InputDevice, dev_paths)
devices = {dev.fd : dev for dev in devices}
devices[monitor.fileno()] = monitor
count = 1

while True:
    r, w, x = select(devices, [], [])

    if monitor.fileno() in r:
        r.remove(monitor.fileno())

        for udev in iter(functools.partial(monitor.poll, 0), None):
            # we're only interested in devices that have a device node
            # (e.g. /dev/input/eventX)
            if not udev.device_node:
                break

            # find the device we're interested in and add it to fds
            for name in (i['NAME'] for i in udev.ancestors if 'NAME' in i):
                # I used a virtual input device for this test - you
                # should adapt this to your needs
                if 'mouse' in name.lower() and 'event' in udev.device_node:
                    if udev.action == 'add':
                        print('Device added: %s' % udev)
                        dev = InputDevice(udev.device_node)
                        devices[dev.fd] = dev
                        break
                    if udev.action == 'remove':
                        print('Device removed: %s' % udev)
                        finalizers.append(udev.device_node)
                        break


    for path in finalizers:
        for dev in devices.keys():
            if dev != monitor.fileno() and devices[dev].fn == path:
                print "delete the device from list"
                del devices[dev]

    for i in r:
        if i in devices.keys() and count != 0:
            count = -1
            for event in devices[i].read():
                count = count + 1
                print(categorize(event))

Upvotes: 2

Views: 2470

Answers (1)

Edward Z. Yang
Edward Z. Yang

Reputation: 26762

The difference between mouseX and eventX is, generally speaking, is eventX is the evdev device, whereas mouseX is the "traditional" device (which, for example, doesn't support various evdev ioctls.)

I don't know what's wrong with the code you posted, but here is a code snippet which does the right thing.

#!/usr/bin/env python

import pyudev
import evdev
import select
import sys
import functools
import errno

context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem='input')
# NB: Start monitoring BEFORE we query evdev initially, so that if
# there is a plugin after we evdev.list_devices() we'll pick it up
monitor.start()

# Modify this predicate function for whatever you want to match against
def pred(d):
    return "keyboard" in d.name.lower() or "mouse" in d.name.lower()

# Populate the "active devices" map, mapping from /dev/input/eventXX to
# InputDevice
devices = {}
for d in map(evdev.InputDevice, evdev.list_devices()):
    if pred(d):
        print d
        devices[d.fn] = d

# "Special" monitor device
devices['monitor'] = monitor

while True:
    rs, _, _ = select.select(devices.values(), [], [])
    # Unconditionally ping monitor; if this is spurious this
    # will no-op because we pass a zero timeout.  Note that
    # it takes some time for udev events to get to us.
    for udev in iter(functools.partial(monitor.poll, 0), None):
        if not udev.device_node: break
        if udev.action == 'add':
            if udev.device_node not in devices:
                print "Device added: %s" % udev
                try:
                    devices[udev.device_node] = evdev.InputDevice(udev.device_node)
                except IOError, e:
                    # udev reports MORE devices than are accessible from
                    # evdev; a simple way to check is see if the devinfo
                    # ioctl fails
                    if e.errno != errno.ENOTTY: raise
                    pass
        elif udev.action == 'remove':
            # NB: This code path isn't exercised very frequently,
            # because select() will trigger a read immediately when file
            # descriptor goes away, whereas the udev event takes some
            # time to propagate to us.
            if udev.device_node in devices:
                print "Device removed (udev): %s" % devices[udev.device_node]
                del devices[udev.device_node]
    for r in rs:
        # You can't read from a monitor
        if r.fileno() == monitor.fileno(): continue
        if r.fn not in devices: continue
        # Select will immediately return an fd for read if it will
        # ENODEV.  So be sure to handle that.
        try:
            for event in r.read():
                pass
                print evdev.categorize(event)
        except IOError, e:
            if e.errno != errno.ENODEV: raise
            print "Device removed: %s" % r
            del devices[r.fn]

Upvotes: 2

Related Questions