1
"""DBus utilities, especially for helping with asynchronous use of DBUS.
3
Notable things in this module:
5
- L{get_object} - Get a DBUS object which returns Deferreds.
6
- L{method} - Declare a server-side DBUS method responder that can return a
7
Deferred in order to delay the return of a DBUS method call.
9
Importing this module will call L{hack_dbus_get_object}.
15
from twisted.internet.defer import Deferred, execute
16
from twisted.python.failure import Failure
17
from twisted.python.reflect import namedClass
19
from dbus.service import Object, BusName, method as dbus_method
20
from dbus import Array, Byte
21
from dbus.exceptions import DBusException
24
from landscape.lib.log import log_failure
26
# These should perhaps be registered externally
27
SAFE_EXCEPTIONS = ["landscape.schema.InvalidError",
28
"landscape.broker.registration.InvalidCredentialsError"]
29
PYTHON_EXCEPTION_PREFIX = "org.freedesktop.DBus.Python."
31
class ServiceUnknownError(Exception):
32
"""Raised when the DBUS Service cannot be found."""
34
class SecurityError(Exception):
35
"""Raised when the DBUS Service is inaccessible."""
37
class NoReplyError(Exception):
38
"""Raised when a DBUS service doesn't respond."""
43
Convenience for creating dbus objects with a particular bus name and object
46
@cvar bus_name: The bus name to listen on.
47
@cvar object_path: The path to listen on.
49
def __init__(self, bus):
50
super(Object, self).__init__(
51
BusName(self.bus_name, bus), object_path=self.object_path)
54
def _method_reply_error(connection, message, exception):
55
if '_dbus_error_name' in exception.__dict__:
56
name = exception._dbus_error_name
57
elif exception.__class__.__module__ == '__main__':
58
name = 'org.freedesktop.DBus.Python.%s' % exception.__class__.__name__
60
name = 'org.freedesktop.DBus.Python.%s.%s' % (
61
exception.__class__.__module__, exception.__class__.__name__)
64
current_exception = sys.exc_info()[1]
65
if exception is current_exception:
66
contents = traceback.format_exc()
67
elif hasattr(exception, "_ls_traceback_string"):
68
contents = exception._ls_traceback_string
70
contents = ''.join(traceback.format_exception_only(exception.__class__,
72
# Explicitly jam the name on the front of the contents so that our error
74
contents = "%s: %s" % (name, contents)
77
if dbus.version[:2] < (0, 80):
78
from dbus import dbus_bindings
79
reply = dbus_bindings.Error(message, name, contents)
80
connection.send(reply)
83
reply = _dbus_bindings.ErrorMessage(message, name, contents)
84
connection.send_message(reply)
86
if dbus.version[:3] <= (0, 80, 2):
88
dbus.service._method_reply_error = _method_reply_error
91
def method(interface, **kwargs):
93
Factory for decorators used export methods of a L{dbus.service.Object}
94
to be exported on the D-Bus.
96
If a method returns a L{Deferred}, it will automatically be handled.
98
def decorator(function):
99
def inner(self, *args, **kwargs):
100
__cb = kwargs.pop("__cb")
101
__eb = kwargs.pop("__eb")
103
def callback(result):
104
# dbus can't serialize None; convert it to a return of 0
111
def errback(failure):
112
# An idea: If we ever want to be able to intentionally send
113
# exceptions to the other side and don't want to log them, we
114
# could have an exception type of which subclasses won't be
115
# logged, but only passed to __eb.
116
log_failure(failure, "Error while running DBUS method handler!")
117
exception = failure.value
118
exception._ls_traceback_string = failure.getTraceback()
121
# don't look. The intent of all of this is to allow failure.tb to
122
# be live by the time our errback is called. If we use
123
# maybeDeferred, the .tb will never be available in a callback.
125
d.addCallback(lambda ignored: function(self, *args, **kwargs))
126
d.addCallbacks(callback, errback)
129
byte_arrays = kwargs.pop("byte_arrays", False)
131
raise NotImplementedError(
132
"Please don't use byte_arrays; it doesn't work on old "
133
"versions of python-dbus.")
135
inner = dbus_method(interface, **kwargs)(inner)
136
# We don't pass async_callbacks to dbus_method, because it does some
137
# dumb introspection on arguments of the function. Basically it
138
# requires the callbacks to be declared as named parameters, instead of
139
# using **kw, which is problematic for us because we also want to take
140
# arbitrary arguments to pass on to the original function.
142
# To get around this we just set the internal attribute which
143
# dbus_method would set itself that specifies what the callback keyword
145
inner._dbus_async_callbacks = ("__cb", "__eb")
150
DBUS_CALL_TIMEOUT = 70
152
class AsynchronousProxyMethod(object):
154
A wrapper for L{dbus.proxies.ProxyMethod}s that causes calls
155
to return L{Deferred}s.
158
def __init__(self, wrapper, method_name, dbus_interface,
160
self.wrapper = wrapper
161
self.method_name = method_name
162
self._dbus_interface = dbus_interface
163
self._retry_timeout = retry_timeout
165
def __call__(self, *args, **kwargs):
167
self._call_with_retrying(result, args, kwargs)
168
result.addErrback(self._massage_errors)
171
def _massage_errors(self, failure):
172
"""Python DBUS has terrible exception reporting.
174
Convert many types of errors which into things which are
177
failure.trap(DBusException)
178
message = failure.getErrorMessage()
179
# handle different crappy error messages from various versions
181
if ("Did not receive a reply" in message
182
or "No reply within specified time" in message):
183
raise NoReplyError(message)
184
if (message.startswith("A security policy in place")
185
or message.startswith("org.freedesktop.DBus.Error.AccessDenied")):
186
raise SecurityError(message)
187
if "was not provided by any .service" in message:
188
raise ServiceUnknownError(message)
189
if "Could not get owner of name" in message:
190
raise ServiceUnknownError(message)
192
if message.startswith(PYTHON_EXCEPTION_PREFIX):
193
python_exception = message[len(PYTHON_EXCEPTION_PREFIX)
195
if python_exception in SAFE_EXCEPTIONS:
196
raise namedClass(python_exception)(message)
200
def _retry_on_failure(self, failure, result,
201
args, kwargs, first_failure_time):
202
failure.trap(DBusException)
203
failure_repr = str(failure) # Yay dbus. :-(
204
if (("org.freedesktop.DBus.Error.ServiceUnknown" in failure_repr or
205
"was not provided by any .service files" in failure_repr) and
206
self._retry_timeout > time.time() - first_failure_time):
207
from twisted.internet import reactor
208
reactor.callLater(0.1, self._call_with_retrying,
209
result, args, kwargs, first_failure_time)
211
result.errback(failure)
213
def _call_with_retrying(self, result, args, kwargs,
214
first_failure_time=None):
215
reset_cache = bool(first_failure_time)
216
if first_failure_time is None:
217
first_failure_time = time.time()
218
execute_result = execute(self.wrapper.get_object, reset_cache)
219
execute_result.addCallback(self._actually_call, result, args, kwargs,
221
execute_result.addErrback(self._retry_on_failure,
222
result, args, kwargs, first_failure_time)
224
def _actually_call(self, object, result, args, kwargs, first_failure_time):
225
if self.method_name in dir(object):
226
# It's a normal method call, such as connect_to_signal().
227
local_method = getattr(object, self.method_name)
228
result.callback(local_method(*args, **kwargs))
230
method = getattr(object, self.method_name)
231
def got_result(*result_args):
232
if len(result_args) == 1:
233
result.callback(result_args[0])
235
result.callback(result_args)
237
def got_error(exception):
238
failure = Failure(exception)
239
self._retry_on_failure(failure, result, args, kwargs,
241
kwargs["reply_handler"] = got_result
242
kwargs["error_handler"] = got_error
243
kwargs["dbus_interface"] = self._dbus_interface
245
method(*args, **kwargs)
249
class AsynchronousDBUSObjectWrapper(object):
251
A wrapper for L{dbus.proxies.ProxyObject}s which causes all method method
252
calls to return L{Deferred}s (by way of L{AsynchronousProxyMethod}).
254
def __init__(self, bus, bus_name, path, retry_timeout,
255
dbus_interface=None):
260
self._bus_name = bus_name
263
if dbus_interface is None:
264
dbus_interface = path.strip("/").replace("/", ".")
265
self._dbus_interface = dbus_interface
266
self._retry_timeout = retry_timeout
268
def get_object(self, reset_cache=False):
271
if self._object is None:
272
self._object = self._bus.get_object(self._bus_name, self._path,
276
def __getattr__(self, name):
278
Get a L{AsynchronousProxyMethod} wrapped around the original attribute
279
of the C{dbus_object}.
281
return AsynchronousProxyMethod(self, name, self._dbus_interface,
285
return "<AsynchronousDBUSObjectWrapper at 0x%x on %r>" % (
286
id(self), self.dbus_object)
290
"""Create a DBUS bus by name."""
291
bus_class_name = name.capitalize() + "Bus"
293
bus_class = getattr(dbus, bus_class_name)
294
except AttributeError:
295
raise ValueError("Invalid bus name: %r" % name)
299
def get_object(bus, bus_name, object_path, interface=None, retry_timeout=None):
300
"""Fetch a DBUS object on which all methods will be asynchronous."""
301
if retry_timeout is None:
302
retry_timeout = DBUS_CALL_TIMEOUT
303
return AsynchronousDBUSObjectWrapper(bus, bus_name, object_path,
304
retry_timeout, interface)
307
def hack_dbus_get_object():
309
Old versions of dbus did not support the 'introspect' argument to
310
Bus.get_object. This method installs a version that does.
312
def get_object(self, service_name, object_path, introspect=True):
313
return self.ProxyObjectClass(self, service_name,
314
object_path, introspect=introspect)
315
dbus.Bus.get_object = get_object
317
if dbus.version[:2] < (0, 80):
318
hack_dbus_get_object()
321
def byte_array(bytestring):
322
"""Convert a Python str to an L{Array} of L{Byte}s.
324
This should be used instead of L{dbus.ByteArray} because in old versions of
325
dbus it is not serialized properly.
327
return Array(Byte(ord(c)) for c in bytestring)
330
def array_to_string(array):
331
"""Convert an L{Array} of L{Byte}s (or integers) to a Python str.
334
# HOLY LORD dbus has horrible bugs
338
result.append(chr(item))
339
return "".join(result)