diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk index 44fe8ab0..7d37551b 100755 --- a/bin/key-mapper-gtk +++ b/bin/key-mapper-gtk @@ -56,8 +56,6 @@ if __name__ == '__main__': update_verbosity(options.debug) log_info() - can_read_devices() - window = Window() def stop(): diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py index d12205f4..f6650b12 100644 --- a/keymapper/dev/injector.py +++ b/keymapper/dev/injector.py @@ -23,6 +23,7 @@ import re +import os import asyncio import time import subprocess diff --git a/keymapper/dev/permissions.py b/keymapper/dev/permissions.py index 9dc03c67..59440af2 100644 --- a/keymapper/dev/permissions.py +++ b/keymapper/dev/permissions.py @@ -24,6 +24,7 @@ import grp import getpass +import os from keymapper.logger import logger from keymapper.paths import USER @@ -35,6 +36,9 @@ def can_read_devices(): is_in_input_group = USER in grp.getgrnam('input').gr_mem is_in_plugdev_group = USER in grp.getgrnam('plugdev').gr_mem + # ubuntu. funnily, individual devices in /dev/input/ have write permitted. + can_write = os.access('/dev/uinput', os.W_OK) + def warn(group): logger.warning( 'Some devices may not be visible without being in the ' @@ -49,7 +53,12 @@ def can_read_devices(): warn('plugdev') if not is_in_input_group: warn('input') + if not can_write: + logger.warning( + 'Injecting keycodes into /dev/uinput is not permitted. ' + 'Either use sudo or run `sudo chmod 660 /dev/uinput`' + ) - ok = is_root or (is_in_input_group and is_in_plugdev_group) + ok = (is_root or (is_in_input_group and is_in_plugdev_group)) and can_write - return ok, is_root, is_in_input_group, is_in_plugdev_group + return ok, is_root, is_in_input_group, is_in_plugdev_group, can_write diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 6932f33f..168ff381 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -114,7 +114,7 @@ class Window: # already visible (without content) to make it look more responsive. gtk_iteration() - ok, _, is_input, is_plugdev = can_read_devices() + ok, _, is_input, is_plugdev, can_write = can_read_devices() if not ok: missing_groups = [] if not is_input: @@ -128,6 +128,11 @@ class Window: f'You are not in the {" and ".join(missing_groups)} ' f'group{"s" if len(missing_groups) > 0 else ""}' ) + elif not can_write: + self.get('status_bar').push( + CTX_ERROR, + f'Insufficient permissions on /dev/uinput' + ) self.populate_devices() diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index 52897806..0b7a2dc5 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -21,6 +21,7 @@ import sys import time +import grp import os import unittest import evdev @@ -36,7 +37,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk from keymapper.state import custom_mapping, system_mapping -from keymapper.paths import CONFIG, get_config_path +from keymapper.paths import CONFIG, get_config_path, USER from keymapper.config import config from keymapper.dev.reader import keycode_reader from keymapper.gtk.row import to_string @@ -51,8 +52,21 @@ def gtk_iteration(): Gtk.main_iteration() +# iterate a few times when Gtk.main() is called, but don't block +# there and just continue to the tests while the UI becomes +# unresponsive +Gtk.main = gtk_iteration + +# doesn't do much except avoid some Gtk assertion error, whatever: +Gtk.main_quit = lambda: None + + def launch(argv=None): """Start key-mapper-gtk with the command line argument array argv.""" + if os.path.exists(tmp): + shutil.rmtree(tmp) + custom_mapping.empty() + bin_path = os.path.join(os.getcwd(), 'bin', 'key-mapper-gtk') if not argv: @@ -85,20 +99,7 @@ class TestIntegration(unittest.TestCase): Try to modify the configuration only by calling functions of the window. """ - @classmethod - def setUpClass(cls): - # iterate a few times when Gtk.main() is called, but don't block - # there and just continue to the tests while the UI becomes - # unresponsive - Gtk.main = gtk_iteration - - # doesn't do much except avoid some Gtk assertion error, whatever: - Gtk.main_quit = lambda: None - def setUp(self): - if os.path.exists(tmp): - shutil.rmtree(tmp) - custom_mapping.empty() self.window = launch() def tearDown(self): @@ -502,5 +503,76 @@ class TestIntegration(unittest.TestCase): self.assertEqual(len(write_history), len_before) +original_access = os.access +original_getgrnam = grp.getgrnam + + +class TestPermissions(unittest.TestCase): + def tearDown(self): + os.access = original_access + os.getgrnam = original_getgrnam + + self.window.on_close() + self.window.window.destroy() + gtk_iteration() + shutil.rmtree('/tmp/key-mapper-test') + + def test_check_groups_missing(self): + class Grnam: + def __init__(self, group): + self.gr_mem = [] + + grp.getgrnam = Grnam + + self.window = launch() + status = self.window.get('status_bar') + + labels = '' + for label in status.get_message_area(): + labels += label.get_text() + self.assertIn('input', labels) + self.assertIn('plugdev', labels) + + def test_check_plugdev_missing(self): + class Grnam: + def __init__(self, group): + if group == 'input': + self.gr_mem = [USER] + else: + self.gr_mem = [] + + grp.getgrnam = Grnam + + self.window = launch() + status = self.window.get('status_bar') + + labels = '' + for label in status.get_message_area(): + labels += label.get_text() + self.assertNotIn('input', labels) + self.assertIn('plugdev', labels) + + def test_check_write_uinput(self): + class Grnam: + def __init__(self, group): + self.gr_mem = [USER] + + grp.getgrnam = Grnam + + def access(path, mode): + return False + + os.access = access + + self.window = launch() + status = self.window.get('status_bar') + + labels = '' + for label in status.get_message_area(): + labels += label.get_text() + self.assertNotIn('plugdev', labels) + self.assertIn('Insufficient permissions on /dev/uinput', labels) + + if __name__ == "__main__": unittest.main()