Skip to content

Commit

Permalink
Copy input-sources from DConf to installed system
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
halfline authored and VladimirSlavik committed Sep 4, 2023
1 parent 980bde4 commit c717a25
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 4 deletions.
79 changes: 78 additions & 1 deletion pyanaconda/modules/payloads/payload/live_image/installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'))
6 changes: 5 additions & 1 deletion pyanaconda/modules/payloads/payload/live_os/live_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 \
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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

0 comments on commit c717a25

Please sign in to comment.