diff options
Diffstat (limited to 'utils/trackertestutils/dbusdaemon.py')
-rw-r--r-- | utils/trackertestutils/dbusdaemon.py | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/utils/trackertestutils/dbusdaemon.py b/utils/trackertestutils/dbusdaemon.py new file mode 100644 index 000000000..c7e4707f3 --- /dev/null +++ b/utils/trackertestutils/dbusdaemon.py @@ -0,0 +1,222 @@ +# Copyright (C) 2018,2019, Sam Thursfield <sam@afuera.me.uk> +# +# 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. + + +from gi.repository import Gio +from gi.repository import GLib + +import logging +import os +import signal +import subprocess +import threading + +log = logging.getLogger(__name__) +dbus_stderr_log = logging.getLogger(__name__ + '.stderr') +dbus_stdout_log = logging.getLogger(__name__ + '.stdout') + + +class DaemonNotStartedError(Exception): + pass + + +class DBusDaemon: + """The private D-Bus instance that provides the sandbox's session bus. + + We support reading and writing the session information to a file. This + means that if the user runs two sandbox instances on the same data + directory at the same time, they will share the same message bus. + + """ + + def __init__(self, session_file=None): + self.session_file = session_file + self.existing_session = False + self.process = None + + self.address = None + self.pid = None + + self._gdbus_connection = None + self._previous_sigterm_handler = None + + self._threads = [] + + if session_file: + try: + self.address, self.pid = self.read_session_file(session_file) + self.existing_session = True + except FileNotFoundError: + log.debug("No existing D-Bus session file was found.") + + def get_session_file(self): + """Returns the path to the session file if we created it, or None.""" + if self.existing_session: + return None + return self.session_file + + def get_address(self): + if self.address is None: + raise DaemonNotStartedError() + return self.address + + def get_connection(self): + if self._gdbus_connection is None: + raise DaemonNotStartedError() + return self._gdbus_connection + + @staticmethod + def read_session_file(session_file): + with open(session_file, 'r') as f: + content = f.read() + + try: + address = content.splitlines()[0] + pid = int(content.splitlines()[1]) + except ValueError: + raise RuntimeError(f"D-Bus session file {session_file} is not valid. " + "Remove this file to start a new session.") + + return address, pid + + @staticmethod + def write_session_file(session_file, address, pid): + os.makedirs(os.path.dirname(session_file), exist_ok=True) + + content = '%s\n%s' % (address, pid) + with open(session_file, 'w') as f: + f.write(content) + + def start_if_needed(self, config_file=None, env=None): + if self.existing_session: + log.debug('Using existing D-Bus session from file "%s" with address "%s"' + ' with PID %d' % (self.session_file, self.address, self.pid)) + else: + dbus_command = ['dbus-daemon', '--print-address=1', '--print-pid=1'] + if config_file: + dbus_command += ['--config-file=' + config_file] + else: + dbus_command += ['--session'] + log.debug("Running: %s", dbus_command) + self.process = subprocess.Popen( + dbus_command, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + self._previous_sigterm_handler = signal.signal( + signal.SIGTERM, self._sigterm_handler) + + try: + self.address = self.process.stdout.readline().strip().decode('ascii') + self.pid = int(self.process.stdout.readline().strip().decode('ascii')) + except ValueError: + error = self.process.stderr.read().strip().decode('unicode-escape') + raise RuntimeError(f"Failed to start D-Bus daemon.\n{error}") + + log.debug("Using new D-Bus session with address '%s' with PID %d", + self.address, self.pid) + + if self.session_file: + self.write_session_file(self.session_file, self.address, self.pid) + log.debug("Wrote D-Bus session file at %s", self.session_file) + + # We must read from the pipes continuously, otherwise the daemon + # process will block. + self._threads=[threading.Thread(target=self.pipe_to_log, args=(self.process.stdout, dbus_stdout_log), daemon=True), + threading.Thread(target=self.pipe_to_log, args=(self.process.stderr, dbus_stdout_log), daemon=True)] + self._threads[0].start() + self._threads[1].start() + + self._gdbus_connection = Gio.DBusConnection.new_for_address_sync( + self.address, + Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | + Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION, None, None) + + log.debug("Pinging the new D-Bus daemon...") + self.ping_sync() + + def stop(self): + if self.process: + log.debug(" Stopping DBus daemon") + self.process.terminate() + self.process.wait() + self.process = None + if len(self._threads) > 0: + log.debug(" Stopping %i pipe reader threads", len(self._threads)) + for thread in self._threads: + thread.join() + self.threads = [] + if self._previous_sigterm_handler: + signal.signal(signal.SIGTERM, self._previous_sigterm_handler) + self._previous_sigterm_handler = None + + def pipe_to_log(self, pipe, dbuslog): + """This function processes the output from our dbus-daemon instance.""" + while True: + line_raw = pipe.readline() + + if len(line_raw) == 0: + break + + line = line_raw.decode('utf-8').rstrip() + + if line.startswith('(tracker-'): + # We set G_MESSAGES_PREFIXED=all, meaning that all log messages + # output by Tracker processes have a prefix. Note that + # g_print() will NOT be captured here. + dbuslog.info(line) + else: + # Log messages from other daemons, including the dbus-daemon + # itself, go here. Any g_print() messages also end up here. + dbuslog.debug(line) + log.debug("Thread stopped") + + # I'm not sure why this is needed, or if it's correct, but without it + # we see warnings like this: + # + # ResourceWarning: unclosed file <_io.BufferedReader name=3> + pipe.close() + + def _sigterm_handler(self, signal, frame): + log.info("Received signal %s", signal) + self.stop() + + def ping_sync(self): + """Call the daemon Ping() method to check that it is alive.""" + self._gdbus_connection.call_sync( + 'org.freedesktop.DBus', '/', 'org.freedesktop.DBus', 'GetId', + None, None, Gio.DBusCallFlags.NONE, 10000, None) + + def list_names_sync(self): + """Get the name of every client connected to the bus.""" + conn = self.get_connection() + result = conn.call_sync('org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', 'ListNames', None, + GLib.VariantType('(as)'), + Gio.DBusCallFlags.NONE, -1, None) + return result[0] + + def get_connection_unix_process_id_sync(self, name): + """Get the process ID for one of the names connected to the bus.""" + conn = self.get_connection() + result = conn.call_sync('org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', + 'GetConnectionUnixProcessID', + GLib.Variant('(s)', [name]), + GLib.VariantType('(u)'), + Gio.DBusCallFlags.NONE, -1, None) + return result[0] |