Mercurial > libervia-backend
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)) |