From c717a25b9ae14e9c612eedcaa92f63a2a95a5630 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Fri, 18 Aug 2023 19:52:32 -0400 Subject: [PATCH] Copy input-sources from DConf to installed system Right now, a users input source configuration gets set up in the live environment and then gets lost. This commit adds some code to put it in the installed system in a system dconf database so that new users will pick it up. --- .../payload/live_image/installation.py | 79 ++++++++++++++++++- .../payloads/payload/live_os/live_os.py | 6 +- .../payload/test_module_payload_live_os.py | 45 ++++++++++- 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/pyanaconda/modules/payloads/payload/live_image/installation.py b/pyanaconda/modules/payloads/payload/live_image/installation.py index 92294d23d4a7..ffb1602bee07 100644 --- a/pyanaconda/modules/payloads/payload/live_image/installation.py +++ b/pyanaconda/modules/payloads/payload/live_image/installation.py @@ -21,12 +21,13 @@ import stat import requests import shutil +import subprocess import blivet.util from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.constants import NETWORK_CONNECTION_TIMEOUT from pyanaconda.core.i18n import _ -from pyanaconda.core.util import execWithRedirect, requests_session +from pyanaconda.core.util import execWithRedirect, startProgram, requests_session from pyanaconda.core.path import join_paths, make_directories from pyanaconda.core.string import lower_ascii from pyanaconda.modules.common.structures.live_image import LiveImageConfigurationData @@ -504,3 +505,79 @@ def run(self): log.debug("Copying %s to %s", path, destination_path) if os.path.exists(path): shutil.copy2(path, destination_path) + +class CopyTransientDConfInputSourcesTask(Task): + """Task to copy transient input source dconf data from live user to installed system""" + + def __init__(self, sysroot): + """Create a new task.""" + super().__init__() + self._sysroot = sysroot + self._paths = ['/org/gnome/desktop/input-sources/'] + self._dconf_dump_file = "/etc/dconf/db/distro.d/10-installer" + + @property + def name(self): + """Name of the task.""" + return "Export live user DConf input source config to installed system""" + + def _get_uid(self): + try: + return int(os.environ.get('PKEXEC_UID')) + except (TypeError, ValueError): + return 0 + + def run(self): + """Run the task.""" + destination_path = join_paths(self._sysroot, self._dconf_dump_file) + destination_dir = os.path.dirname(destination_path) + make_directories(destination_dir) + uid = self._get_uid() + + output_lines = [] + for path in self._paths: + log.debug("Exporting DConf settings from uid %d under %s", uid, path) + # TODO: Use instead execWithCaptureAsLiveUser or some variant of it + process = startProgram( + ["dconf", "dump", path], + stderr=subprocess.PIPE, + env_prune=["USER", "LOGNAME", "HOME"], + user=uid + ) + stdout, stderr = process.communicate() + + if process.returncode != 0: + log.error("dconf dump for %s failed: %s", path, stderr.decode('utf-8')) + continue + + lines = stdout.decode('utf-8').split('\n') + + # The group on first line is relative to the path passed, so ends up always being + # [/]. Rewrite it to [org/gnome/desktop/input-sources] or whatever was the file name. + if len(lines) > 1: + lines[0] = "[{}]".format(path.lstrip('/').rstrip('/')) + output_lines += lines + + if len(output_lines) < 2 : + # one line or less = nothing to copy + return + + try: + log.debug("Writing exported settings to: %s", destination_path) + with open(destination_path, 'w') as file: + file.write('\n'.join(output_lines)) + except IOError as e: + log.error("Failed to write dconf settings: %s", e) + return + + log.debug("Running dconf update on installed system") + process = startProgram( + ["dconf", "update"], + stderr=subprocess.PIPE, + root=self._sysroot + ) + + stdout, stderr = process.communicate() + + if process.returncode != 0: + log.error("dconf update failed: %s", stderr.decode('utf-8')) diff --git a/pyanaconda/modules/payloads/payload/live_os/live_os.py b/pyanaconda/modules/payloads/payload/live_os/live_os.py index 00773bbb1c38..e20b947a1c3b 100644 --- a/pyanaconda/modules/payloads/payload/live_os/live_os.py +++ b/pyanaconda/modules/payloads/payload/live_os/live_os.py @@ -21,7 +21,7 @@ from pyanaconda.modules.common.errors.payload import IncompatibleSourceError from pyanaconda.modules.payloads.constants import SourceType, PayloadType from pyanaconda.modules.payloads.payload.live_image.installation import InstallFromImageTask, \ - CopyTransientGnomeInitialSetupStateTask + CopyTransientGnomeInitialSetupStateTask, CopyTransientDConfInputSourcesTask from pyanaconda.modules.payloads.payload.live_os.utils import get_kernel_version_list from pyanaconda.modules.payloads.payload.payload_base import PayloadBase from pyanaconda.modules.payloads.payload.live_os.live_os_interface import LiveOSInterface @@ -84,6 +84,10 @@ def install_with_tasks(self): sysroot=conf.target.system_root, )] + tasks += [CopyTransientDConfInputSourcesTask( + sysroot=conf.target.system_root, + )] + return tasks def _update_kernel_version_list(self, image_source): diff --git a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_module_payload_live_os.py b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_module_payload_live_os.py index 29f8ba4d620d..4978d7ed3a04 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_module_payload_live_os.py +++ b/tests/unit_tests/pyanaconda_tests/modules/payloads/payload/test_module_payload_live_os.py @@ -20,6 +20,7 @@ import os import tempfile import unittest +from unittest.mock import patch, MagicMock from pyanaconda.core.constants import SOURCE_TYPE_LIVE_OS_IMAGE, PAYLOAD_TYPE_LIVE_OS from pyanaconda.core.path import join_paths, make_directories, touch @@ -28,7 +29,7 @@ from pyanaconda.modules.payloads.payload.live_os.live_os import LiveOSModule from pyanaconda.modules.payloads.payload.live_os.live_os_interface import LiveOSInterface from pyanaconda.modules.payloads.payload.live_image.installation import InstallFromImageTask, \ - CopyTransientGnomeInitialSetupStateTask + CopyTransientGnomeInitialSetupStateTask, CopyTransientDConfInputSourcesTask from tests.unit_tests.pyanaconda_tests import patch_dbus_publish_object from tests.unit_tests.pyanaconda_tests.modules.payloads.payload.module_payload_shared import \ @@ -107,9 +108,10 @@ def test_install_with_task(self): self.module.set_sources([source]) tasks = self.module.install_with_tasks() - assert len(tasks) == 2 + assert len(tasks) == 3 assert isinstance(tasks[0], InstallFromImageTask) assert isinstance(tasks[1], CopyTransientGnomeInitialSetupStateTask) + assert isinstance(tasks[2], CopyTransientDConfInputSourcesTask) def test_install_with_task_no_source(self): """Test Live OS install with tasks with no source fail.""" @@ -155,3 +157,42 @@ def test_transient_gis_task_missing(self): result_path = join_paths(newroot, mocked_path) assert not os.path.exists(result_path) + + def _make_process_result(self, retval, stdout, stderr): + proc = MagicMock() + proc.returncode = retval + proc.communicate.return_value=( + stdout.encode("utf-8"), + stderr.encode("utf-8"), + ) + return proc + + @patch("pyanaconda.modules.payloads.payload.live_image.installation.startProgram") + def test_transient_dconf_task_present(self, start_mock): + """Test copying transient dconf files when present""" + dconf_output = """\ +[/] +mru-sources=[('xkb', 'cz'), ('xkb', 'us')] +per-window=false +sources=[('xkb', 'cz'), ('xkb', 'us')] +xkb-options=['grp:win_space_toggle', 'lv3:ralt_switch'] +""" + start_mock.side_effect = [ + self._make_process_result(0, dconf_output, "dump error"), + self._make_process_result(0, "update output", "update error"), + ] + with tempfile.TemporaryDirectory() as newroot: + task = CopyTransientDConfInputSourcesTask(newroot) + task.run() + + assert start_mock.call_count == 2 + + result_path = join_paths(newroot, task._dconf_dump_file) + assert os.path.isfile(result_path) + + expected = dconf_output.splitlines(keepends=True) + # match rewriting to input file name + expected[0] = "[org/gnome/desktop/input-sources]\n" + + with open(result_path, "r") as f: + assert f.readlines() == expected