summaryrefslogtreecommitdiff
path: root/dbus/proxies.py
blob: 66dc9a9e35fbf0b24b9f34d37022b776c4810586 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# Copyright (C) 2003, 2004, 2005, 2006 Red Hat Inc. <http://www.redhat.com/>
# Copyright (C) 2003 David Zeuthen
# Copyright (C) 2004 Rob Taylor
# Copyright (C) 2005, 2006 Collabora Ltd. <http://www.collabora.co.uk/>
#
# Licensed under the Academic Free License version 2.1
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import sys
import logging

import _dbus_bindings
from dbus._expat_introspect_parser import process_introspection_data
from dbus.exceptions import MissingReplyHandlerException, MissingErrorHandlerException, IntrospectionParserException, DBusException

__docformat__ = 'restructuredtext'


_logger = logging.getLogger('dbus.proxies')


BUS_DAEMON_NAME = 'org.freedesktop.DBus'
BUS_DAEMON_PATH = '/org/freedesktop/DBus'
BUS_DAEMON_IFACE = BUS_DAEMON_NAME


class _ReplyHandler(object):
    __slots__ = ('_on_error', '_on_reply', '_get_args_options')
    def __init__(self, on_reply, on_error, **get_args_options):
        self._on_error = on_error
        self._on_reply = on_reply
        self._get_args_options = get_args_options

    def __call__(self, message):
        if isinstance(message, _dbus_bindings.MethodReturnMessage):
            self._on_reply(*message.get_args_list(**self._get_args_options))
        elif isinstance(message, _dbus_bindings.ErrorMessage):
            args = message.get_args_list()
            if len(args) > 0:
                self._on_error(DBusException(args[0]))
            else:
                self._on_error(DBusException())
        else:
            self._on_error(DBusException('Unexpected reply message type: %s'
                                        % message))


class DeferedMethod:
    """A DeferedMethod
    
    This is returned instead of ProxyMethod when we are defering DBus calls
    while waiting for introspection data to be returned
    """
    def __init__(self, proxy_method):
        self._proxy_method = proxy_method
        self._method_name  = proxy_method._method_name
    
    def __call__(self, *args, **keywords):
        reply_handler = None
        if keywords.has_key('reply_handler'):
            reply_handler = keywords['reply_handler']

        #block for now even on async
        # FIXME: put ret in async queue in future if we have a reply handler

        self._proxy_method._proxy._pending_introspect.block()
        ret = self._proxy_method (*args, **keywords)
        
        return ret

class ProxyMethod:
    """A proxy Method.

    Typically a member of a ProxyObject. Calls to the
    method produce messages that travel over the Bus and are routed
    to a specific named Service.
    """
    def __init__(self, proxy, connection, named_service, object_path, method_name, iface):
        self._proxy          = proxy
        self._connection     = connection
        self._named_service  = named_service
        self._object_path    = object_path
        self._method_name    = method_name
        self._dbus_interface = iface

    def __call__(self, *args, **keywords):
        timeout = -1
        if keywords.has_key('timeout'):
            timeout = keywords['timeout']

        reply_handler = None
        if keywords.has_key('reply_handler'):
            reply_handler = keywords['reply_handler']

        error_handler = None
        if keywords.has_key('error_handler'):
            error_handler = keywords['error_handler']

        ignore_reply = False
        if keywords.has_key('ignore_reply'):
            ignore_reply = keywords['ignore_reply']

        get_args_options = {}
        if keywords.has_key('utf8_strings'):
            get_args_options['utf8_strings'] = keywords['utf8_strings']
        if keywords.has_key('byte_arrays'):
            get_args_options['byte_arrays'] = keywords['byte_arrays']

        if not(reply_handler and error_handler):
            if reply_handler:
                raise MissingErrorHandlerException()
            elif error_handler:
                raise MissingReplyHandlerException()

        dbus_interface = self._dbus_interface
        if keywords.has_key('dbus_interface'):
            dbus_interface = keywords['dbus_interface']

        tmp_iface = ''
        if dbus_interface:
            tmp_iface = dbus_interface + '.'

        key = tmp_iface + self._method_name

        introspect_sig = None
        if self._proxy._introspect_method_map.has_key (key):
            introspect_sig = self._proxy._introspect_method_map[key]

        message = _dbus_bindings.MethodCallMessage(destination=None,
                                                   path=self._object_path,
                                                   interface=dbus_interface,
                                                   method=self._method_name)
        message.set_destination(self._named_service)
        
        # Add the arguments to the function
        try:
            message.append(signature=introspect_sig, *args)
        except Exception, e:
            _logger.error('Unable to set arguments %r according to '
                          'introspected signature %r: %s: %s',
                          args, introspect_sig, e.__class__, e)
            raise

        if ignore_reply:
            self._connection.send_message(message)
            return None
        elif reply_handler:
            self._connection.send_message_with_reply(message, _ReplyHandler(reply_handler, error_handler, **get_args_options), timeout/1000.0, require_main_loop=1)
            return None
        else:
            reply_message = self._connection.send_message_with_reply_and_block(message, timeout)
            args_list = reply_message.get_args_list(**get_args_options)
            if len(args_list) == 0:
                return None
            elif len(args_list) == 1:
                return args_list[0]
            else:
                return tuple(args_list)


class ProxyObject:
    """A proxy to the remote Object.

    A ProxyObject is provided by the Bus. ProxyObjects
    have member functions, and can be called like normal Python objects.
    """
    ProxyMethodClass = ProxyMethod
    DeferedMethodClass = DeferedMethod

    INTROSPECT_STATE_DONT_INTROSPECT = 0
    INTROSPECT_STATE_INTROSPECT_IN_PROGRESS = 1
    INTROSPECT_STATE_INTROSPECT_DONE = 2

    def __init__(self, bus, named_service, object_path, introspect=True,
                 follow_name_owner_changes=False):
        """Initialize the proxy object.

        :Parameters:
            `bus` : `dbus.Bus`
                The bus on which to find this object
            `named_service` : str
                A bus name for the endpoint owning the object (need not
                actually be a service name)
            `object_path` : str
                The object path at which the endpoint exports the object
            `introspect` : bool
                If true (default), attempt to introspect the remote
                object to find out supported methods and their signatures
            `follow_name_owner_changes` : bool
                If true (default is false) and the `named_service` is a
                well-known name, follow ownership changes for that name
        """
        if follow_name_owner_changes:
            bus._require_main_loop()   # we don't get the signals otherwise

        self._bus           = bus
        self._named_service = named_service
        self.__dbus_object_path__ = object_path

        if (named_service[:1] != ':' and named_service != BUS_DAEMON_NAME
            and not follow_name_owner_changes):
            bus_object = bus.get_object(BUS_DAEMON_NAME, BUS_DAEMON_PATH)
            try:
                self._named_service = bus_object.GetNameOwner(named_service,
                        dbus_interface=BUS_DAEMON_IFACE)
            except DBusException, e:
                # FIXME: detect whether it's NameHasNoOwner
                #if not str(e).startswith('org.freedesktop.DBus.Error.NameHasNoOwner:'):
                #    raise
                # it might not exist: try to start it
                bus_object.StartServiceByName(named_service,
                                              _dbus_bindings.UInt32(0))
                self._named_service = bus_object.GetNameOwner(named_service,
                        dbus_interface=BUS_DAEMON_IFACE)

        #PendingCall object for Introspect call
        self._pending_introspect = None
        #queue of async calls waiting on the Introspect to return 
        self._pending_introspect_queue = []
        #dictionary mapping method names to their input signatures
        self._introspect_method_map = {}

        if not introspect:
            self._introspect_state = self.INTROSPECT_STATE_DONT_INTROSPECT
        else:
            self._introspect_state = self.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS
            
            self._pending_introspect = self._Introspect()

    def connect_to_signal(self, signal_name, handler_function, dbus_interface=None, **keywords):
        """Arrange for the given function to be called when the given signal
        is received.

        :Parameters:
            `signal_name` : str
                The name of the signal
            `handler_function` : callable
                A function to be called when the signal is emitted by
                the remote object. Its positional arguments will be the
                arguments of the signal; optionally, it may be given
                keyword arguments as described below.
            `dbus_interface` : str
                Optional interface with which to qualify the signal name.
                If None (the default) the handler will be called whenever a
                signal of the given member name is received, whatever
                its interface.
        :Keywords:
            `utf8_strings` : bool
                If True, the handler function will receive any string
                arguments as dbus.UTF8String objects (a subclass of str
                guaranteed to be UTF-8). If False (default) it will receive
                any string arguments as dbus.String objects (a subclass of
                unicode).
            `byte_arrays` : bool
                If True, the handler function will receive any byte-array
                arguments as dbus.ByteArray objects (a subclass of str).
                If False (default) it will receive any byte-array
                arguments as a dbus.Array of dbus.Byte (subclasses of:
                a list of ints).
            `sender_keyword` : str
                If not None (the default), the handler function will receive
                the unique name of the sending endpoint as a keyword
                argument with this name
            `destination_keyword` : str
                If not None (the default), the handler function will receive
                the bus name of the destination (or None if the signal is a
                broadcast, as is usual) as a keyword argument with this name.
            `interface_keyword` : str
                If not None (the default), the handler function will receive
                the signal interface as a keyword argument with this name.
            `member_keyword` : str
                If not None (the default), the handler function will receive
                the signal name as a keyword argument with this name.
            `path_keyword` : str
                If not None (the default), the handler function will receive
                the object-path of the sending object as a keyword argument
                with this name
            `message_keyword` : str
                If not None (the default), the handler function will receive
                the `dbus.lowlevel.SignalMessage` as a keyword argument with
                this name.
            `arg...` : unicode or UTF-8 str
                If there are additional keyword parameters of the form
                ``arg``\ *n*, match only signals where the *n*\ th argument
                is the value given for that keyword parameter. As of this time
                only string arguments can be matched (in particular,
                object paths and signatures can't).
        """
        return \
        self._bus.add_signal_receiver(handler_function,
                                      signal_name=signal_name,
                                      dbus_interface=dbus_interface,
                                      named_service=self._named_service,
                                      path=self.__dbus_object_path__,
                                      **keywords)

    def _Introspect(self):
        message = _dbus_bindings.MethodCallMessage(None, self.__dbus_object_path__, 'org.freedesktop.DBus.Introspectable', 'Introspect')
        message.set_destination(self._named_service)
        
        result = self._bus.get_connection().send_message_with_reply(message, _ReplyHandler(self._introspect_reply_handler, self._introspect_error_handler, utf8_strings=True), -1)
        return result
    
    def _introspect_execute_queue(self):
        for call in self._pending_introspect_queue:
            (member, iface, args, keywords) = call

            introspect_sig = None

            tmp_iface = ''
            if iface:
                tmp_iface = iface + '.'
                    
            key = tmp_iface + '.' + member
            if self._introspect_method_map.has_key (key):
                introspect_sig = self._introspect_method_map[key]

            
            call_object = self.ProxyMethodClass(self._bus.get_connection(),
                                                self._named_service,
                                                self.__dbus_object_path__,
                                                iface,
                                                member,
                                                introspect_sig)
                                                                       
            call_object(args, keywords)

    def _introspect_reply_handler(self, data):
        try:
            self._introspect_method_map = process_introspection_data(data)
        except IntrospectionParserException, e:
            self._introspect_error_handler(e)
            return
        
        self._introspect_state = self.INTROSPECT_STATE_INTROSPECT_DONE
        #self._introspect_execute_queue()

    def _introspect_error_handler(self, error):
        self._introspect_state = self.INTROSPECT_STATE_DONT_INTROSPECT
        self._introspect_execute_queue()
        sys.stderr.write("Introspect error: " + str(error) + "\n")

    def __getattr__(self, member, dbus_interface=None):
        if member == '__call__':
            return object.__call__
        elif member.startswith('__') and member.endswith('__'):
            raise AttributeError(member)
        else:
            return self.get_dbus_method(member, dbus_interface)

    def get_dbus_method(self, member, dbus_interface=None):
        """Return a proxy method representing the given D-Bus method. The
        returned proxy method can be called in the usual way. For instance, ::

            proxy.get_dbus_method("Foo", dbus_interface='com.example.Bar')(123)

        is equivalent to::

            proxy.Foo(123, dbus_interface='com.example.Bar')

        or even::

            getattr(proxy, "Foo")(123, dbus_interface='com.example.Bar')

        However, using `get_dbus_method` is the only way to call D-Bus
        methods with certain awkward names - if the author of a service
        implements a method called ``connect_to_signal`` or even
        ``__getattr__``, you'll need to use `get_dbus_method` to call them.

        For services which follow the D-Bus convention of CamelCaseMethodNames
        this won't be a problem.
        """

        ret = self.ProxyMethodClass(self, self._bus.get_connection(),
                                    self._named_service,
                                    self.__dbus_object_path__, member,
                                    dbus_interface)
    
        if self._introspect_state == self.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS:
            ret = self.DeferedMethodClass(ret)

        return ret

    def __repr__(self):
        return '<ProxyObject wrapping %s %s %s at %#x>'%(
            self._bus, self._named_service, self.__dbus_object_path__, id(self))
    __str__ = __repr__