Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use keyboard configuration from Gnome Shell live #4976

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add live environment keyboard settings support
This configuration will be used if no keyboard layout is set by user and
Anaconda is running on supported live environment.

This solution is integrated to the already existing
`get_missing_keyboard_configuration` function before using the
DEFAULT_KEYBOARD layout (US).

Before this change the logic was:
- use configuration if set by user
- if only keyboard layout or virtual console is set convert the other by
localed
- if nothing set use DEFAULT_KEYBOARD

After this change:
- use configuration if set by user
- if on supported live environment, read the current keyboard
configuration
- if only keyboard layout or virtual console is set convert the other by
localed
- if nothing set use DEFAULT_KEYBOARD

Currently, only Gnome Shell is supported but we should extend this
solution ideally to all Wayland based spins. Wayland is hostile to
Anaconda because it don't allow us to control live keyboard
configuration. Live environment now have configuration of the keyboard
separate from the Anaconda configuration, which is used for the
installed system. That means that users can write passwords with other
layout than what is installed (especially problem for LUKS).
  • Loading branch information
jkonecny12 committed Aug 10, 2023
commit 5dc8832ff034bea564d26fc1e1952bddc336276b
99 changes: 99 additions & 0 deletions pyanaconda/modules/localization/live_keyboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright (C) 2023 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 abc import ABC, abstractmethod
from pyanaconda.core.util import execWithCaptureAsLiveUser
from pyanaconda.core.configuration.anaconda import conf

from pyanaconda.anaconda_loggers import get_module_logger
log = get_module_logger(__name__)


def get_live_keyboard_instance():
"""Return instance of a class based on the current running live system.

:return: instance of a LiveSystemKeyboardBase based class for the current live environment
:rtype: instance of class inherited from LiveSystemKeyboardBase class
"""
if conf.system.provides_liveuser:
return GnomeShellKeyboard()

# TODO: Add support for other Live systems
return None


class LiveSystemKeyboardBase(ABC):

@abstractmethod
def read_keyboard_layouts(self):
"""Read keyboard configuration from the current system.

The configuration have to be returned in format which is understandable by us for the
installation. That means localed will understand it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be a good idea to note there what formats are supported ? Or at least what tooling is going to work with them, so one can find out the supported formats transitively. Is it just localed or also something else ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is specified in the return value of the docstring.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, now I see it. :)


:return: a list of "layout (variant)" or "layout" layout specifications
:rtype: list(str)
"""
pass

@staticmethod
def _run_as_liveuser(argv):
"""Run the command in a system as liveuser user.

:param list argv: list of arguments for the command.
:return: output of the command
:rtype: str
"""
return execWithCaptureAsLiveUser(argv[0], argv[1:])


class GnomeShellKeyboard(LiveSystemKeyboardBase):

def read_keyboard_layouts(self):
"""Read keyboard configuration from the current system.

The configuration have to be returned in format which is understandable by us for the
installation. That means localed will understand it.

:return: a list of "layout (variant)" or "layout" layout specifications
:rtype: list(str)
"""
command_args = ["gsettings", "get", "org.gnome.desktop.input-sources", "sources"]
sources = self._run_as_liveuser(command_args)

# convert output "[('xkb', 'us'), ('xkb', 'cz+qwerty')]\n"
try:
sources = eval(sources.rstrip())
except SyntaxError:
log.error("Gnome Shell keyboard configuration can't be obtained from source %s!",
sources)
return []

result = []

for t in sources:
# keep only 'xkb' type and ignore 'ibus' variants which can't be used in localed
if t[0] == "xkb":
layout = t[1]
# change layout variant from 'cz+qwerty' to 'cz (qwerty)'
if '+' in layout:
layout, variant = layout.split('+')
result.append(f"{layout} ({variant})")
else:
result.append(layout)

return result
39 changes: 38 additions & 1 deletion pyanaconda/modules/localization/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,25 @@
# Red Hat, Inc.
#
from pyanaconda.core.constants import DEFAULT_KEYBOARD
from pyanaconda.modules.localization.live_keyboard import get_live_keyboard_instance

from pyanaconda.anaconda_loggers import get_module_logger
log = get_module_logger(__name__)


def get_missing_keyboard_configuration(localed_wrapper, x_layouts, vc_keymap):
"""Get missing keyboard settings by conversion and default values.
"""Get keyboard configuration if not set by user.

Algorith works as this:
1. Check if keyboard configuration is not already set by user
-> return these
2. Check if keyboard confifuration can be obtained from live system
-> read layouts
-> convert currently used live layout to virtual console keymap by localed
3a. If still no configuration is present
-> use DEFAULT_KEYBOARD
3b. If only one of the keyboard layout or virtual console keymap is set
-> convert one to the other by localed

:param localed_wrapper: instance of systemd-localed service wrapper
:type localed_wrapper: LocaledWrapper
Expand All @@ -33,10 +45,35 @@ def get_missing_keyboard_configuration(localed_wrapper, x_layouts, vc_keymap):
:returns: tuple of X layouts and VC keyboard settings
:rtype: (list(str), str))
"""
if vc_keymap and x_layouts:
log.debug("Keyboard layouts and virtual console keymap already set - nothing to do")
return x_layouts, vc_keymap

live_keyboard = get_live_keyboard_instance()

if live_keyboard:
log.debug("Keyboad configuration from Live system is available")
x_layouts = _resolve_missing_from_live(live_keyboard, x_layouts)

if not vc_keymap and not x_layouts:
log.debug("Using default value %s for missing virtual console keymap", DEFAULT_KEYBOARD)
vc_keymap = DEFAULT_KEYBOARD

if not vc_keymap or not x_layouts:
x_layouts, vc_keymap = _resolve_missing_by_conversion(localed_wrapper, x_layouts, vc_keymap)

return x_layouts, vc_keymap


def _resolve_missing_from_live(live_keyboard, x_layouts):

if not x_layouts:
return live_keyboard.read_keyboard_layouts()

return x_layouts


def _resolve_missing_by_conversion(localed_wrapper, x_layouts, vc_keymap):
if not vc_keymap:
vc_keymap = localed_wrapper.convert_layouts(x_layouts)
log.debug("Missing virtual console keymap value %s converted from %s X layouts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
from pyanaconda.modules.localization.installation import LanguageInstallationTask, \
KeyboardInstallationTask, write_vc_configuration, VC_CONF_FILE_PATH, write_x_configuration, \
X_CONF_DIR, X_CONF_FILE_NAME
from pyanaconda.modules.localization.live_keyboard import GnomeShellKeyboard, \
get_live_keyboard_instance
from pyanaconda.modules.localization.localization import LocalizationService
from pyanaconda.modules.localization.localed import LocaledWrapper
from pyanaconda.modules.localization.localization_interface import LocalizationInterface
Expand Down Expand Up @@ -657,6 +659,7 @@ def test_get_missing_keyboard_configuration_task(self, get_missing_mock):
def _get_missing_keyboard_configuration_test(self,
x_layouts,
converted_x_layouts,
live_x_layouts,
vc_keymap,
converted_vc_keymap,
result_x_layouts,
Expand All @@ -665,12 +668,20 @@ def _get_missing_keyboard_configuration_test(self,
localed.convert_keymap.return_value = converted_vc_keymap
localed.convert_layouts.return_value = converted_x_layouts

result = get_missing_keyboard_configuration(
localed,
x_layouts,
vc_keymap
)
assert result == (result_x_layouts, result_vc_keymap)
live_keyboard = Mock()
live_keyboard.read_keyboard_layouts.return_value = live_x_layouts

with patch("pyanaconda.modules.localization.utils.get_live_keyboard_instance") as \
get_live_keyboard_mock:

get_live_keyboard_mock.return_value = live_keyboard

result = get_missing_keyboard_configuration(
localed,
x_layouts,
vc_keymap
)
assert result == (result_x_layouts, result_vc_keymap)

def test_get_missing_keyboard_configuration(self):
"""Test the get_missing_keyboard_configuration."""
Expand All @@ -679,6 +690,7 @@ def test_get_missing_keyboard_configuration(self):
self._get_missing_keyboard_configuration_test(
x_layouts=[],
converted_x_layouts="",
live_x_layouts=[],
vc_keymap="",
converted_vc_keymap=[DEFAULT_KEYBOARD],
result_x_layouts=[DEFAULT_KEYBOARD],
Expand All @@ -688,6 +700,7 @@ def test_get_missing_keyboard_configuration(self):
self._get_missing_keyboard_configuration_test(
x_layouts=["cz (qwerty)"],
converted_x_layouts="cz-qwerty",
live_x_layouts=[],
vc_keymap="us",
converted_vc_keymap=["us"],
result_x_layouts=["cz (qwerty)"],
Expand All @@ -697,6 +710,7 @@ def test_get_missing_keyboard_configuration(self):
self._get_missing_keyboard_configuration_test(
x_layouts=["cz (qwerty)"],
converted_x_layouts="cz-qwerty",
live_x_layouts=[],
vc_keymap="",
converted_vc_keymap=[""],
result_x_layouts=["cz (qwerty)"],
Expand All @@ -706,11 +720,22 @@ def test_get_missing_keyboard_configuration(self):
self._get_missing_keyboard_configuration_test(
x_layouts=[],
converted_x_layouts="",
live_x_layouts=[],
vc_keymap="us",
converted_vc_keymap=["us"],
result_x_layouts=["us"],
result_vc_keymap="us",
)
# Take layouts from Live system (vc will be converted from Live layouts)
self._get_missing_keyboard_configuration_test(
x_layouts=[],
converted_x_layouts="cz",
live_x_layouts=["cz", "cz (qwerty)"],
vc_keymap="",
converted_vc_keymap=[""],
result_x_layouts=["cz", "cz (qwerty)"],
result_vc_keymap="cz",
)

@patch("pyanaconda.modules.localization.runtime.conf")
@patch("pyanaconda.modules.localization.runtime.try_to_load_keymap")
Expand Down Expand Up @@ -1088,3 +1113,72 @@ 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)


class LiveSystemKeyboardTestCase(unittest.TestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider doing this in a new test file matching the new module file.

This whole file could use some splitting anyway, but that's another independent PR...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do that I should also split localed test from this to keep consistency. However, not sure if it should be part of this PR (it is already big). Let's handle it as followup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - #5029.

@patch("pyanaconda.modules.localization.live_keyboard.conf")
def test_get_live_keyboard_instance(self, mocked_conf):
"""Test get_live_keyboard_instance function."""
mocked_conf.system.provides_liveuser = True
assert isinstance(get_live_keyboard_instance(), GnomeShellKeyboard)

mocked_conf.reset_mock()
mocked_conf.system.provides_liveuser = False
assert get_live_keyboard_instance() is None

def _check_gnome_shell_layouts_conversion(self, mocked_exec_with_capture, system_input, output):
mocked_exec_with_capture.reset_mock()
mocked_exec_with_capture.return_value = system_input

gs = GnomeShellKeyboard()

assert gs.read_keyboard_layouts() == output
mocked_exec_with_capture.assert_called_once_with(
"gsettings",
["get", "org.gnome.desktop.input-sources", "sources"]
)

@patch("pyanaconda.modules.localization.live_keyboard.execWithCaptureAsLiveUser")
def test_gnome_shell_keyboard(self, mocked_exec_with_capture):
"""Test GnomeShellKeyboard live instance."""
# test one simple layout set
self._check_gnome_shell_layouts_conversion(
mocked_exec_with_capture=mocked_exec_with_capture,
system_input=r"[('xkb', 'cz')]",
output=["cz"]
)

# test one complex layout is set
self._check_gnome_shell_layouts_conversion(
mocked_exec_with_capture=mocked_exec_with_capture,
system_input=r"[('xkb', 'cz+qwerty')]",
output=["cz (qwerty)"]
)

# test multiple layouts are set
self._check_gnome_shell_layouts_conversion(
mocked_exec_with_capture=mocked_exec_with_capture,
system_input=r"[('xkb', 'cz+qwerty'), ('xkb', 'us'), ('xkb', 'cz+dvorak-ucw')]",
output=["cz (qwerty)", "us", "cz (dvorak-ucw)"]
)

# test layouts with ibus (ibus is ignored)
self._check_gnome_shell_layouts_conversion(
mocked_exec_with_capture=mocked_exec_with_capture,
system_input=r"[('xkb', 'cz'), ('ibus', 'libpinyin')]",
output=["cz"]
)

# test only ibus layout
self._check_gnome_shell_layouts_conversion(
mocked_exec_with_capture=mocked_exec_with_capture,
system_input=r"[('ibus', 'libpinyin')]",
output=[]
)

# test wrong input
self._check_gnome_shell_layouts_conversion(
mocked_exec_with_capture=mocked_exec_with_capture,
system_input=r"wrong input",
output=[]
)