Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,16 @@ Three action modes are available, configured through ``--action-mode``:
Input
-----

inputexec can read from stdin, from a file or from a character device.
inputexec can read from stdin, from a file, from a character device or a directory with character devices.

For stdin, simply pass ``--source-file=-``

If another file path is provided, inputexec will look at its type and,
if the file is a device node with major 13 (i.e an input device on linux),
if the file is a device node with major 13 (i.e an input device on Linux),
use the ``evdev`` reader.
A linux input device can be opened either in ``shared`` mode
If provided path is in fact a directory, inputexec will monitor it for created / deleted input devices and
use the ``evdev`` reader for each device.
A Linux input device can be opened either in ``shared`` mode
(events are propagated to all other readers) or in ``exclusive`` mode;
this behaviour is controlled by the ``--source-mode=exclusive|shared`` flag.

Expand Down
52 changes: 27 additions & 25 deletions inputexec/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,31 +123,33 @@ def setup_logging(self, args):

def make_reader(self, args):
src = args.source_file
if src == '-':
evdev = False
else:
evdev = self._is_evdev(src)

if evdev:
try:
from .readers import evdev as evdev_readers
except ImportError:
logger.error("Unable to import python-evdev, but targeting a /dev/input device.")
raise

event_filter = evdev_readers.Filter(args.filter_kinds.split(','))
exclusive = args.source_mode == 'exclusive'
evdev_device = evdev_readers.open_device(src)
return evdev_readers.EvdevReader(evdev_device,
exclusive=exclusive,
filter=event_filter,
)

else:
return line_readers.LineReader(src,
pattern=args.format_pattern,
end_line=unescape(args.format_endline),
)
if src != '-':
is_dir = os.path.isdir(src)
if is_dir or self._is_evdev(src):
try:
from .readers import evdev as evdev_readers
except ImportError:
logger.error("Unable to import python-evdev, but targeting /dev/input device(s).")
raise

event_filter = evdev_readers.Filter(args.filter_kinds.split(','))
exclusive = args.source_mode == 'exclusive'

if is_dir:
return evdev_readers.EvdevDirReader(src,
exclusive=exclusive,
filter=event_filter,
)
else:
return evdev_readers.EvdevReader(src,
exclusive=exclusive,
filter=event_filter,
)

return line_readers.LineReader(src,
pattern=args.format_pattern,
end_line=unescape(args.format_endline),
)

def make_executor(self, args):
if args.action_mode in ('run_sync', 'run_async'):
Expand Down
2 changes: 1 addition & 1 deletion inputexec/executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class AsyncExecutor(BaseCommandExecutor):
def __init__(self, jobs=1, **kwargs):
super(AsyncExecutor, self).__init__(**kwargs)
self.nb_jobs = jobs
self.queue = Queue.Queue()
self.queue = queue.Queue()
self.stopped = threading.Event()

def setup(self):
Expand Down
147 changes: 122 additions & 25 deletions inputexec/readers/evdev.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@

import evdev
import logging
import select

from .. import events
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

from . import base

from .. import events

logger = logging.getLogger(__name__)

Expand All @@ -30,7 +32,18 @@ class UnhandledEvent(Exception):
EVENT_ABSMOVE = 'absmove'


def map_event(evdev_event):
class Filter(object):
"""Filters events."""
def __init__(self, kinds=(EVENT_KEYPRESS,), **kwargs):
super(Filter, self).__init__(**kwargs)
self.kinds = kinds

def should_send(self, event):
"""Whether an Event should be handled."""
return event.kind in self.kinds


def _map_event(evdev_event):

code = evdev_event.code

Expand Down Expand Up @@ -58,7 +71,7 @@ def map_event(evdev_event):

symbol = evdev.events.keys[evdev_event.code]
if isinstance(symbol, list):
# More than on symbol for that code
# More than one symbol for that code
symbol = symbol[0]

else:
Expand All @@ -67,18 +80,7 @@ def map_event(evdev_event):
return events.Event(kind, code, symbol, evdev_event.value)


class Filter(object):
"""Filters events."""
def __init__(self, kinds=(EVENT_KEYPRESS,), **kwargs):
super(Filter, self).__init__(**kwargs)
self.kinds = kinds

def should_send(self, event):
"""Whether an Event should be handled."""
return event.kind in self.kinds


def open_device(path):
def _open_device(path):
device = evdev.InputDevice(path)
logger.info("Opened device %s (%s)", device.fn, device.name)
return device
Expand All @@ -89,8 +91,8 @@ class EvdevReader(base.BaseReader):

Handles:
- Reading lines
- exclusive device access
- Conversion into Event.
- Exclusive device access
- Conversion into Event

Attributes:
device (evdev.InputDevice): the device to read from
Expand All @@ -99,22 +101,22 @@ class EvdevReader(base.BaseReader):
reading
"""

def __init__(self, evdev_device, filter=None, exclusive=True, **kwargs):
def __init__(self, device_path, filter=None, exclusive=True, **kwargs):
super(EvdevReader, self).__init__(**kwargs)
self.device = evdev_device
self.device = _open_device(device_path)
self.filter = filter
self.exclusive = exclusive

def setup(self):
super(EvdevReader, self).setup()
if self.exclusive:
logger.info("Grapping exclusive use of %s", self.device)
logger.info("Grabbing exclusive use of %s", self.device)
self.device.grab()

def convert_event(self, event):
"""Try to convert a evdev.events.InputEvent into an Event."""
"""Try to convert an evdev.events.InputEvent into an Event."""
try:
return map_event(event)
return _map_event(event)
except UnhandledEvent:
logger.debug("Skipping unhandled event %s", event, exc_info=True)
return None
Expand All @@ -131,6 +133,101 @@ def read(self):
yield event

def cleanup(self):
if self.exclusive:
self.device.ungrab()
# closing the device also ungrabs it
self.device.close()
super(EvdevReader, self).cleanup()


class EvdevDirReader(base.BaseReader):
"""Low-level evdev reader that captures events from devices
residing in a directory.

Handles:
- Reading lines
- Subscriptions to devices
- Exclusive device access
- Conversion into Event

Attributes:
devices (dict of str: evdev.InputDevice): devices to read from
dir_path (str): path to the directory
filter (Filter): helper to filter events at the sources
exclusive (bool): whether to grab exclusive hold of devices while
reading
"""

class EventHandler(FileSystemEventHandler):
def __init__(self, reader):
self.reader = reader

def on_created(self, event):
self.reader.register_device(event.src_path)

def on_deleted(self, event):
self.reader.unregister_device(event.src_path)

def __init__(self, dir_path, filter=None, exclusive=True, **kwargs):
super(EvdevDirReader, self).__init__(**kwargs)
self.devices = {}
self.dir_path = dir_path
self.exclusive = exclusive
self.filter = filter
self._observer = Observer()
self._observer.schedule(
EvdevDirReader.EventHandler(self), self.dir_path
)

def setup(self):
super(EvdevDirReader, self).setup()
for device in evdev.util.list_devices(self.dir_path):
self.register_device(device)
self._observer.start()

def convert_event(self, event):
"""Try to convert an evdev.events.InputEvent into an Event."""
try:
return _map_event(event)
except UnhandledEvent:
logger.debug("Skipping unhandled event %s", event, exc_info=True)
return None

def read(self):
"""Read data from the evdev InputDevices.

Yields:
evdev.events.InputEvent
"""
while True:
# wait for events 5s max to detect changes in the devices map
rlist, _, _ = select.select(
list(self.devices.values()), [], [], 5
)
for device in rlist:
# check the device is still valid in case select
# exited early, e.g. when the device was deleted
if evdev.util.is_device(device.path):
for evdev_evt in device.read():
evt = self.convert_event(evdev_evt)
if evt is not None and self.filter.should_send(evt):
yield evt

def cleanup(self):
self._observer.stop()
for device_path in list(self.devices.keys()):
self.unregister_device(device_path)
super(EvdevDirReader, self).cleanup()

def register_device(self, device_path):
if evdev.util.is_device(device_path):
logger.debug("Registering device at %s", device_path)
device = _open_device(device_path)
if self.exclusive:
device.grab()
self.devices[device_path] = device

def unregister_device(self, device_path):
device = self.devices.pop(device_path, None)
if device:
logger.debug("Unregistering device at %s", device_path)
# closing the device also ungrabs it
device.close()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def get_version(package_name):
],
install_requires=[
'evdev',
'watchdog',
],
classifiers=[
"Development Status :: 5 - Production/Stable",
Expand Down