# Copyright (C) 2010, Nokia # Copyright (C) 2019, Sam Thursfield # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # """ Sandbox environment for running tests. The sandbox is essentially a private D-Bus daemon. """ from gi.repository import Gio import atexit import logging import os import signal import subprocess from . import dbusdaemon from .dconf import DConfClient, TemporaryDConfProfile from . import psutil_mini as psutil log = logging.getLogger(__name__) TRACKER_DBUS_PREFIX = 'org.freedesktop.LocalSearch3' TRACKER_MINER_FS_BUSNAME = 'org.freedesktop.LocalSearch3' _process_list = [] def _cleanup_processes(): for process in _process_list: log.debug("helpers._cleanup_processes: stopping %s", process) process.stop() atexit.register(_cleanup_processes) class TrackerSandbox: """ Run Tracker daemons isolated from the real user session. The primary method of sandboxing is running one or more private D-Bus daemons, which take place of the host's session and system bus. """ def __init__(self, session_bus_config_file, system_bus_config_file=None, extra_env=None): self.extra_env = extra_env or {} self.session_bus = dbusdaemon.DBusDaemon( name='sandbox-session-bus', config_file=session_bus_config_file) if system_bus_config_file: self.system_bus = dbusdaemon.DBusDaemon( name='sandbox-system-bus', config_file=system_bus_config_file) else: self.system_bus = None self._dconf_profile = TemporaryDConfProfile() def get_environment(self): env = os.environ env.update(self.extra_env) env['G_MESSAGES_PREFIXED'] = 'all' # This avoids an issue where gvfsd-fuse can start up while the bus is # shutting down. If it fails to connect to the bus, it continues to # run anyway which leads to our dbus-daemon failing to shut down. # # Since https://gitlab.gnome.org/GNOME/gvfs/issues/323 was implemented # in GVFS 1.42 this problem may have gone away. env['GVFS_DISABLE_FUSE'] = '1' # Precreate runtime dir, to avoid this warning from dbus-daemon: # # Unable to set up transient service directory: XDG_RUNTIME_DIR "/home/sam/tracker-tests/tmp_59i3ev1/run" not available: No such file or directory # xdg_runtime_dir = env.get('XDG_RUNTIME_DIR') if xdg_runtime_dir: os.makedirs(xdg_runtime_dir, exist_ok=True) # Separate DConf configuration from host in all cases. env['DCONF_PROFILE'] = self._dconf_profile.get_path() def start(self, new_session=False): if self.system_bus: log.info("Starting D-Bus system bus for sandbox.") log.debug("Added environment variables: %s", self.extra_env) self.system_bus.start(env=self.get_environment(), new_session=new_session) self.extra_env['DBUS_SYSTEM_BUS_ADDRESS'] = self.system_bus.get_address() log.info("Starting D-Bus session bus for sandbox.") log.debug("Added environment variables: %s", self.extra_env) self.session_bus.start(env=self.get_environment(), new_session=new_session) def stop(self): tracker_processes = [] log.info("Looking for active Tracker processes on the session bus") for busname in self.session_bus.list_names_sync(): if busname.startswith(TRACKER_DBUS_PREFIX): log.info("Terminating '%s'", busname) pid = self.session_bus.get_connection_unix_process_id_sync(busname) if pid is not None: os.kill(pid, signal.SIGTERM) self.session_bus.await_bus_name(busname, acquired=False) log.info("Stopping D-Bus session bus for sandbox.") self.session_bus.stop() if self.system_bus: log.info("Stopping D-Bus system bus for sandbox.") self.system_bus.stop() def stop_daemon(self, busname): """Stops the daemon that owns 'busname'. This can be used if you want to force the miner-fs to exit, for example. """ log.info("Stopping daemon process that owns %s.", busname) pid = self.session_bus.get_connection_unix_process_id_sync(busname) if pid: os.kill(pid, signal.SIGTERM) psutil.wait_pid(pid) log.info("Process %i has stopped.", pid) else: log.info("Couldn't find a process owning %s.", busname) def get_session_bus_connection(self): """Return a Gio.BusConnection to the sandbox D-Bus session bus.""" return self.session_bus.get_connection() def get_system_bus_connection(self): """Return a Gio.BusConnection to the sandbox D-Bus system bus.""" return self.system_bus.get_connection() def get_session_bus_address(self): return self.session_bus.get_address() def get_dconf_client(self): return DConfClient( self.extra_env, self.get_session_bus_address() ) def set_config(self, schema_config_dict): """Set config values in multiple GSettings schemas. Example input: set_all({ 'org.freedesktop.Tracker3.Miner.Files': { 'enable-writeback': GLib.Variant.new_boolean(False), } }) """ dconf_client = self.get_dconf_client() for schema_name, contents in schema_config_dict.items(): for key, value in contents.items(): dconf_client.write(schema_name, key, value)