~ahasenack/landscape-client/landscape-client-1.5.5-0ubuntu0.9.04.0

« back to all changes in this revision

Viewing changes to landscape/lib/dbus_util.py

  • Committer: Bazaar Package Importer
  • Author(s): Rick Clark
  • Date: 2008-09-08 16:35:57 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080908163557-l3ixzj5dxz37wnw2
Tags: 1.0.18-0ubuntu1
New upstream release 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""DBus utilities, especially for helping with asynchronous use of DBUS.
 
2
 
 
3
Notable things in this module:
 
4
 
 
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.
 
8
 
 
9
Importing this module will call L{hack_dbus_get_object}.
 
10
"""
 
11
import sys
 
12
import time
 
13
import traceback
 
14
 
 
15
from twisted.internet.defer import Deferred, execute
 
16
from twisted.python.failure import Failure
 
17
from twisted.python.reflect import namedClass
 
18
 
 
19
from dbus.service import Object, BusName, method as dbus_method
 
20
from dbus import Array, Byte
 
21
from dbus.exceptions import DBusException
 
22
import dbus
 
23
 
 
24
from landscape.lib.log import log_failure
 
25
 
 
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."
 
30
 
 
31
class ServiceUnknownError(Exception):
 
32
    """Raised when the DBUS Service cannot be found."""
 
33
 
 
34
class SecurityError(Exception):
 
35
    """Raised when the DBUS Service is inaccessible."""
 
36
 
 
37
class NoReplyError(Exception):
 
38
    """Raised when a DBUS service doesn't respond."""
 
39
 
 
40
 
 
41
class Object(Object):
 
42
    """
 
43
    Convenience for creating dbus objects with a particular bus name and object
 
44
    path.
 
45
 
 
46
    @cvar bus_name: The bus name to listen on.
 
47
    @cvar object_path: The path to listen on.
 
48
    """
 
49
    def __init__(self, bus):
 
50
        super(Object, self).__init__(
 
51
            BusName(self.bus_name, bus), object_path=self.object_path)
 
52
        self.bus = bus
 
53
 
 
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__
 
59
    else:
 
60
        name = 'org.freedesktop.DBus.Python.%s.%s' % (
 
61
            exception.__class__.__module__, exception.__class__.__name__)
 
62
 
 
63
    # LS CUSTOM
 
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
 
69
    else:
 
70
        contents = ''.join(traceback.format_exception_only(exception.__class__,
 
71
                                                           exception))
 
72
    # Explicitly jam the name on the front of the contents so that our error
 
73
    # detection works.
 
74
    contents = "%s: %s" % (name, contents)
 
75
    # END LS CUSTOM
 
76
 
 
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)
 
81
    else:
 
82
        import _dbus_bindings
 
83
        reply = _dbus_bindings.ErrorMessage(message, name, contents)
 
84
        connection.send_message(reply)
 
85
 
 
86
if dbus.version[:3] <= (0, 80, 2):
 
87
    import dbus.service
 
88
    dbus.service._method_reply_error = _method_reply_error
 
89
 
 
90
 
 
91
def method(interface, **kwargs):
 
92
    """
 
93
    Factory for decorators used export methods of a L{dbus.service.Object}
 
94
    to be exported on the D-Bus.
 
95
 
 
96
    If a method returns a L{Deferred}, it will automatically be handled.
 
97
    """
 
98
    def decorator(function):
 
99
        def inner(self, *args, **kwargs):
 
100
            __cb = kwargs.pop("__cb")
 
101
            __eb = kwargs.pop("__eb")
 
102
 
 
103
            def callback(result):
 
104
                # dbus can't serialize None; convert it to a return of 0
 
105
                # values.
 
106
                if result is None:
 
107
                    __cb()
 
108
                else:
 
109
                    __cb(result)
 
110
 
 
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()
 
119
                __eb(exception)
 
120
 
 
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.
 
124
            d = Deferred()
 
125
            d.addCallback(lambda ignored: function(self, *args, **kwargs))
 
126
            d.addCallbacks(callback, errback)
 
127
            d.callback(None)
 
128
 
 
129
        byte_arrays = kwargs.pop("byte_arrays", False)
 
130
        if byte_arrays:
 
131
            raise NotImplementedError(
 
132
                "Please don't use byte_arrays; it doesn't work on old "
 
133
                "versions of python-dbus.")
 
134
 
 
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.
 
141
 
 
142
        # To get around this we just set the internal attribute which
 
143
        # dbus_method would set itself that specifies what the callback keyword
 
144
        # arguments are.
 
145
        inner._dbus_async_callbacks = ("__cb", "__eb")
 
146
        return inner
 
147
    return decorator
 
148
 
 
149
 
 
150
DBUS_CALL_TIMEOUT = 70
 
151
 
 
152
class AsynchronousProxyMethod(object):
 
153
    """
 
154
    A wrapper for L{dbus.proxies.ProxyMethod}s that causes calls
 
155
    to return L{Deferred}s.
 
156
    """
 
157
 
 
158
    def __init__(self, wrapper, method_name, dbus_interface,
 
159
                 retry_timeout):
 
160
        self.wrapper = wrapper
 
161
        self.method_name = method_name
 
162
        self._dbus_interface = dbus_interface
 
163
        self._retry_timeout = retry_timeout
 
164
 
 
165
    def __call__(self, *args, **kwargs):
 
166
        result = Deferred()
 
167
        self._call_with_retrying(result, args, kwargs)
 
168
        result.addErrback(self._massage_errors)
 
169
        return result
 
170
 
 
171
    def _massage_errors(self, failure):
 
172
        """Python DBUS has terrible exception reporting.
 
173
 
 
174
        Convert many types of errors which into things which are
 
175
        actually catchable.
 
176
        """
 
177
        failure.trap(DBusException)
 
178
        message = failure.getErrorMessage()
 
179
        # handle different crappy error messages from various versions
 
180
        # of DBUS.
 
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)
 
191
 
 
192
        if message.startswith(PYTHON_EXCEPTION_PREFIX):
 
193
            python_exception = message[len(PYTHON_EXCEPTION_PREFIX)
 
194
                                       :message.find(":")]
 
195
            if python_exception in SAFE_EXCEPTIONS:
 
196
                raise namedClass(python_exception)(message)
 
197
 
 
198
        return failure
 
199
 
 
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)
 
210
        else:
 
211
            result.errback(failure)
 
212
 
 
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,
 
220
                                   first_failure_time)
 
221
        execute_result.addErrback(self._retry_on_failure,
 
222
                                  result, args, kwargs, first_failure_time)
 
223
 
 
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))
 
229
        else:
 
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])
 
234
                else:
 
235
                    result.callback(result_args)
 
236
 
 
237
            def got_error(exception):
 
238
                failure = Failure(exception)
 
239
                self._retry_on_failure(failure, result, args, kwargs,
 
240
                                       first_failure_time)
 
241
            kwargs["reply_handler"] = got_result
 
242
            kwargs["error_handler"] = got_error
 
243
            kwargs["dbus_interface"] = self._dbus_interface
 
244
            try:
 
245
                method(*args, **kwargs)
 
246
            except Exception, e:
 
247
                result.errback()
 
248
 
 
249
class AsynchronousDBUSObjectWrapper(object):
 
250
    """
 
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}).
 
253
    """
 
254
    def __init__(self, bus, bus_name, path, retry_timeout,
 
255
                 dbus_interface=None):
 
256
        """
 
257
        @param bus: The bus.
 
258
        """
 
259
        self._bus = bus
 
260
        self._bus_name = bus_name
 
261
        self._path = path
 
262
        self._object = None
 
263
        if dbus_interface is None:
 
264
            dbus_interface = path.strip("/").replace("/", ".")
 
265
        self._dbus_interface = dbus_interface
 
266
        self._retry_timeout = retry_timeout
 
267
 
 
268
    def get_object(self, reset_cache=False):
 
269
        if reset_cache:
 
270
            self._object = None
 
271
        if self._object is None:
 
272
            self._object = self._bus.get_object(self._bus_name, self._path,
 
273
                                                introspect=False)
 
274
        return self._object
 
275
 
 
276
    def __getattr__(self, name):
 
277
        """
 
278
        Get a L{AsynchronousProxyMethod} wrapped around the original attribute
 
279
        of the C{dbus_object}.
 
280
        """
 
281
        return AsynchronousProxyMethod(self, name, self._dbus_interface,
 
282
                                       self._retry_timeout)
 
283
 
 
284
    def __repr__(self):
 
285
        return "<AsynchronousDBUSObjectWrapper at 0x%x on %r>" % (
 
286
            id(self), self.dbus_object)
 
287
 
 
288
 
 
289
def get_bus(name):
 
290
    """Create a DBUS bus by name."""
 
291
    bus_class_name = name.capitalize() + "Bus"
 
292
    try:
 
293
        bus_class = getattr(dbus, bus_class_name)
 
294
    except AttributeError:
 
295
        raise ValueError("Invalid bus name: %r" % name)
 
296
    return bus_class()
 
297
 
 
298
 
 
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)
 
305
 
 
306
 
 
307
def hack_dbus_get_object():
 
308
    """
 
309
    Old versions of dbus did not support the 'introspect' argument to
 
310
    Bus.get_object. This method installs a version that does.
 
311
    """
 
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
 
316
 
 
317
if dbus.version[:2] < (0, 80):
 
318
    hack_dbus_get_object()
 
319
 
 
320
 
 
321
def byte_array(bytestring):
 
322
    """Convert a Python str to an L{Array} of L{Byte}s.
 
323
 
 
324
    This should be used instead of L{dbus.ByteArray} because in old versions of
 
325
    dbus it is not serialized properly.
 
326
    """
 
327
    return Array(Byte(ord(c)) for c in bytestring)
 
328
 
 
329
 
 
330
def array_to_string(array):
 
331
    """Convert an L{Array} of L{Byte}s (or integers) to a Python str.
 
332
    """
 
333
    result = []
 
334
    # HOLY LORD dbus has horrible bugs
 
335
    for item in array:
 
336
        if item < 0:
 
337
            item = item + 256
 
338
        result.append(chr(item))
 
339
    return "".join(result)