mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-06 08:55:34 +00:00
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
125 lines
4.8 KiB
Python
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()
|