spiegel-keyman/linux/keyman-config/keyman_config/gsettings.py
Eberhard Beilharz 7574d2eb68
Merge pull request #14953 from keymanapp/fix/linux/14911_instshared
fix(linux): fix installing keyboard in shared area 🍲

Installing into the shared area (`/usr/local/share`) requires `root` permissions, so in order to install into the shared area the user has to call `km-package-install` with `sudo`. But we also want to add the keyboard to the user's list of available keyboards so that he can switch to it. That needs to be done in the user context. However, when `sudo` ran the command in the user context, the environment variables to connect to the user's session dbus were not set.

This change fixes this by passing `DBUS_SESSION_BUS_ADDRESS` along with the `sudo -u <user>` call. Since `root` doesn't have this variable set (or at least not pointing to the user's dbus), we construct the dbus address based on the user's uid.

On newer Ubuntu versions (>=25.04) we also have to set `XDG_RUNTIME_DIR`, at least when running the autopkgtests.

Fixes: #14911
2025-10-16 15:33:34 +02:00

125 lines
4.8 KiB
Python

#!/usr/bin/python3
import configparser
import logging
import os
import subprocess
import sys
from gi.repository import Gio # needs to come before gi.overrides.GLib!
from gi.overrides.GLib import Variant
from keyman_config.dbus_util import DBusSessionBusAddress, XdgRuntimeDir
# The xdg module got renamed to xdg_base_dirs, but the package
# (python3-xdg) on Ubuntu still ships xdg. So we support both.
try:
from xdg_base_dirs import xdg_config_home
except ModuleNotFoundError:
from xdg.BaseDirectory import xdg_config_home
from keyman_config import file_cleanup, get_dbus_started_for_session
class GSettings():
def __init__(self, schema_id):
"""
Wrapper around Gio Settings that deals with running under sudo
"""
self.schema_id = schema_id
if os.environ.get('SUDO_USER'):
self.is_sudo = True
logging.debug('Running with sudo. Real user: %s', os.environ.get('SUDO_USER'))
else:
self.schema = Gio.Settings.new(self.schema_id)
self.is_sudo = False
logging.debug('Running as regular user')
def _convert_variant_to_array(self, variant):
if variant is None:
return []
if variant.get_type_string() == 'as':
return variant.get_strv()
assert variant.get_type_string() == 'a(ss)'
values = []
# Process variant of type "a(ss)" (array of tuples with two strings)
nChildren = variant.n_children()
for i in range(nChildren):
# Process variant of type "(ss)" (tuple with two strings)
val = variant.get_child_value(i)
typeVariant = val.get_child_value(0)
idVariant = val.get_child_value(1)
values.append((typeVariant.get_string(), idVariant.get_string()))
return values
def _convert_array_to_variant(self, array, type_string):
if len(array) == 0:
return Variant(type_string, None)
if type_string == 'as':
return Variant('as', array)
assert type_string == 'a(ss)'
children = []
for (type_string, id) in array:
typeVariant = Variant.new_string(type_string)
idVariant = Variant.new_string(id)
child = Variant.new_tuple(typeVariant, idVariant)
children.append(child)
return Variant.new_array(None, children)
def get(self, key):
if self.is_sudo:
args = ['sudo', '-H', '-u', os.environ.get('SUDO_USER'),
DBusSessionBusAddress, XdgRuntimeDir,
'gsettings', 'get', self.schema_id, key]
process = subprocess.run(args, capture_output=True)
if process.returncode == 0:
output = process.stdout
if output.decode('utf-8').startswith('['):
return eval(output)
logging.debug('Could not convert to sources')
return None
else:
variant = self.schema.get_value(key)
return self._convert_variant_to_array(variant)
def _get_schema_path(self):
return self.schema_id.replace('.', '/')
def _set_key_file(self, key, value, type_string):
dconf_config_dir = os.path.join(xdg_config_home, 'dconf')
os.makedirs(os.path.join(dconf_config_dir, 'user.d'), exist_ok=True)
keyfile = file_cleanup.get('keyfile')
if not keyfile:
keyfile = os.path.join(dconf_config_dir, 'user.d', 'keyman-settings')
file_cleanup.register('keyfile', keyfile)
keyman_settings = configparser.ConfigParser()
keyman_settings.read(keyfile, encoding='utf-8')
keyman_settings[self._get_schema_path()] = {}
keyman_settings[self._get_schema_path()][key] = f'@{type_string} {value}'
with open(keyfile, mode='w', encoding='utf-8') as configfile:
keyman_settings.write(configfile)
# Update dconf database immediately for current dbus session, i.e.
# current process, so that further reads and writes will work.
# Cleanup, i.e. removal of the settings file, will be done when
# the process exits. With the next login the user will see the
# updated values.
subprocess.run(['dconf', 'update', dconf_config_dir], check=False)
def set(self, key, value, type_string) -> None:
if self.is_sudo:
variant = str(value)
args = ['sudo', '-H', '-u', os.environ.get('SUDO_USER'),
DBusSessionBusAddress, XdgRuntimeDir,
'gsettings', 'set', self.schema_id, key, variant]
subprocess.run(args, check=False)
elif get_dbus_started_for_session():
self._set_key_file(key, value, type_string)
else:
variant = self._convert_array_to_variant(value, type_string)
self.schema.set_value(key, variant)
self.schema.sync()