comparison sat/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py @ 3539:60d3861e5996

bridge (dbus): use Tx DBus for backend part of D-Bus bridge: Due to recent SQLAlchemy integration, Libervia is now using AsyncIO loop exclusively as main loop, thus GLib's one can't be used anymore (event if it could be in a separate thread). Furthermore Python-DBus is known to have design flaws mentioned even in the official documentation. Tx DBus is now used to replace Python-DBus, but only for the backend for now, as it will need some work on the frontend before we can get completely rid of it.
author Goffi <goffi@goffi.org>
date Thu, 03 Jun 2021 15:21:43 +0200
parents 7550ae9cfbac
children 524856bd7b19
comparison
equal deleted inserted replaced
3538:c605a0d6506f 3539:60d3861e5996
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 # SàT communication bridge 3 # Libervia communication bridge
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) 4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
5 5
6 # This program is free software: you can redistribute it and/or modify 6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by 7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or 8 # the Free Software Foundation, either version 3 of the License, or
14 # GNU Affero General Public License for more details. 14 # GNU Affero General Public License for more details.
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 from types import MethodType
20 from functools import partialmethod
21 from twisted.internet import defer, reactor
19 from sat.core.i18n import _ 22 from sat.core.i18n import _
20 import dbus
21 import dbus.service
22 import dbus.mainloop.glib
23 import inspect
24 from sat.core.log import getLogger 23 from sat.core.log import getLogger
24 from sat.core.exceptions import BridgeInitError
25 from sat.tools import config 25 from sat.tools import config
26 from twisted.internet.defer import Deferred 26 from txdbus import client, objects, error
27 from sat.core.exceptions import BridgeInitError 27 from txdbus.interface import DBusInterface, Method, Signal
28 28
29 29
30 log = getLogger(__name__) 30 log = getLogger(__name__)
31 31
32 # Interface prefix 32 # Interface prefix
43 43
44 class ParseError(Exception): 44 class ParseError(Exception):
45 pass 45 pass
46 46
47 47
48 class MethodNotRegistered(dbus.DBusException): 48 class DBusException(Exception):
49 _dbus_error_name = const_ERROR_PREFIX + ".MethodNotRegistered" 49 pass
50 50
51 51
52 class InternalError(dbus.DBusException): 52 class MethodNotRegistered(DBusException):
53 _dbus_error_name = const_ERROR_PREFIX + ".InternalError" 53 dbusErrorName = const_ERROR_PREFIX + ".MethodNotRegistered"
54 54
55 55
56 class AsyncNotDeferred(dbus.DBusException): 56 class GenericException(DBusException):
57 _dbus_error_name = const_ERROR_PREFIX + ".AsyncNotDeferred"
58
59
60 class DeferredNotAsync(dbus.DBusException):
61 _dbus_error_name = const_ERROR_PREFIX + ".DeferredNotAsync"
62
63
64 class GenericException(dbus.DBusException):
65 def __init__(self, twisted_error): 57 def __init__(self, twisted_error):
66 """ 58 """
67 59
68 @param twisted_error (Failure): instance of twisted Failure 60 @param twisted_error (Failure): instance of twisted Failure
69 @return: DBusException 61 error message is used to store a repr of message and condition in a tuple,
62 so it can be evaluated by the frontend bridge.
70 """ 63 """
71 super(GenericException, self).__init__()
72 try: 64 try:
73 # twisted_error.value is a class 65 # twisted_error.value is a class
74 class_ = twisted_error.value().__class__ 66 class_ = twisted_error.value().__class__
75 except TypeError: 67 except TypeError:
76 # twisted_error.value is an instance 68 # twisted_error.value is an instance
77 class_ = twisted_error.value.__class__ 69 class_ = twisted_error.value.__class__
78 message = twisted_error.getErrorMessage() 70 data = twisted_error.getErrorMessage()
79 try: 71 try:
80 self.args = (message, twisted_error.value.condition) 72 data = (data, twisted_error.value.condition)
81 except AttributeError: 73 except AttributeError:
82 self.args = (message,) 74 data = (data,)
83 self._dbus_error_name = ".".join( 75 else:
84 [const_ERROR_PREFIX, class_.__module__, class_.__name__] 76 data = (str(twisted_error),)
77 self.dbusErrorName = ".".join(
78 (const_ERROR_PREFIX, class_.__module__, class_.__name__)
85 ) 79 )
80 super(GenericException, self).__init__(repr(data))
81
82 @classmethod
83 def create_and_raise(cls, exc):
84 raise cls(exc)
86 85
87 86
88 class DbusObject(dbus.service.Object): 87 class DBusObject(objects.DBusObject):
89 def __init__(self, bus, path): 88
90 dbus.service.Object.__init__(self, bus, path) 89 core_iface = DBusInterface(
91 log.debug("Init DbusObject...") 90 const_INT_PREFIX + const_CORE_SUFFIX,
91 ##METHODS_DECLARATIONS_PART##
92 ##SIGNALS_DECLARATIONS_PART##
93 )
94 plugin_iface = DBusInterface(
95 const_INT_PREFIX + const_PLUGIN_SUFFIX
96 )
97
98 dbusInterfaces = [core_iface, plugin_iface]
99
100 def __init__(self, path):
101 super().__init__(path)
102 log.debug("Init DBusObject...")
92 self.cb = {} 103 self.cb = {}
93 104
94 def register_method(self, name, cb): 105 def register_method(self, name, cb):
95 self.cb[name] = cb 106 self.cb[name] = cb
96 107
97 def _callback(self, name, *args, **kwargs): 108 def _callback(self, name, *args, **kwargs):
98 """call the callback if it exists, raise an exception else 109 """Call the callback if it exists, raise an exception else"""
99 if the callback return a deferred, use async methods""" 110 try:
100 if not name in self.cb: 111 cb = self.cb[name]
112 except KeyError:
101 raise MethodNotRegistered 113 raise MethodNotRegistered
114 else:
115 d = defer.maybeDeferred(cb, *args, **kwargs)
116 d.addErrback(GenericException.create_and_raise)
117 return d
102 118
103 if "callback" in kwargs: 119 ##METHODS_PART##
104 # we must have errback too
105 if not "errback" in kwargs:
106 log.error("errback is missing in method call [%s]" % name)
107 raise InternalError
108 callback = kwargs.pop("callback")
109 errback = kwargs.pop("errback")
110 async_ = True
111 else:
112 async_ = False
113 result = self.cb[name](*args, **kwargs)
114 if async_:
115 if not isinstance(result, Deferred):
116 log.error("Asynchronous method [%s] does not return a Deferred." % name)
117 raise AsyncNotDeferred
118 result.addCallback(
119 lambda result: callback() if result is None else callback(result)
120 )
121 result.addErrback(lambda err: errback(GenericException(err)))
122 else:
123 if isinstance(result, Deferred):
124 log.error("Synchronous method [%s] return a Deferred." % name)
125 raise DeferredNotAsync
126 return result
127 120
128 ### signals ### 121 class Bridge:
129 122
130 @dbus.service.signal(const_INT_PREFIX + const_PLUGIN_SUFFIX, signature="") 123 def __init__(self):
131 def dummySignal(self): 124 log.info("Init DBus...")
132 # FIXME: workaround for addSignal (doesn't work if one method doensn't 125 self._obj = DBusObject(const_OBJ_PATH)
133 # already exist for plugins), probably missing some initialisation, need 126
134 # further investigations 127 async def postInit(self):
135 pass 128 try:
129 conn = await client.connect(reactor)
130 except error.DBusException as e:
131 if e.errName == "org.freedesktop.DBus.Error.NotSupported":
132 log.error(
133 _(
134 "D-Bus is not launched, please see README to see instructions on "
135 "how to launch it"
136 )
137 )
138 raise BridgeInitError(str(e))
139
140 conn.exportObject(self._obj)
141 await conn.requestBusName(const_INT_PREFIX)
136 142
137 ##SIGNALS_PART## 143 ##SIGNALS_PART##
138 ### methods ### 144 def register_method(self, name, callback):
145 log.debug(f"registering DBus bridge method [{name}]")
146 self._obj.register_method(name, callback)
139 147
140 ##METHODS_PART## 148 def emitSignal(self, name, *args):
141 def __attributes(self, in_sign): 149 self._obj.emitSignal(name, *args)
142 """Return arguments to user given a in_sign
143 @param in_sign: in_sign in the short form (using s,a,i,b etc)
144 @return: list of arguments that correspond to a in_sign (e.g.: "sss" return "arg1, arg2, arg3")"""
145 i = 0
146 idx = 0
147 attr = []
148 while i < len(in_sign):
149 if in_sign[i] not in ["b", "y", "n", "i", "x", "q", "u", "t", "d", "s", "a"]:
150 raise ParseError("Unmanaged attribute type [%c]" % in_sign[i])
151 150
152 attr.append("arg_%i" % idx) 151 def addMethod(
153 idx += 1 152 self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={}
154 153 ):
155 if in_sign[i] == "a": 154 """Dynamically add a method to D-Bus Bridge"""
156 i += 1 155 # FIXME: doc parameter is kept only temporary, the time to remove it from calls
157 if ( 156 log.debug(f"Adding method {name!r} to D-Bus bridge")
158 in_sign[i] != "{" and in_sign[i] != "(" 157 self._obj.plugin_iface.addMethod(
159 ): # FIXME: must manage tuples out of arrays 158 Method(name, arguments=in_sign, returns=out_sign)
160 i += 1
161 continue # we have a simple type for the array
162 opening_car = in_sign[i]
163 assert opening_car in ["{", "("]
164 closing_car = "}" if opening_car == "{" else ")"
165 opening_count = 1
166 while True: # we have a dict or a list of tuples
167 i += 1
168 if i >= len(in_sign):
169 raise ParseError("missing }")
170 if in_sign[i] == opening_car:
171 opening_count += 1
172 if in_sign[i] == closing_car:
173 opening_count -= 1
174 if opening_count == 0:
175 break
176 i += 1
177 return attr
178
179 def addMethod(self, name, int_suffix, in_sign, out_sign, method, async_=False):
180 """Dynamically add a method to Dbus Bridge"""
181 inspect_args = inspect.getfullargspec(method)
182
183 _arguments = inspect_args.args
184 _defaults = list(inspect_args.defaults or [])
185
186 if inspect.ismethod(method):
187 # if we have a method, we don't want the first argument (usually 'self')
188 del (_arguments[0])
189
190 # first arguments are for the _callback method
191 arguments_callback = ", ".join(
192 [repr(name)]
193 + (
194 (_arguments + ["callback=callback", "errback=errback"])
195 if async_
196 else _arguments
197 )
198 ) 159 )
199 160 # we have to create a method here instead of using partialmethod, because txdbus
200 if async_: 161 # uses __func__ which doesn't work with partialmethod
201 _arguments.extend(["callback", "errback"]) 162 def caller(self_, *args, **kwargs):
202 _defaults.extend([None, None]) 163 return self_._callback(name, *args, **kwargs)
203 164 setattr(self._obj, f"dbus_{name}", MethodType(caller, self._obj))
204 # now we create a second list with default values
205 for i in range(1, len(_defaults) + 1):
206 _arguments[-i] = "%s = %s" % (_arguments[-i], repr(_defaults[-i]))
207
208 arguments_defaults = ", ".join(_arguments)
209
210 code = compile(
211 "def %(name)s (self,%(arguments_defaults)s): return self._callback(%(arguments_callback)s)"
212 % {
213 "name": name,
214 "arguments_defaults": arguments_defaults,
215 "arguments_callback": arguments_callback,
216 },
217 "<DBus bridge>",
218 "exec",
219 )
220 exec(code) # FIXME: to the same thing in a cleaner way, without compile/exec
221 method = locals()[name]
222 async_callbacks = ("callback", "errback") if async_ else None
223 setattr(
224 DbusObject,
225 name,
226 dbus.service.method(
227 const_INT_PREFIX + int_suffix,
228 in_signature=in_sign,
229 out_signature=out_sign,
230 async_callbacks=async_callbacks,
231 )(method),
232 )
233 function = getattr(self, name)
234 func_table = self._dbus_class_table[
235 self.__class__.__module__ + "." + self.__class__.__name__
236 ][function._dbus_interface]
237 func_table[function.__name__] = function # Needed for introspection
238
239 def addSignal(self, name, int_suffix, signature, doc={}):
240 """Dynamically add a signal to Dbus Bridge"""
241 attributes = ", ".join(self.__attributes(signature))
242 # TODO: use doc parameter to name attributes
243
244 # code = compile ('def '+name+' (self,'+attributes+'): log.debug ("'+name+' signal")', '<DBus bridge>','exec') #XXX: the log.debug is too annoying with xmllog
245 code = compile(
246 "def " + name + " (self," + attributes + "): pass", "<DBus bridge>", "exec"
247 )
248 exec(code)
249 signal = locals()[name]
250 setattr(
251 DbusObject,
252 name,
253 dbus.service.signal(const_INT_PREFIX + int_suffix, signature=signature)(
254 signal
255 ),
256 )
257 function = getattr(self, name)
258 func_table = self._dbus_class_table[
259 self.__class__.__module__ + "." + self.__class__.__name__
260 ][function._dbus_interface]
261 func_table[function.__name__] = function # Needed for introspection
262
263
264 class Bridge(object):
265 def __init__(self):
266 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
267 log.info("Init DBus...")
268 try:
269 self.session_bus = dbus.SessionBus()
270 except dbus.DBusException as e:
271 if e._dbus_error_name == "org.freedesktop.DBus.Error.NotSupported":
272 log.error(
273 _(
274 "D-Bus is not launched, please see README to see instructions on how to launch it"
275 )
276 )
277 raise BridgeInitError
278 self.dbus_name = dbus.service.BusName(const_INT_PREFIX, self.session_bus)
279 self.dbus_bridge = DbusObject(self.session_bus, const_OBJ_PATH)
280
281 ##SIGNAL_DIRECT_CALLS_PART##
282 def register_method(self, name, callback):
283 log.debug("registering DBus bridge method [%s]" % name)
284 self.dbus_bridge.register_method(name, callback)
285
286 def addMethod(self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={}):
287 """Dynamically add a method to Dbus Bridge"""
288 # FIXME: doc parameter is kept only temporary, the time to remove it from calls
289 log.debug("Adding method [%s] to DBus bridge" % name)
290 self.dbus_bridge.addMethod(name, int_suffix, in_sign, out_sign, method, async_)
291 self.register_method(name, method) 165 self.register_method(name, method)
292 166
293 def addSignal(self, name, int_suffix, signature, doc={}): 167 def addSignal(self, name, int_suffix, signature, doc={}):
294 self.dbus_bridge.addSignal(name, int_suffix, signature, doc) 168 """Dynamically add a signal to D-Bus Bridge"""
295 setattr(Bridge, name, getattr(self.dbus_bridge, name)) 169 log.debug(f"Adding signal {name!r} to D-Bus bridge")
170 self._obj.plugin_iface.addSignal(Signal(name, signature))
171 setattr(Bridge, name, partialmethod(Bridge.emitSignal, name))