spiegel-keyman/linux/keyman-config/keyman_config/gsettings.py
Eberhard Beilharz 23a558d88e
feat(linux): Deal with temporary dbus session
When we launch dbus if it is not running, we're probably (un-)installing
keyboards for a different user. This is an unusual environment where not
everything is available, e.g. dconf-service won't be running and can't
be started because DISPLAY variable is not set. This means we can't
write gsettings values through the API. However, we can read them, and we
can write a keyfile and update the database. This is what this change
implements for the cases where we started dbus.
2024-03-22 12:54:50 +01:00

128 lines
5.1 KiB
Python

#!/usr/bin/python3
import configparser
import logging
import os
import subprocess
import sys
from xdg.BaseDirectory import xdg_config_home
from gi.repository import Gio # needs to come before gi.overrides.GLib!
from gi.overrides.GLib import Variant
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'),
f"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{os.environ.get('SUDO_UID')}/bus",
'gsettings', 'get', self.schema_id, key]
if sys.version_info.major <= 3 and sys.version_info.minor < 7:
# capture_output got added in Python 3.7
try:
output = subprocess.check_output(args)
value = eval(output)
except subprocess.CalledProcessError:
value = None
logging.warning('Could not convert to sources')
else:
process = subprocess.run(args, capture_output=True)
if process.returncode == 0:
output = process.stdout
if output.decode('utf-8').startswith('['):
value = eval(output)
return value
logging.debug('Could not convert to sources')
return None
else:
variant = self.schema.get_value(key)
value = self._convert_variant_to_array(variant)
return value
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)
subprocess.run(
['sudo', '-H', '-u', os.environ.get('SUDO_USER'),
f"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{os.environ.get('SUDO_UID')}/bus",
'gsettings', 'set', self.schema_id, key, variant], 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()