diff --git a/lib/hidapi/udev.py b/lib/hidapi/udev.py index bcdd80cd..aa815bf1 100644 --- a/lib/hidapi/udev.py +++ b/lib/hidapi/udev.py @@ -114,53 +114,45 @@ def _match(action, device, vendor_id=None, product_id=None, interface_number=Non return d_info -def monitor_async(callback, *device_filters): - from threading import Thread as _Thread - t = _Thread(name='monitor_async_' + callback.__name__, - target=monitor, - args=[callback] + list(device_filters)) - t.daemon = True - t.start() +def monitor_glib(callback, *device_filters): + from gi.repository import GLib + c = _Context() -def monitor(callback, *device_filters): - def _monitor(): - c = _Context() + # already existing devices + for device in c.list_devices(subsystem='hidraw'): + # print (device, dict(device), dict(device.attributes)) + for filter in device_filters: + d_info = _match('add', device, *filter) + if d_info: + GLib.idle_add(callback, 'add', d_info) + break - for device in c.list_devices(subsystem='hidraw'): - # print (device, dict(device), dict(device.attributes)) - for filter in device_filters: - d_info = _match('add', device, *filter) - if d_info: - callback('add', d_info) - break + m = _Monitor.from_netlink(c) + m.filter_by(subsystem='hidraw') - m = _Monitor.from_netlink(c) - del c - - m.filter_by(subsystem='hidraw') - try: - for action, device in m: - # print ('----', action, device) - if action in ('add', 'remove'): - for filter in device_filters: - d_info = _match(action, device, *filter) + def _process_udev_event(monitor, condition, cb, filters): + if condition == GLib.IO_IN: + event = monitor.receive_device() + if event: + action, device = event + # print ("udev action:", action, device) + if action == 'add': + for filter in filters: + d_info = _match('add', device, *filter) if d_info: - callback(action, d_info) + GLib.idle_add(cb, 'add', d_info) break - finally: - del m + elif action == 'remove': + for filter in filters: + d_info = _match('remove', device, *filter) + if d_info: + GLib.idle_add(cb, 'remove', d_info) + break + return True + GLib.io_add_watch(m, GLib.PRIORITY_DEFAULT, GLib.IO_IN, _process_udev_event, callback, device_filters) - while True: - try: - _monitor() - except IOError as e: - print ("monitor IOError", e) - if e.errno == _errno.EINTR: - # raised when the computer wakes from sleep - # in this case, just restart the monitor - continue - raise + m.start() def enumerate(vendor_id=None, product_id=None, interface_number=None, hid_driver=None): diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index 57fc6339..52350f53 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -85,15 +85,18 @@ def receivers(): # yield d -def notify_on_receivers(callback): - """Starts a thread that monitors receiver events from udev.""" - _hid.monitor_async(callback, +def notify_on_receivers_glib(callback): + """Watch for matching devices and notifies the callback on the GLib thread.""" + _hid.monitor_glib(callback, DEVICE_UNIFYING_RECEIVER, DEVICE_UNIFYING_RECEIVER_2, DEVICE_NANO_RECEIVER, # DEVICE_VXNANO_RECEIVER, ) +# +# +# def open_path(path): """Checks if the given Linux device path points to the right UR device. diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py index 21dba863..c34661f3 100644 --- a/lib/solaar/gtk.py +++ b/lib/solaar/gtk.py @@ -51,48 +51,7 @@ def _run(args): status_icon = ui.status_icon.create(ui.main_window.toggle_all, ui.main_window.popup) assert status_icon - listeners = {} - from solaar.listener import ReceiverListener - - def handle_receivers_events(action, device_info): - assert action is not None - assert device_info is not None - - _log.info("receiver event %s: %s", action, device_info) - - # whatever the action, stop any previous receivers at this path - l = listeners.pop(device_info.path, None) - if l is not None: - assert isinstance(l, ReceiverListener) - l.stop() - - if action == 'add': - # a new receiver device was detected - try: - l = ReceiverListener.open(device_info, status_changed) - if l is not None: - listeners[device_info.path] = l - except OSError: - # permission error, blacklist this path for now - listeners.pop(device_info.path, None) - GLib.idle_add(ui.error_dialog, 'Permissions error', - 'Found a Logitech Receiver, but did not have permission to open it.\n' - '\n' - 'If you\'ve just installed Solaar, try removing the receiver\n' - 'and plugging it back in.') - - # elif action == 'remove': - # # we'll be receiving remove events for any hidraw devices, - # # not just Logitech receivers, so it's okay if the device is not - # # already in our listeners map - # l = listeners.pop(device.path, None) - # if l is not None: - # l.stop() - - # print ("****", action, device, listeners) - # callback delivering status notifications from the receiver/devices to the UI - from gi.repository import GLib from logitech.unifying_receiver.status import ALERT def status_changed(device, alert=ALERT.NONE, reason=None): assert device is not None @@ -109,28 +68,15 @@ def _run(args): if alert & ALERT.NOTIFICATION: ui.async(ui.notify.show, device, reason) - # ugly... - def _startup_check_receiver(): - if not listeners: - # this is called on the Main (GTK) thread, so we can make direct calls - ui.notify.alert('No receiver found.') - ui.status_icon.update(status_icon) - ui.status_icon.attention(status_icon, 'No receiver found.') - # check for a receiver 1 second after the app was started - GLib.timeout_add(1000, _startup_check_receiver) + import solaar.listener as listener + listener.start_scanner(status_changed, ui.error_dialog) - from logitech.unifying_receiver import base as _base - # receiver add/remove events will start/stop listener threads - GLib.timeout_add(10, _base.notify_on_receivers, handle_receivers_events) # main UI event loop ui.run_loop() ui.status_icon.destroy(status_icon) - - for l in listeners.values(): - l.stop() ui.notify.uninit() - for l in listeners.values(): - l.join() + + listener.stop_all() def main(): diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index 2cc6c8a1..32e8d3a6 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -169,13 +169,69 @@ class ReceiverListener(_listener.EventsListener): return '' % (self.receiver.path, self.receiver.handle) __unicode__ = __str__ - @classmethod - def open(self, device_info, status_changed_callback): - assert status_changed_callback - receiver = Receiver.open(device_info) - if receiver: - rl = ReceiverListener(receiver, status_changed_callback) - rl.start() - return rl - else: - _log.warn("failed to open %s", device_info) +# +# +# + +# all currently running receiver listeners +_all_listeners = {} + + +def start(device_info, status_changed_callback): + assert status_changed_callback + receiver = Receiver.open(device_info) + if receiver: + rl = ReceiverListener(receiver, status_changed_callback) + rl.start() + _all_listeners[device_info.path] = rl + return rl + else: + _log.warn("failed to open %s", device_info) + + +def stop_all(): + listeners = list(_all_listeners.values()) + _all_listeners.clear() + + for l in listeners: + l.stop() + for l in listeners: + l.join() + + +_status_callback = None +_error_callback = None + +def start_scanner(status_changed_callback, error_callback): + global _status_callback, _error_callback + if _status_callback: + raise Exception("scanner was already set-up") + + _status_callback = status_changed_callback + _error_callback = error_callback + + from logitech.unifying_receiver import base as _base + _base.notify_on_receivers_glib(_process_receiver_event) + + +# receiver add/remove events will start/stop listener threads +def _process_receiver_event(action, device_info): + assert action is not None + assert device_info is not None + + _log.info("receiver event %s %s", action, device_info) + + # whatever the action, stop any previous receivers at this path + l = _all_listeners.pop(device_info.path, None) + if l is not None: + assert isinstance(l, ReceiverListener) + l.stop() + + if action == 'add': + # a new receiver device was detected + try: + l = start(device_info, _status_callback) + except OSError: + # permission error, ignore this path for now + _all_listeners.pop(device_info.path, None) + _error_callback('permissions', device_info.path) diff --git a/lib/solaar/ui/__init__.py b/lib/solaar/ui/__init__.py index 495f07a4..8af23128 100644 --- a/lib/solaar/ui/__init__.py +++ b/lib/solaar/ui/__init__.py @@ -11,12 +11,23 @@ GLib.threads_init() async = GLib.idle_add run_loop = Gtk.main +def error_dialog(reason, object): + if reason == 'permission': + title = 'Permissions error' + text = ('Found a Logitech Receiver (%s), but did not have permission to open it.\n' + '\n' + 'If you\'ve just installed Solaar, try removing the receiver\n' + 'and plugging it back in.' % object) + else: + raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object) + + def _show_dialog(d): + d.run() + d.destroy() -def error_dialog(title, text): m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text) m.set_title(title) - m.run() - m.destroy() + async(_show_dialog, m) # #