Skip to content

Commit

Permalink
Add class to easily implement DBus Interfaces
Browse files Browse the repository at this point in the history
In GLib, there is an Client Proxy you can use to access signals and
properties from an DBus interface, but nothing similar for the
Server-Side without generating code.

So this lays out an class implementers subclass, specify some
metadata and implement the methods that the interfaces uses.
  • Loading branch information
Neui committed Mar 26, 2021
1 parent d271be7 commit 93989df
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 0 deletions.
Empty file added GTG/core/dbus/__init__.py
Empty file.
149 changes: 149 additions & 0 deletions GTG/core/dbus/dbus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from gi.repository import GLib, Gio
from GTG.core.logger import log
import os
import traceback


def _get_installed_xml_from_interface_name(interface_name):
"""Return an DBus interface XML from system installed folders"""
raw_paths = GLib.get_system_data_dirs()
raw_paths.append(GLib.get_user_data_dir())
for path in [os.path.join(p, 'dbus-1', 'interfaces') for p in raw_paths]:
full_path = os.path.join(path, interface_name + ".xml")
try:
with open(full_path, 'rt') as f:
log.debug(f"Found file {full_path}")
return f.read()
except OSError:
pass
return None

def _get_internal_xml_from_interface_name(interface_name):
"""Return an DBus interface XML from internal data folders"""
log.debug("TODO: Implement this later")
pass

def get_xml_from_interface_name(interface_name, use_internal=True, use_system=True):
"""Return an DBus interface XML from either provided by the system (distro)
or internal"""
if use_internal:
xml = _get_internal_xml_from_interface_name(interface_name)
if xml is not None:
return xml
if use_system:
xml = _get_installed_xml_from_interface_name(interface_name)
if xml is not None:
return xml
return None


class DBusReturnError(Exception):
"""An error that an DBus interface implementation can throw to indicate
that it should return with that error"""
def __init__(self, name, message):
super().__init__(message)
self.name = str(name)
self.message = str(message)

class DBusInterfaceImplService():
INTERFACE_NAME = None
NODE_INFO = None
INTERFACE_INFO = None
_object_id = None
_dbus_connection = None

def __init__(self):
pass

def __get_interface_info_from_node_info(self):
for interface in self.NODE_INFO.interfaces:
if interface.name == self.INTERFACE_NAME:
return interface
return None

def __get_node_info(self):
xml = get_xml_from_interface_name(self.INTERFACE_NAME)
if xml is not None:
return Gio.DBusNodeInfo.new_for_xml(xml)
return None

def _get_info(self):
if self.NODE_INFO is None and self.INTERFACE_NAME is not None:
self.NODE_INFO = self.__get_node_info()
log.debug(f"Got node info for {self.INTERFACE_NAME}: {self.NODE_INFO}")
if self.NODE_INFO is not None and self.INTERFACE_INFO is None:
self.INTERFACE_INFO = self.__get_interface_info_from_node_info()
log.debug(f"Got interface info for {self.INTERFACE_NAME}: {self.INTERFACE_INFO}")

def dbus_register(self, dbus_connection, object_path):
"""Register this implementation on the specified connection and object"""
self._get_info()
self._object_id = dbus_connection.register_object(object_path,
self.INTERFACE_INFO,
self._handle_method_call,
self._handle_get_property, self._handle_set_property)
self._dbus_connection = dbus_connection
return self._object_id

def dbus_unregister(self):
"""Unregister this implementation"""
if self._dbus_connection is not None and self._object_id is not None:
log.debug(f"Unregister {self.INTERFACE_NAME} on {self._dbus_connection} via {self._object_id}")
ret = self._dbus_connection.unregister_object(self._object_id)
self._dbus_connection = None
self._object_id = None
if not ret:
log.warn(f"Unregister for {self.INTERFACE_NAME} failed!")
return ret
else:
log.warn(f"Trying to unregister not-registered {self.INTERFACE_NAME}")
return False

def _handle_method_call(self, connection,
sender, object_path,
interface_name, method_name,
parameters, invocation):
dbg = f"{interface_name}.{method_name}{parameters} on {object_path} by {sender}"
log.debug(f"Called {dbg}")

try:
method = self.__getattribute__(method_name)
except AttributeError as e:
log.debug(f"{dbg} → Internal python exception: {str(e)}")
invocation.return_dbus_error("python." + type(e).__name__, str(e))
return

try:
ret = method(*parameters.unpack())
except GLib.Error as e:
log.debug(f"{dbg} → GLib Error: {str(e)}")
invocation.return_gerror(e)
except DBusReturnError as e:
log.debug(f"{dbg} → Custom Error: {e.name, e.message}")
invocation.return_dbus_error(e.name, e.message)
except Exception as e:
log.warn(f"{dbg} → Python exception: {str(e)}")
traceback.print_exc()
invocation.return_dbus_error("python." + type(e).__name__, str(e))
else:
if type(ret) != GLib.Variant:
if len(invocation.get_method_info().out_args) == 1:
ret = (ret,) # Make one-value return work as expected
ret = GLib.Variant(self.__get_return_variant(invocation), ret)
log.debug(f"{dbg} → Returning {ret}")
invocation.return_value(ret)

def _handle_get_property(self, *args):
"""Get a DBus-available property"""
log.debug(f"TODO: Handle get property: {args}") # Not used currently
return None

def _handle_set_property(self, *args):
"""Set a DBus-available property"""
log.debug(f"TODO: Handle set property: {args}") # Not used currently
return None

def __get_return_variant(self, invocation):
"""Get Variant Type string that should be returned for a DBus method"""
info = invocation.get_method_info()
return '(' + "".join([arg.signature for arg in info.out_args]) + ')'
6 changes: 6 additions & 0 deletions GTG/core/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,11 @@ gtg_core_plugin_sources = [
'plugins/engine.py',
]

gtg_core_dbus_sources = [
'dbus/__init__.py',
'dbus/dbus.py',
]

python3.install_sources(gtg_core_sources, subdir: 'GTG' / 'core', pure: true)
python3.install_sources(gtg_core_plugin_sources, subdir: 'GTG' / 'core' / 'plugins', pure: true)
python3.install_sources(gtg_core_dbus_sources, subdir: 'GTG' / 'core' / 'dbus', pure: true)
2 changes: 2 additions & 0 deletions scripts/debug.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export XDG_DATA_HOME="$PWD/tmp/$dataset/xdg/data"
export XDG_CACHE_HOME="$PWD/tmp/$dataset/xdg/cache"
export XDG_CONFIG_HOME="$PWD/tmp/$dataset/xdg/config"

export XDG_DATA_DIRS="$PWD/.local_build/install/share:$XDG_DATA_DIRS"

# Title has to be passed to GTG directly, not through $args
# title could be more word, and only the first word would be taken
if [[ "$title" = "" ]]; then
Expand Down

0 comments on commit 93989df

Please sign in to comment.