From 54469e94c7d504be10cf00bc6a9cca7b22cf77f2 Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Mon, 26 Aug 2024 17:52:34 +0200 Subject: [PATCH 1/6] Add missing support to localed for compositor The plan is to use systemd-localed to control compositor keyboard and replace current `gk_keyboard_manager`. Most of the code is in place already but we need to add a few missing ones. The most problematic is missing support for next layout. The issue is that localed service don't have support for selection, to resolve this issue we will set the first in the list as selected. However, it means that we have to keep what user has set from Anaconda so we can find next candidate when switch to next layout is requested. Keep the information when user set the layouts to the compositor. (cherry picked from commit d9ed3f7946395b0e024bd532050246e808cf14ba) Related: RHEL-58181 --- pyanaconda/modules/localization/localed.py | 104 +++++++- .../localization/test_localed_wrapper.py | 223 ++++++++++++++++++ 2 files changed, 324 insertions(+), 3 deletions(-) diff --git a/pyanaconda/modules/localization/localed.py b/pyanaconda/modules/localization/localed.py index 10b4f95081a..3c25c68c319 100644 --- a/pyanaconda/modules/localization/localed.py +++ b/pyanaconda/modules/localization/localed.py @@ -30,6 +30,7 @@ class LocaledWrapper(object): def __init__(self): self._localed_proxy = None + self._user_layouts_variants = [] if not conf.system.provides_system_bus: log.debug("Not using localed service: " @@ -77,6 +78,15 @@ def layouts_variants(self): return [join_layout_variant(layout, variant) for layout, variant in zip(layouts, variants)] + @property + def current_layout_variant(self): + """Get first (current) layout with variant. + + :return: a list of "layout (variant)" or "layout" layout specifications + :rtype: list(str) + """ + return "" if not self.layouts_variants else self.layouts_variants[0] + @property def options(self): """Get current X11 options. @@ -125,7 +135,7 @@ def convert_keymap(self, keymap): orig_layouts_variants = self.layouts_variants orig_keymap = self.keymap converted_layouts = self.set_and_convert_keymap(keymap) - self.set_layouts(orig_layouts_variants) + self._set_layouts(orig_layouts_variants) self.set_keymap(orig_keymap) return converted_layouts @@ -155,6 +165,12 @@ def set_layouts(self, layouts_variants, options=None, convert=False): (see set_and_convert_layouts) :type convert: bool """ + # store configuration from user + self._set_layouts(layouts_variants, options, convert) + log.debug("Storing layouts for compositor configured by user") + self._user_layouts_variants = layouts_variants + + def _set_layouts(self, layouts_variants, options=None, convert=False): if not self._localed_proxy: return @@ -162,6 +178,9 @@ def set_layouts(self, layouts_variants, options=None, convert=False): variants = [] parsing_failed = False + log.debug("Setting system/compositor keyboard layouts: '%s' options: '%s' convert: '%s", + layouts_variants, options, convert) + for layout_variant in (nonempty for nonempty in layouts_variants if nonempty): try: (layout, variant) = parse_layout_variant(layout_variant) @@ -198,7 +217,7 @@ def set_and_convert_layouts(self, layouts_variants): :rtype: str """ - self.set_layouts(layouts_variants, convert=True) + self._set_layouts(layouts_variants, convert=True) return self.keymap @@ -223,7 +242,86 @@ def convert_layouts(self, layouts_variants): orig_layouts_variants = self.layouts_variants orig_keymap = self.keymap ret = self.set_and_convert_layouts(layouts_variants) - self.set_layouts(orig_layouts_variants) + self._set_layouts(orig_layouts_variants) self.set_keymap(orig_keymap) return ret + + # TODO: rename to select_layout + def set_current_layout(self, layout_variant): + """Set given layout as first (current) layout for compositor. + + This will search for the given layout variant in the list and move it as first in the list. + + :param layout_variant: The layout to set, with format "layout (variant)" + (e.g. "cz (qwerty)") + :type layout_variant: str + :return: If the keyboard layout was activated + :rtype: bool + """ + # ignore compositor layouts but force Anaconda configuration + layouts = self._user_layouts_variants + + try: + new_layouts = self._shift_list(layouts, layout_variant) + self._set_layouts(new_layouts) + return True + except ValueError: + log.warning("Can't set layout: '%s' as first to the current set: %s", + layout_variant, layouts) + return False + + @staticmethod + def _shift_list(source_layouts, value_to_first): + """Helper method to reorder list of layouts and move one as first in the list. + + We should preserve the ordering just shift items from start of the list to the + end in the same order. + + When we want to set 2nd as first in this list: + ["cz", "es", "us"] + The result should be: + ["es", "us", "cz"] + + So the compositor has the same next layout as Anaconda. + + :raises: ValueError: if the list is small or the layout is not inside + """ + value_id = source_layouts.index(value_to_first) + new_list = source_layouts[value_id:len(source_layouts)] + source_layouts[0:value_id] + return new_list + + def select_next_layout(self): + """Select (make it first) next layout for compositor. + + Find current compositor layout in the list of defined layouts and set next to it as + current (first) for compositor. We need to have user defined list because compositor + layouts will change with the selection. Store this list when user is setting configuration + to compositor. This list must not change ordering. + + :param user_layouts: List of layouts selected by user in Anaconda. + :type user_layouts: [str] + :return: If switch was successful True otherwise False + :rtype: bool + """ + current_layout = self.current_layout_variant + layout_id = 0 + + if not self._user_layouts_variants: + log.error("Can't switch next layout - user defined keyboard layout is not present!") + return False + + # find next layout + for i, v in enumerate(self._user_layouts_variants): + if v == current_layout: + layout_id = i + 1 + layout_id %= len(self._user_layouts_variants) + + try: + new_layouts = self._shift_list(self._user_layouts_variants, + self._user_layouts_variants[layout_id]) + self._set_layouts(new_layouts) + return True + except ValueError: + log.warning("Can't set next keyboard layout %s", self._user_layouts_variants) + return False diff --git a/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py b/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py index 48b060a5f61..8f8471db173 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py +++ b/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py @@ -66,6 +66,7 @@ def test_localed_wrapper_properties(self, mocked_conf, mocked_localed_service, "cz" assert localed_wrapper.layouts_variants == \ ["cz (qwerty)", "fi", "us (euro)", "fr"] + assert localed_wrapper.current_layout_variant == "cz (qwerty)" assert localed_wrapper.options == \ ["grp:alt_shift_toggle", "grp:ctrl_alt_toggle"] @@ -76,6 +77,7 @@ def test_localed_wrapper_properties(self, mocked_conf, mocked_localed_service, assert localed_wrapper.keymap == "" assert localed_wrapper.options == [] assert localed_wrapper.layouts_variants == [] + assert localed_wrapper.current_layout_variant == "" @patch("pyanaconda.modules.localization.localed.SystemBus") @patch("pyanaconda.modules.localization.localed.LOCALED") @@ -114,6 +116,16 @@ def test_localed_wrapper_safe_calls(self, mocked_conf, mocked_localed_service, localed_wrapper.set_and_convert_layouts(["us-altgr-intl"]) localed_wrapper.convert_layouts(["us-altgr-intl"]) + # verify that user defined list doesn't change + localed_wrapper._user_layouts_variants = [] + localed_wrapper.set_keymap("cz") + localed_wrapper.convert_keymap("cz") + localed_wrapper.set_and_convert_keymap("cz") + assert localed_wrapper._user_layouts_variants == [] + # only set_layouts should change user defined layouts + localed_wrapper.set_layouts(["cz", "us (euro)"]) + assert localed_wrapper._user_layouts_variants == ["cz", "us (euro)"] + @patch("pyanaconda.modules.localization.localed.SystemBus") def test_localed_wrapper_no_systembus(self, mocked_system_bus): """Test LocaledWrapper in environment without system bus. @@ -124,3 +136,214 @@ def test_localed_wrapper_no_systembus(self, mocked_system_bus): mocked_system_bus.check_connection.return_value = False localed_wrapper = LocaledWrapper() self._guarded_localed_wrapper_calls_check(localed_wrapper) + + @patch("pyanaconda.modules.localization.localed.SystemBus") + @patch("pyanaconda.modules.localization.localed.LOCALED") + @patch("pyanaconda.modules.localization.localed.conf") + def test_localed_wrapper_set_current_layout(self, mocked_conf, + mocked_localed_service, + mocked_system_bus): + """Test LocaledWrapper method to set current layout to compositor. + + Verify that the layout to be set is moved to the first place. + """ + mocked_system_bus.check_connection.return_value = True + mocked_conf.system.provides_system_bus = True + mocked_localed_proxy = Mock() + mocked_localed_service.get_proxy.return_value = mocked_localed_proxy + mocked_localed_proxy.X11Layout = "cz,fi,us,fr" + mocked_localed_proxy.X11Variant = "qwerty,,euro" + localed_wrapper = LocaledWrapper() + user_defined = ["cz (qwerty)", "fi", "us (euro)", "fr"] + + # check if layout is correctly set + localed_wrapper._user_layouts_variants = user_defined + localed_wrapper.set_current_layout("fi") + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "fi,us,fr,cz", + "pc105", # hardcoded + ",euro,,qwerty", + "", + False, + False + ) + + # check if layout is correctly set including variant + mocked_localed_proxy.SetX11Keyboard.reset_mock() + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.set_current_layout("us (euro)") is True + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "us,fr,cz,fi", + "pc105", # hardcoded + "euro,,qwerty,", + "", + False, + False + ) + + # check when we are selecting non-existing layout + mocked_localed_proxy.SetX11Keyboard.reset_mock() + mocked_localed_proxy.X11Layout = "fi" + mocked_localed_proxy.X11Variant = "" + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.set_current_layout("cz") is False + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_not_called() + + # check when the layout set is empty + mocked_localed_proxy.SetX11Keyboard.reset_mock() + mocked_localed_proxy.X11Layout = "" + mocked_localed_proxy.X11Variant = "" + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.set_current_layout("fr") is True + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "fr,cz,fi,us", + "pc105", # hardcoded + ",qwerty,,euro", + "", + False, + False + ) + + # can't set layout when we don't have user defined set + mocked_localed_proxy.SetX11Keyboard.reset_mock() + mocked_localed_proxy.X11Layout = "cz, us" + mocked_localed_proxy.X11Variant = "" + user_defined = [] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.set_current_layout("cz (qwerty)") is False + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_not_called() + + @patch("pyanaconda.modules.localization.localed.SystemBus") + @patch("pyanaconda.modules.localization.localed.LOCALED") + @patch("pyanaconda.modules.localization.localed.conf") + def test_localed_wrapper_set_next_layout(self, mocked_conf, + mocked_localed_service, + mocked_system_bus): + """Test LocaledWrapper method to set current layout to compositor. + + Verify that we are selecting next layout to what is currently set in compositor. + Because setting current layout changing the ordering we have to decide next layout based + on the user selection. + """ + mocked_system_bus.check_connection.return_value = True + mocked_conf.system.provides_system_bus = True + mocked_localed_proxy = Mock() + mocked_localed_service.get_proxy.return_value = mocked_localed_proxy + # currently selected is first in this list 'cz (qwerty)' + mocked_localed_proxy.X11Layout = "cz,fi,us,fr" + mocked_localed_proxy.X11Variant = "qwerty,,euro" + localed_wrapper = LocaledWrapper() + + # test switch to next layout + user_defined = ["cz (qwerty)", "fi", "us (euro)", "fr"] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.select_next_layout() is True + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "fi,us,fr,cz", + "pc105", # hardcoded + ",euro,,qwerty", + "", + False, + False + ) + + # test switch to next layout in the middle of user defined list + mocked_localed_proxy.SetX11Keyboard.reset_mock() + user_defined = ["es", "cz (qwerty)", "us (euro)", "fr"] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.select_next_layout() is True + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "us,fr,es,cz", + "pc105", # hardcoded + "euro,,,qwerty", + "", + False, + False + ) + + # test switch to next layout with different user defined list + mocked_localed_proxy.SetX11Keyboard.reset_mock() + user_defined = ["cz (qwerty)", "es"] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.select_next_layout() is True + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "es,cz", + "pc105", # hardcoded + ",qwerty", + "", + False, + False + ) + + # the compositor list is empty test + mocked_localed_proxy.SetX11Keyboard.reset_mock() + mocked_localed_proxy.X11Layout = "" + mocked_localed_proxy.X11Variant = "" + user_defined = ["cz (qwerty)", "es"] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.select_next_layout() is True + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "cz,es", + "pc105", # hardcoded + "qwerty,", + "", + False, + False + ) + + # the user defined list is empty test + mocked_localed_proxy.SetX11Keyboard.reset_mock() + mocked_localed_proxy.X11Layout = "cz,fi,us,fr" + mocked_localed_proxy.X11Variant = "qwerty,,euro" + user_defined = [] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.select_next_layout() is False + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_not_called() + + # the user defined list has only one value + mocked_localed_proxy.SetX11Keyboard.reset_mock() + mocked_localed_proxy.X11Layout = "cz,fi,us,es" + mocked_localed_proxy.X11Variant = "qwerty,,euro" + user_defined = ["es (euro)"] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.select_next_layout() is True + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "es", + "pc105", # hardcoded + "euro", + "", + False, + False + ) + + # everything is empty + mocked_localed_proxy.SetX11Keyboard.reset_mock() + mocked_localed_proxy.X11Layout = "" + mocked_localed_proxy.X11Variant = "" + user_defined = [] + localed_wrapper._user_layouts_variants = user_defined + + assert localed_wrapper.select_next_layout() is False + assert user_defined == localed_wrapper._user_layouts_variants # must not change + mocked_localed_proxy.SetX11Keyboard.assert_not_called() From f93dce957c569cb391590b047ecc2652a928cef5 Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Wed, 28 Aug 2024 13:18:13 +0200 Subject: [PATCH 2/6] Add localed signal support to LocaledWrapper with this patch when compositor will change keyboard layout we will be able to react to that in Anaconda. This is mostly useful for Live ISO images. We have two signals currently to resolve that something has changed, one of the signals is that selected layout has changed in the compositor. The issue is that localed service doesn't have information about selected (first is taken as selected). To resolve that we need to keep values from our last query or last signal about the change so we are able to detect the change in selection. (cherry picked from commit 1b6b2c6d2f6acde035b0f3fc32443b4784b9e2ad) Resolves: RHEL-58181 --- pyanaconda/modules/localization/localed.py | 50 ++++++ .../localization/test_localed_wrapper.py | 154 ++++++++++++++++++ 2 files changed, 204 insertions(+) diff --git a/pyanaconda/modules/localization/localed.py b/pyanaconda/modules/localization/localed.py index 3c25c68c319..2f4a43daeac 100644 --- a/pyanaconda/modules/localization/localed.py +++ b/pyanaconda/modules/localization/localed.py @@ -16,6 +16,7 @@ # Red Hat, Inc. # from pyanaconda.core.dbus import SystemBus +from pyanaconda.core.signal import Signal from pyanaconda.modules.common.constants.services import LOCALED from pyanaconda.core.configuration.anaconda import conf from pyanaconda.keyboard import join_layout_variant, parse_layout_variant, \ @@ -31,6 +32,9 @@ class LocaledWrapper(object): def __init__(self): self._localed_proxy = None self._user_layouts_variants = [] + self._last_layouts_variants = [] + self.compositor_layouts_changed = Signal() + self.compositor_selected_layout_changed = Signal() if not conf.system.provides_system_bus: log.debug("Not using localed service: " @@ -43,6 +47,47 @@ def __init__(self): return self._localed_proxy = LOCALED.get_proxy() + self._localed_proxy.PropertiesChanged.connect(self._on_properties_changed) + + def _on_properties_changed(self, interface, changed_props, invalid_props): + if "X11Layout" in changed_props or "X11Variant" in changed_props: + layouts_variants = self._from_localed_format(changed_props["X11Layout"].get_string(), + changed_props["X11Variant"].get_string()) + # This part is a bit tricky. The signal processing here means that compositor has + # changed current layouts configuration. This could happen for multiple reasons: + # - user changed the layout in compositor + # - Anaconda set the layout to compositor + # - any other magic logic for compositor (we just don't know) + # + # The question is how we should behave: + # - we don't want to take compositor layouts to Anaconda because that will change + # what user will have in the installed system. + # - we don't want to force our layouts to compositor because that would forbid user + # to change compositor layout when Anaconda runs in background + # + # The best shot seems to just signal out that the layout has changed and nothing else. + + # layouts has changed in compositor, always emit this signal + log.debug("Localed layouts has changed. Last known: '%s' current: '%s'", + self._last_layouts_variants, layouts_variants) + self.compositor_layouts_changed.emit(layouts_variants) + + # check if last selected variant has changed + # nothing is selected in compositor + if not layouts_variants: + log.warning("Compositor layouts not set.") + self.compositor_selected_layout_changed.emit("") + # we don't know last used layouts + elif not self._last_layouts_variants: + log.debug("Compositor selected layout is different. " + "Missing information about last selected layouts.") + self.compositor_selected_layout_changed.emit(layouts_variants[0]) + # selected (first) has changed + elif layouts_variants[0] != self._last_layouts_variants[0]: + log.debug("Compositor selected layout is different.") + self.compositor_selected_layout_changed.emit(layouts_variants[0]) + + self._last_layouts_variants = layouts_variants @property def keymap(self): @@ -69,6 +114,11 @@ def layouts_variants(self): layouts = self._localed_proxy.X11Layout variants = self._localed_proxy.X11Variant + self._last_layouts_variants = self._from_localed_format(layouts, variants) + return self._last_layouts_variants + + @staticmethod + def _from_localed_format(layouts, variants): layouts = layouts.split(",") if layouts else [] variants = variants.split(",") if variants else [] diff --git a/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py b/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py index 8f8471db173..97a77fba370 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py +++ b/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py @@ -17,6 +17,8 @@ # import unittest from unittest.mock import patch, Mock +from pyanaconda.core.signal import Signal +from pyanaconda.core.glib import Variant from pyanaconda.modules.localization.localed import LocaledWrapper @@ -347,3 +349,155 @@ def test_localed_wrapper_set_next_layout(self, mocked_conf, assert localed_wrapper.select_next_layout() is False assert user_defined == localed_wrapper._user_layouts_variants # must not change mocked_localed_proxy.SetX11Keyboard.assert_not_called() + + @patch("pyanaconda.modules.localization.localed.SystemBus") + @patch("pyanaconda.modules.localization.localed.LOCALED") + @patch("pyanaconda.modules.localization.localed.conf") + def test_localed_wrapper_signals(self, mocked_conf, + mocked_localed_service, + mocked_system_bus): + """Test signals from the localed wrapper + + This one could be tricky. The issue is that this class has to store last known values to + be able to recognize changes. + + We need: + last_known_from_compositor - we need to store what was in compositor before it changed + compositor configuration, so we can correct sent a message + that current selection is different + + None of the information above could be found directly from localed service. + """ + mocked_system_bus.check_connection.return_value = True + mocked_conf.system.provides_system_bus = True + mocked_localed_proxy = Mock() + mocked_localed_proxy.PropertiesChanged = Signal() + mocked_localed_service.get_proxy.return_value = mocked_localed_proxy + mocked_layouts_changed = Mock() + mocked_selected_layout_changed = Mock() + localed_wrapper = LocaledWrapper() + localed_wrapper.compositor_layouts_changed = mocked_layouts_changed + localed_wrapper.compositor_selected_layout_changed = mocked_selected_layout_changed + + def _check_localed_wrapper_signals(last_known_state, compositor_state, + expected_selected_signal, expected_layouts_signal): + """Test the localed wrapper signals are correctly emitted. + + :param last_known_state: State of the localed before the change. Used to resolve if + selected layout has changed. + :type last_known_state: [(str,str)] e.g.:[('cz', 'qwerty'), ('us','')...] + :param compositor_state: New state the compositor will get into. + :type compositor_state: {str: str} e.g.: {"X11Layout": "cz", "X11Variant": "qwerty"} + :param expected_selected_signal: Currently selected layout we expect LocaledWrapper + will signal out. If signal shouldn't set None. + :type expected_selected_signal: str + :param expected_layouts_signal: Current configuration of the compositor signaled from + LocaledWrapper. + :type expected_layouts_signal: [str] e.g.: ["cz", "us (euro)"] + """ + mocked_layouts_changed.reset_mock() + mocked_selected_layout_changed.reset_mock() + # set user defined layouts by setting current ones (mock will take this) + mocked_localed_proxy.X11Layout = ",".join(map(lambda x: x[0], last_known_state)) + mocked_localed_proxy.X11Variant = ",".join(map(lambda x: x[1], last_known_state)) + # loading the above values to local last known list + # pylint: disable=pointless-statement + localed_wrapper.layouts_variants + + for k in compositor_state: + compositor_state[k] = Variant('s', compositor_state[k]) + + mocked_localed_proxy.PropertiesChanged.emit(None, compositor_state, None) + # these signals should be called by localed wrapper + if expected_selected_signal is None: + mocked_selected_layout_changed.emit.assert_not_called() + else: + mocked_selected_layout_changed.emit.assert_called_once_with( + expected_selected_signal + ) + if expected_layouts_signal is None: + mocked_layouts_changed.emit.assert_not_called() + else: + mocked_layouts_changed.emit.assert_called_once_with(expected_layouts_signal) + # we shouldn't set values back to localed service + mocked_localed_proxy.SetX11Keyboard.assert_not_called() + + # basic test compositor changing different values + _check_localed_wrapper_signals( + last_known_state=[], + compositor_state={"X11Options": "grp:something"}, + expected_selected_signal=None, + expected_layouts_signal=None + ) + + # basic test with no knowledge of previous state + _check_localed_wrapper_signals( + last_known_state=[], + compositor_state={"X11Layout": "cz", + "X11Variant": "qwerty"}, + expected_selected_signal="cz (qwerty)", + expected_layouts_signal=["cz (qwerty)"] + ) + + # basic test with no knowledge of previous state and multiple values + _check_localed_wrapper_signals( + last_known_state=[], + compositor_state={"X11Layout": "cz,es", + "X11Variant": "qwerty,"}, + expected_selected_signal="cz (qwerty)", + expected_layouts_signal=["cz (qwerty)", "es"] + ) + + # test no values from compositor + _check_localed_wrapper_signals( + last_known_state=[("cz", "")], + compositor_state={"X11Layout": "", + "X11Variant": ""}, + expected_selected_signal="", + expected_layouts_signal=[] + ) + + # test with knowledge of previous state everything changed + _check_localed_wrapper_signals( + last_known_state=[("es", "euro"), ("us", "")], + compositor_state={"X11Layout": "cz", + "X11Variant": "qwerty"}, + expected_selected_signal="cz (qwerty)", + expected_layouts_signal=["cz (qwerty)"] + ) + + # test with knowledge of previous state no change + _check_localed_wrapper_signals( + last_known_state=[("cz", "qwerty"), ("es", "")], + compositor_state={"X11Layout": "cz,es", + "X11Variant": "qwerty,"}, + expected_selected_signal=None, + expected_layouts_signal=["cz (qwerty)", "es"] + ) + + # test with knowledge of previous state selected has changed + _check_localed_wrapper_signals( + last_known_state=[("cz", "qwerty"), ("es", "")], + compositor_state={"X11Layout": "es,cz", + "X11Variant": ",qwerty"}, + expected_selected_signal="es", + expected_layouts_signal=["es", "cz (qwerty)"] + ) + + # test with knowledge of previous state layouts has changed + _check_localed_wrapper_signals( + last_known_state=[("cz", "qwerty"), ("es", "")], + compositor_state={"X11Layout": "cz,es,us", + "X11Variant": "qwerty,,"}, + expected_selected_signal=None, + expected_layouts_signal=["cz (qwerty)", "es", "us"] + ) + + # test with knowledge of previous state just variant change + _check_localed_wrapper_signals( + last_known_state=[("cz", "qwerty"), ("es", "")], + compositor_state={"X11Layout": "cz,es,us", + "X11Variant": ",,"}, + expected_selected_signal="cz", + expected_layouts_signal=["cz", "es", "us"] + ) From 7f17a5b3737a937509de701b7d0f5e941d43bea1 Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Wed, 28 Aug 2024 17:24:24 +0200 Subject: [PATCH 3/6] Switch keyboard management to Localed Because of the switch to Wayland Anaconda has to change compositor keyboard manager because libxklavier doesn't work on Wayland. To fix that we migrated to Gnome Kiosk DBus API in RHEL-10, however, this solution can't be used outside of Gnome Kiosk. For that reason, we are switching to Localed which we set as the default to enable Anaconda to control keyboard switching. (cherry picked from commit 4dcdcf61ee0294348a978bdefaea270eb2bfde4c) Resolves: RHEL-58181 --- .../modules/common/constants/services.py | 5 - .../localization/gk_keyboard_manager.py | 135 --------------- .../modules/localization/localization.py | 32 ++-- .../localization/gk_keyboard_manager_test.py | 156 ------------------ .../localization/test_module_localization.py | 40 ++--- 5 files changed, 33 insertions(+), 335 deletions(-) delete mode 100644 pyanaconda/modules/localization/gk_keyboard_manager.py delete mode 100644 tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py diff --git a/pyanaconda/modules/common/constants/services.py b/pyanaconda/modules/common/constants/services.py index 88b3e6f41d2..72ed9b4890a 100644 --- a/pyanaconda/modules/common/constants/services.py +++ b/pyanaconda/modules/common/constants/services.py @@ -110,11 +110,6 @@ # Session services. -GK_INPUT_SOURCES = DBusServiceIdentifier( - namespace=("org", "gnome", "Kiosk"), - message_bus=SessionBus -) - MUTTER_DISPLAY_CONFIG = DBusServiceIdentifier( namespace=("org", "gnome", "Mutter", "DisplayConfig"), message_bus=SessionBus diff --git a/pyanaconda/modules/localization/gk_keyboard_manager.py b/pyanaconda/modules/localization/gk_keyboard_manager.py deleted file mode 100644 index 8a68e59990f..00000000000 --- a/pyanaconda/modules/localization/gk_keyboard_manager.py +++ /dev/null @@ -1,135 +0,0 @@ -# -# Copyright (C) 2024 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# - -from pyanaconda.core.signal import Signal -from pyanaconda.keyboard import join_layout_variant, parse_layout_variant, KeyboardConfigError -from pyanaconda.modules.common.constants.services import GK_INPUT_SOURCES - - -class GkKeyboardManager(object): - """Class wrapping GNOME Kiosk's input sources API.""" - - def __init__(self): - self.compositor_selected_layout_changed = Signal() - self.compositor_layouts_changed = Signal() - - object_path = GK_INPUT_SOURCES.object_path + '/InputSources/Manager' - self._proxy = GK_INPUT_SOURCES.get_proxy(object_path=object_path) - self._proxy.PropertiesChanged.connect(self._on_properties_changed) - - def _on_properties_changed(self, interface, changed_props, invalid_props): - for prop in changed_props: - if prop == 'SelectedInputSource': - layout_path = changed_props[prop] - layout_variant = self._path_to_layout(layout_path.get_string()) - self.compositor_selected_layout_changed.emit(layout_variant) - if prop == 'InputSources': - layout_paths = changed_props[prop] - layout_variants = map(self._path_to_layout, list(layout_paths)) - self.compositor_layouts_changed.emit(list(layout_variants)) - - def _path_to_layout(self, layout_path): - """Transforms a layout path as returned by GNOME Kiosk to "layout (variant)". - - :param layout_path: D-Bus path to the layout. - (e.g. "/org/gnome/Kiosk/InputSources/xkb_cz_2b_mon_5f_todo_5f_galik") - :type layout_path: str - :return: The layout with format "layout (variant)" (e.g. "cn (mon_todo_galik)") - :rtype: str - - :raise KeyboardConfigError: if layouts with invalid backend type is found - """ - layout_proxy = GK_INPUT_SOURCES.get_proxy(object_path=layout_path) - - if layout_proxy.BackendType != 'xkb': - raise KeyboardConfigError('Failed to get configuration from compositor') - - if '+' in layout_proxy.BackendId: - layout, variant = layout_proxy.BackendId.split('+') - return join_layout_variant(layout, variant) - else: - return layout_proxy.BackendId - - def _layout_to_xkb(self, layout_variant): - """Transforms a "layout (variant)" to a "('xkb', 'layout+variant')". - - :param layout_variant: The layout with format "layout (variant)" (e.g. "cz (qwerty)") - :type layout_variant: str - :return: The layout with format "('xkb', 'layout+variant')" (e.g. "('xkb', 'cz+qwerty')") - :rtype: str - """ - layout, variant = parse_layout_variant(layout_variant) - if variant: - return ('xkb', '{0}+{1}'.format(layout, variant)) - else: - return ('xkb', layout) - - def get_compositor_selected_layout(self): - """Get the activated keyboard layout. - - :return: Current keyboard layout (e.g. "cz (qwerty)") - :rtype: str - """ - layout_path = self._proxy.SelectedInputSource - if not layout_path or layout_path == '/': - return '' - - return self._path_to_layout(layout_path) - - def set_compositor_selected_layout(self, layout_variant): - """Set the activated keyboard layout. - - :param layout_variant: The layout to set, with format "layout (variant)" - (e.g. "cz (qwerty)") - :type layout_variant: str - :return: If the keyboard layout was activated - :rtype: bool - """ - layout_paths = self._proxy.InputSources - for layout_path in layout_paths: - if self._path_to_layout(layout_path) == layout_variant: - self._proxy.SelectInputSource(layout_path) - return True - - return False - - def select_next_compositor_layout(self): - """Set the next available layout as active.""" - self._proxy.SelectNextInputSource() - - def get_compositor_layouts(self): - """Get all available keyboard layouts. - - :return: A list of keyboard layouts (e.g. ["cz (qwerty)", cn (mon_todo_galik)]) - :rtype: list of strings - """ - layout_paths = self._proxy.InputSources - layout_variants = map(self._path_to_layout, list(layout_paths)) - return list(layout_variants) - - def set_compositor_layouts(self, layout_variants, options): - """Set the available keyboard layouts. - - :param layout_variants: A list of keyboard layouts (e.g. ["cz (qwerty)", - cn (mon_todo_galik)]) - :type layout_variants: list of strings - :param options: A list of switching options - :type options: list of strings - """ - xkb_layouts = list(map(self._layout_to_xkb, layout_variants)) - self._proxy.SetInputSources(xkb_layouts, options) diff --git a/pyanaconda/modules/localization/localization.py b/pyanaconda/modules/localization/localization.py index 452481e56ac..6d8e44951d1 100644 --- a/pyanaconda/modules/localization/localization.py +++ b/pyanaconda/modules/localization/localization.py @@ -35,7 +35,6 @@ from pyanaconda.modules.localization.runtime import GetMissingKeyboardConfigurationTask, \ ApplyKeyboardTask, AssignGenericKeyboardSettingTask from pyanaconda.modules.localization.localed import LocaledWrapper -from pyanaconda.modules.localization.gk_keyboard_manager import GkKeyboardManager from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) @@ -71,7 +70,6 @@ def __init__(self): self.compositor_layouts_changed = Signal() self._localed_wrapper = None - self._compositor_keyboard_manager = None def publish(self): """Publish the module.""" @@ -249,6 +247,13 @@ def set_keyboard_seen(self, keyboard_seen): def localed_wrapper(self): if not self._localed_wrapper: self._localed_wrapper = LocaledWrapper() + + self._localed_wrapper.compositor_selected_layout_changed.connect( + self.compositor_selected_layout_changed.emit + ) + self._localed_wrapper.compositor_layouts_changed.connect( + self.compositor_layouts_changed.emit + ) return self._localed_wrapper def install_with_tasks(self): @@ -321,30 +326,17 @@ def set_from_generic_keyboard_setting(self, keyboard): result = task.run() self._update_settings_from_task(result) - @property - def compositor_keyboard_manager(self): - if not self._compositor_keyboard_manager: - self._compositor_keyboard_manager = GkKeyboardManager() - self._compositor_keyboard_manager.compositor_selected_layout_changed.connect( - lambda layout: self.compositor_selected_layout_changed.emit(layout) - ) - self._compositor_keyboard_manager.compositor_layouts_changed.connect( - lambda layouts: self.compositor_layouts_changed.emit(layouts) - ) - - return self._compositor_keyboard_manager - def get_compositor_selected_layout(self): - return self.compositor_keyboard_manager.get_compositor_selected_layout() + return self.localed_wrapper.current_layout_variant def set_compositor_selected_layout(self, layout_variant): - return self.compositor_keyboard_manager.set_compositor_selected_layout(layout_variant) + return self.localed_wrapper.set_current_layout(layout_variant) def select_next_compositor_layout(self): - return self.compositor_keyboard_manager.select_next_compositor_layout() + return self.localed_wrapper.select_next_layout() def get_compositor_layouts(self): - return self.compositor_keyboard_manager.get_compositor_layouts() + return self.localed_wrapper.layouts_variants def set_compositor_layouts(self, layout_variants, options): - self.compositor_keyboard_manager.set_compositor_layouts(layout_variants, options) + self.localed_wrapper.set_layouts(layout_variants, options) diff --git a/tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py b/tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py deleted file mode 100644 index 021b08ab9b9..00000000000 --- a/tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py +++ /dev/null @@ -1,156 +0,0 @@ -# -# Copyright (C) 2024 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# -import unittest -import pytest - -from unittest.mock import patch, Mock - -from pyanaconda.modules.localization.gk_keyboard_manager import GkKeyboardManager -from pyanaconda.keyboard import KeyboardConfigError - - -LAYOUT_PROXY_MOCKS = { - "/org/gnome/Kiosk/InputSources/xkb_fr": - Mock(BackendType="xkb", BackendId="fr"), - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik": - Mock(BackendType="xkb", BackendId="cn+mon_todo_galik"), - "/org/gnome/Kiosk/InputSources/non-xkb_fr": - Mock(BackendType="non-xkb", BackendId="fr"), - "/org/gnome/Kiosk/InputSources/Manager": - Mock(), -} - -MockedGKIS = Mock() -MockedGKIS.get_proxy = lambda object_path: LAYOUT_PROXY_MOCKS[object_path] -MockedGKIS.object_path = "/org/gnome/Kiosk" - - -@patch("pyanaconda.modules.localization.gk_keyboard_manager.GK_INPUT_SOURCES", new=MockedGKIS) -class GkKeyboardManagerTestCase(unittest.TestCase): - """Test the Gnome Kiosk keyboard manager.""" - - def test_properties_changed(self): - """Test _on_properties_changed callback""" - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - callback1_mock = Mock() - callback2_mock = Mock() - mocked_manager.compositor_selected_layout_changed.connect(callback1_mock) - mocked_manager.compositor_layouts_changed.connect(callback2_mock) - - object_path_mock = Mock() - object_path_mock.get_string.return_value = "/org/gnome/Kiosk/InputSources/xkb_fr" - mocked_manager._on_properties_changed( - "org.gnome.Kiosk.InputSources", - {"SelectedInputSource": object_path_mock}, - {}, - ) - callback1_mock.assert_called_once_with("fr") - callback2_mock.assert_not_called() - - mocked_manager._on_properties_changed( - "org.gnome.Kiosk.InputSources", - {"InputSources": ["/org/gnome/Kiosk/InputSources/xkb_fr"]}, - [], - ) - callback1_mock.assert_called_once_with("fr") - callback2_mock.assert_called_once_with(["fr"]) - - def test_get_compositor_selected_layout(self): - """Test the get_compositor_selected_layout method""" - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - - mocked_manager._proxy.SelectedInputSource = "/" - assert mocked_manager.get_compositor_selected_layout() == "" - - mocked_manager._proxy.SelectedInputSource = None - assert mocked_manager.get_compositor_selected_layout() == "" - - layout_path = "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik" - mocked_manager._proxy.SelectedInputSource = layout_path - assert mocked_manager.get_compositor_selected_layout() == "cn (mon_todo_galik)" - - def test_set_compositor_selected_layout(self): - """Test the set_compositor_selected_layout method""" - - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - assert mocked_manager.set_compositor_selected_layout("cn (mon_todo_galik)") is True - mocked_manager._proxy.SelectInputSource.assert_called_with( - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik" - ) - - # non-xkb type raises exception - # (even in case there is xkb-type data for the layout) - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/non-xkb_fr", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - with pytest.raises(KeyboardConfigError): - mocked_manager.set_compositor_selected_layout("fr") - - # Source not found - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - assert mocked_manager.set_compositor_selected_layout("cn (mon_todo_galik)") is False - - def test_select_next_compositor_layout(self): - """Test the select_next_compositor_layout method""" - mocked_manager = GkKeyboardManager() - mocked_manager.select_next_compositor_layout() - mocked_manager._proxy.SelectNextInputSource.assert_called_once() - - def test_get_compositor_layouts(self): - """Test the get_compositor_layouts method""" - - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr", - ] - assert mocked_manager.get_compositor_layouts() == ["cn (mon_todo_galik)", "fr"] - - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/non-xkb_fr", - "/org/gnome/Kiosk/InputSources/xkb_fr", - ] - with pytest.raises(KeyboardConfigError): - mocked_manager.get_compositor_layouts() - - def test_set_compositor_layouts(self): - """Test the set_compositor_layouts method""" - mocked_manager = GkKeyboardManager() - mocked_manager.set_compositor_layouts( - ["cz (qwerty)", "fi", "us (euro)", "fr"], - ["grp:alt_shift_toggle", "grp:ctrl_alt_toggle"], - ) - mocked_manager._proxy.SetInputSources.assert_called_with( - [("xkb", "cz+qwerty"), ("xkb", "fi"), ("xkb", "us+euro"), ("xkb", "fr")], - ["grp:alt_shift_toggle", "grp:ctrl_alt_toggle"], - ) diff --git a/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py b/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py index 0ed2b6d2b3d..75c4d05a8aa 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py +++ b/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py @@ -393,32 +393,34 @@ def test_keyboard_kickstart4(self): """ self._test_kickstart(ks_in, ks_out) - @patch("pyanaconda.modules.localization.localization.GkKeyboardManager") - def test_compositor_layouts_api(self, gk_manager_cls): - manager_class_mock = Mock() - manager_class_mock.compositor_selected_layout_changed = Signal() - manager_class_mock.compositor_layouts_changed = Signal() - gk_manager_cls.return_value = manager_class_mock + @patch("pyanaconda.modules.localization.localization.LocaledWrapper") + def test_compositor_layouts_api(self, mocked_localed_wrapper): + localed_class_mock = Mock() + localed_class_mock.compositor_selected_layout_changed = Signal() + localed_class_mock.compositor_layouts_changed = Signal() + mocked_localed_wrapper.return_value = localed_class_mock - self.localization_module._compositor_keyboard_manager = None - manager_mock = self.localization_module.compositor_keyboard_manager + self.localization_module._localed_wrapper = None + manager_mock = self.localization_module.localed_wrapper + + manager_mock.current_layout_variant = "cz" + assert self.localization_interface.GetCompositorSelectedLayout() == "cz" - self.localization_interface.GetCompositorSelectedLayout() - # pylint: disable=no-member - manager_mock.get_compositor_selected_layout.assert_called_once() self.localization_interface.SetCompositorSelectedLayout("cz (qwerty)") # pylint: disable=no-member - manager_mock.set_compositor_selected_layout.assert_called_once_with("cz (qwerty)") + manager_mock.set_current_layout.assert_called_once_with("cz (qwerty)") + self.localization_interface.SelectNextCompositorLayout() # pylint: disable=no-member - manager_mock.select_next_compositor_layout.assert_called_once() - self.localization_interface.GetCompositorLayouts() - # pylint: disable=no-member - manager_mock.get_compositor_layouts.assert_called_once() + manager_mock.select_next_layout.assert_called_once() + + manager_mock.layouts_variants = ["us", "es"] + assert self.localization_interface.GetCompositorLayouts() == ["us", "es"] + self.localization_interface.SetCompositorLayouts(["cz (qwerty)", "cn (mon_todo_galik)"], ["option"]) # pylint: disable=no-member - manager_mock.set_compositor_layouts.assert_called_once_with( + manager_mock.set_layouts.assert_called_once_with( ["cz (qwerty)", "cn (mon_todo_galik)"], ["option"] ) @@ -427,13 +429,13 @@ def test_compositor_layouts_api(self, gk_manager_cls): callback_mock = Mock() # pylint: disable=no-member self.localization_interface.CompositorSelectedLayoutChanged.connect(callback_mock) - manager_class_mock.compositor_selected_layout_changed.emit("cz (qwerty)") + localed_class_mock.compositor_selected_layout_changed.emit("cz (qwerty)") callback_mock.assert_called_once_with("cz (qwerty)") callback_mock = Mock() # pylint: disable=no-member self.localization_interface.CompositorLayoutsChanged.connect(callback_mock) - manager_class_mock.compositor_layouts_changed.emit(["cz (qwerty)", "cn (mon_todo_galik)"]) + localed_class_mock.compositor_layouts_changed.emit(["cz (qwerty)", "cn (mon_todo_galik)"]) callback_mock.assert_called_once_with(["cz (qwerty)", "cn (mon_todo_galik)"]) class LocalizationModuleTestCase(unittest.TestCase): From 9e2d7c356cd955d81464206b92ad3e50ec26bd53 Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Tue, 3 Sep 2024 11:52:26 +0200 Subject: [PATCH 4/6] Remove Wayland detection logic from code This logic was used to disable keyboard switching for given system. However, we we support even Wayland systems with the new solution, so let's remove this. (cherry picked from commit cc9ad83c9968043a7ac500a7c9b806585d5a24bb) Related: RHEL-58181 --- anaconda.spec.in | 1 - pyanaconda/core/configuration/system.py | 5 --- pyanaconda/keyboard.py | 43 +++---------------- .../pyanaconda_tests/test_keyboard.py | 38 +--------------- 4 files changed, 6 insertions(+), 81 deletions(-) diff --git a/anaconda.spec.in b/anaconda.spec.in index 6ea127f1eef..17eba982538 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -174,7 +174,6 @@ BuildRequires: desktop-file-utils Requires: anaconda-gui = %{version}-%{release} Requires: usermode Requires: zenity -Requires: xisxwayland Recommends: xhost %description live diff --git a/pyanaconda/core/configuration/system.py b/pyanaconda/core/configuration/system.py index caedd1e5fa3..c92398d217a 100644 --- a/pyanaconda/core/configuration/system.py +++ b/pyanaconda/core/configuration/system.py @@ -127,11 +127,6 @@ def can_configure_keyboard(self): """Can we configure the keyboard?""" return self._is_boot_iso or self._is_live_os or self._is_booted_os - @property - def can_run_on_xwayland(self): - """Could we run on XWayland?""" - return self._is_live_os - @property def can_modify_syslog(self): """Can we modify syslog?""" diff --git a/pyanaconda/keyboard.py b/pyanaconda/keyboard.py index 0705c784406..353ad110b48 100644 --- a/pyanaconda/keyboard.py +++ b/pyanaconda/keyboard.py @@ -26,7 +26,6 @@ from pyanaconda.core.configuration.anaconda import conf from pyanaconda import localization from pyanaconda.core.constants import DEFAULT_KEYBOARD -from pyanaconda.core.util import execWithRedirect from pyanaconda.modules.common.task import sync_run_task from pyanaconda.modules.common.constants.services import LOCALIZATION @@ -56,47 +55,15 @@ class InvalidLayoutVariantSpec(Exception): pass -def _is_xwayland(): - """Is Anaconda running in XWayland environment? - - This can't be easily detected from the Anaconda because Anaconda - is running as XWayland app. Use xisxwayland tool for the detection. - """ - try: - rc = execWithRedirect('xisxwayland', []) - - if rc == 0: - return True - - log.debug( - "Anaconda doesn't run on XWayland. " - "See xisxwayland --help for more info." - ) - except FileNotFoundError: - log.warning( - "The xisxwayland tool is not available! " - "Taking the environment as not Wayland." - ) - - return False - - def can_configure_keyboard(): """Can we configure the keyboard? - FIXME: This is a temporary solution. - - The is_wayland logic is not part of the configuration so we would - have to add it to the configuration otherwise it won't be accessible - in the Anaconda modules. + NOTE: + This function could be inlined, however, this give us a possibility for future limitation + when needed. For example we could use this method to limit keyboard configuration if we + are able to detect that current system doesn't support localed keyboard layout switching. """ - if not conf.system.can_configure_keyboard: - return False - - if conf.system.can_run_on_xwayland and _is_xwayland(): - return False - - return True + return conf.system.can_configure_keyboard def parse_layout_variant(layout_variant_str): diff --git a/tests/unit_tests/pyanaconda_tests/test_keyboard.py b/tests/unit_tests/pyanaconda_tests/test_keyboard.py index b97aef06649..798732fafa4 100644 --- a/tests/unit_tests/pyanaconda_tests/test_keyboard.py +++ b/tests/unit_tests/pyanaconda_tests/test_keyboard.py @@ -26,51 +26,15 @@ class KeyboardUtilsTestCase(unittest.TestCase): """Test the keyboard utils.""" @patch("pyanaconda.keyboard.conf") - @patch("pyanaconda.keyboard.execWithRedirect") - def test_can_configure_keyboard(self, exec_mock, conf_mock): + def test_can_configure_keyboard(self, conf_mock): """Check if the keyboard configuration is enabled or disabled.""" # It's a dir installation. conf_mock.system.can_configure_keyboard = False - conf_mock.system.can_run_on_xwayland = False assert keyboard.can_configure_keyboard() is False - exec_mock.assert_not_called() # It's a boot.iso. conf_mock.system.can_configure_keyboard = True - conf_mock.system.can_run_on_xwayland = False assert keyboard.can_configure_keyboard() is True - exec_mock.assert_not_called() - - # It's a Live installation on Wayland. - conf_mock.system.can_configure_keyboard = True - conf_mock.system.can_run_on_xwayland = True - exec_mock.return_value = 0 - assert keyboard.can_configure_keyboard() is False - exec_mock.assert_called_once_with('xisxwayland', []) - exec_mock.reset_mock() - - # It's a Live installation and not on Wayland. - conf_mock.system.can_configure_keyboard = True - conf_mock.system.can_run_on_xwayland = True - exec_mock.return_value = 1 # xisxwayland returns 1 if it is not XWayland - assert keyboard.can_configure_keyboard() is True - exec_mock.assert_called_once_with('xisxwayland', []) - exec_mock.reset_mock() - - # It's a Live installation and probably not on Wayland, - # because the xisxwayland tooling is not present. - conf_mock.system.can_configure_keyboard = True - conf_mock.system.can_run_on_xwayland = True - exec_mock.side_effect = FileNotFoundError() - - with self.assertLogs(level="WARNING") as cm: - keyboard.can_configure_keyboard() - - msg = "The xisxwayland tool is not available!" - assert any(map(lambda x: msg in x, cm.output)) - - exec_mock.assert_called_once_with('xisxwayland', []) - exec_mock.reset_mock() class ParsingAndJoiningTests(unittest.TestCase): From 3842800139902a14bbf536581cf8dd0a568b8357 Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Wed, 14 Aug 2024 18:22:13 +0200 Subject: [PATCH 5/6] Add release-notes for Wayland migration https://fedoraproject.org/wiki/Changes/Anaconda_As_Native_Wayland_Application (cherry picked from commit 0abcdac822c9bbaa0b1389813f94a3adcd02d35a) Related: RHEL-58181 --- docs/release-notes/rdp-support.rst | 16 ++++++++++++++++ docs/release-notes/wayland-migration.rst | 13 +++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 docs/release-notes/rdp-support.rst create mode 100644 docs/release-notes/wayland-migration.rst diff --git a/docs/release-notes/rdp-support.rst b/docs/release-notes/rdp-support.rst new file mode 100644 index 00000000000..fc51e5dd8d8 --- /dev/null +++ b/docs/release-notes/rdp-support.rst @@ -0,0 +1,16 @@ +:Type: GUI +:Summary: Replace VNC with RDP (#2231339) + +:Description: + As part of the X11 dependencies removals, Anaconda also drops VNC. As a replacement + RDP (Remote Desktop Protocol) is implemented. + + What has changed: + - Adding new kernel boot arguments: ``inst.rdp``, ``inst.rdp.username``, ``inst.rdp.password``. + - Drop existing kernel boot argument: ``inst.vnc``, ``inst.vncpassword``, ``inst.vncconnect``. + - Drop the existing ``vnc`` kickstart command. + +:Links: + - https://fedoraproject.org/wiki/Changes/Anaconda_As_Native_Wayland_Application + - https://github.com/rhinstaller/anaconda/pull/5829 + - https://bugzilla.redhat.com/show_bug.cgi?id=1955025 diff --git a/docs/release-notes/wayland-migration.rst b/docs/release-notes/wayland-migration.rst new file mode 100644 index 00000000000..0af19d15437 --- /dev/null +++ b/docs/release-notes/wayland-migration.rst @@ -0,0 +1,13 @@ +:Type: GUI +:Summary: Migrate Anaconda to Wayland application (#2231339) + +:Description: + This change enables Anaconda to run natively on Wayland. Previously, Anaconda operated as an + Xorg application or relied on XWayland for support. + + By implementing this update, we can eliminate dependencies on X11 and embrace newer, more + secure technologies. + +:Links: + - https://fedoraproject.org/wiki/Changes/Anaconda_As_Native_Wayland_Application + - https://github.com/rhinstaller/anaconda/pull/5829 From d1f442f269f52f59693a2a5f329ae98011e96e9b Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Fri, 13 Sep 2024 13:21:14 +0200 Subject: [PATCH 6/6] Do not change compositor options when not defined If Anaconda will set keyboard layouts to compositor but options are missed then we shouldn't change the compositor options but rather use what is already set. This will avoid problematic behavior of changing what user has defined in the system or similar cases. Also we have this tested in kickstart-tests, so this commit is fixing these tests. Also fix existing tests and cover this functionality by tests. (cherry picked from commit e810bba5ec71fa035c4c4a98bd977f42bca83db9) Related: RHEL-58181 --- pyanaconda/modules/localization/localed.py | 13 +++++-- .../localization/test_localed_wrapper.py | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/pyanaconda/modules/localization/localed.py b/pyanaconda/modules/localization/localed.py index 2f4a43daeac..d49c9a1b938 100644 --- a/pyanaconda/modules/localization/localed.py +++ b/pyanaconda/modules/localization/localed.py @@ -228,9 +228,6 @@ def _set_layouts(self, layouts_variants, options=None, convert=False): variants = [] parsing_failed = False - log.debug("Setting system/compositor keyboard layouts: '%s' options: '%s' convert: '%s", - layouts_variants, options, convert) - for layout_variant in (nonempty for nonempty in layouts_variants if nonempty): try: (layout, variant) = parse_layout_variant(layout_variant) @@ -244,9 +241,17 @@ def _set_layouts(self, layouts_variants, options=None, convert=False): if not layouts and parsing_failed: return + if options is None: + options = self.options + log.debug("Keyboard layouts for system/compositor are missing options. " + "Use compositor options: %s", options) + layouts_str = ",".join(layouts) variants_str = ",".join(variants) - options_str = ",".join(options) if options else "" + options_str = ",".join(options) + + log.debug("Setting system/compositor keyboard layouts: '%s' options: '%s' convert: '%s", + layouts_variants, options, convert) self._localed_proxy.SetX11Keyboard( layouts_str, diff --git a/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py b/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py index 97a77fba370..ac73f31b5a3 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py +++ b/tests/unit_tests/pyanaconda_tests/modules/localization/test_localed_wrapper.py @@ -128,6 +128,41 @@ def test_localed_wrapper_safe_calls(self, mocked_conf, mocked_localed_service, localed_wrapper.set_layouts(["cz", "us (euro)"]) assert localed_wrapper._user_layouts_variants == ["cz", "us (euro)"] + # test set_layout on proxy with options + mocked_localed_proxy.SetX11Keyboard.reset_mock() + localed_wrapper.set_layouts(["cz (qwerty)", "us"]) + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "cz,us", + "pc105", # hardcoded + "qwerty,", + "grp:alt_shift_toggle,grp:ctrl_alt_toggle", # options will be reused what is set + False, + False + ) + + # test set_layout on proxy with options not set explicitly (None) + mocked_localed_proxy.SetX11Keyboard.reset_mock() + localed_wrapper.set_layouts(["cz (qwerty)", "us"], options=None) + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "cz,us", + "pc105", # hardcoded + "qwerty,", + "grp:alt_shift_toggle,grp:ctrl_alt_toggle", # options will be reused what is set + False, + False + ) + + mocked_localed_proxy.SetX11Keyboard.reset_mock() + localed_wrapper.set_layouts(["us"], "", True) + mocked_localed_proxy.SetX11Keyboard.assert_called_once_with( + "us", + "pc105", # hardcoded + "", + "", # empty options will remove existing options + True, + False + ) + @patch("pyanaconda.modules.localization.localed.SystemBus") def test_localed_wrapper_no_systembus(self, mocked_system_bus): """Test LocaledWrapper in environment without system bus. @@ -155,6 +190,7 @@ def test_localed_wrapper_set_current_layout(self, mocked_conf, mocked_localed_service.get_proxy.return_value = mocked_localed_proxy mocked_localed_proxy.X11Layout = "cz,fi,us,fr" mocked_localed_proxy.X11Variant = "qwerty,,euro" + mocked_localed_proxy.X11Options = "" localed_wrapper = LocaledWrapper() user_defined = ["cz (qwerty)", "fi", "us (euro)", "fr"] @@ -190,6 +226,7 @@ def test_localed_wrapper_set_current_layout(self, mocked_conf, mocked_localed_proxy.SetX11Keyboard.reset_mock() mocked_localed_proxy.X11Layout = "fi" mocked_localed_proxy.X11Variant = "" + mocked_localed_proxy.X11Options = "" localed_wrapper._user_layouts_variants = user_defined assert localed_wrapper.set_current_layout("cz") is False @@ -217,6 +254,7 @@ def test_localed_wrapper_set_current_layout(self, mocked_conf, mocked_localed_proxy.SetX11Keyboard.reset_mock() mocked_localed_proxy.X11Layout = "cz, us" mocked_localed_proxy.X11Variant = "" + mocked_localed_proxy.X11Options = "" user_defined = [] localed_wrapper._user_layouts_variants = user_defined @@ -243,6 +281,7 @@ def test_localed_wrapper_set_next_layout(self, mocked_conf, # currently selected is first in this list 'cz (qwerty)' mocked_localed_proxy.X11Layout = "cz,fi,us,fr" mocked_localed_proxy.X11Variant = "qwerty,,euro" + mocked_localed_proxy.X11Options = "" localed_wrapper = LocaledWrapper() # test switch to next layout