~pieq/checkbox/lp1464195-double-clicking

« back to all changes in this revision

Viewing changes to plainbox-client/client.py

"automatic merge of lp:~zkrynicki/checkbox/purge-obsolete-stuff/ by tarmac [r=sylvain-pineau][bug=][author=zkrynicki]"

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python3
2
 
"""
3
 
client for DBus services using ObjectManager
4
 
============================================
5
 
 
6
 
This module defines :class:`ObjectManagerClient` - a general purpose client for
7
 
services that rely on the standard ObjectManager and Properties interfaces to
8
 
notify clients about state changes.
9
 
 
10
 
Using this library an application can easily observe changes to state of any
11
 
number of objects and acess the locally-held state without any additonal
12
 
round-trips.
13
 
 
14
 
The ObjectManagerClient class exposes a dictionary of :class:`MirroredObject`
15
 
which itself exposes a dictionary of interfaces and properties defined by each
16
 
such object. Invalidated properties are represented as an unique
17
 
:attr:`Invalidated` object.
18
 
"""
19
 
 
20
 
__all__ = [
21
 
    'Invalidated',
22
 
    'MirroredObject',
23
 
    'ObjectManagerClient',
24
 
    'PlainBoxClient'
25
 
]
26
 
 
27
 
from collections import defaultdict
28
 
from logging import getLogger
29
 
 
30
 
 
31
 
_logger = getLogger("plainbox.client")
32
 
 
33
 
 
34
 
class InvalidatedType:
35
 
    """
36
 
    Class representing the special property value `Invalidated`
37
 
    """
38
 
 
39
 
 
40
 
Invalidated = InvalidatedType
41
 
 
42
 
 
43
 
class MirroredObject:
44
 
    """
45
 
    Class representing a mirror of state exposed by an object on DBus.
46
 
 
47
 
    Instances of this class are crated and destroyed automatically by
48
 
    :class:`ObjectManagerClient`. Applications can look at the
49
 
    :attr:`interfaces_and_properties` without incurring any additional costs
50
 
    (expressed as the latency of DBus calls)
51
 
 
52
 
    Managed objects have a number of properties (anchored at particular
53
 
    interfaces) that are automatically updated whenever the particular object
54
 
    is changed remotely. Some properties may be expensive to compute or
55
 
    transfer and are invalidated instead. Such properties are represented as the
56
 
    special Invalidated value. Applications are free to perform explicit DBus
57
 
    Get() calls for any such property at the time that value is desired.
58
 
 
59
 
    :ivar _interfaces_and_properties:
60
 
        Dictionary mapping interface names to a collection of properties
61
 
    """
62
 
 
63
 
    def __init__(self):
64
 
        """
65
 
        Initialize a new MirroredObject
66
 
        """
67
 
        self._interfaces_and_properties = defaultdict(dict)
68
 
 
69
 
    @property
70
 
    def interfaces_and_properties(self):
71
 
        """
72
 
        dictionary mapping interface name to a dictionary mapping property name
73
 
        to property value.
74
 
        """
75
 
        return self._interfaces_and_properties
76
 
 
77
 
    def _add_properties(self, interfaces_and_properties):
78
 
        """
79
 
        Add interfaces (and optionally, properties)
80
 
 
81
 
        :param interfaces_and_properties:
82
 
            Mapping from interface name to a map of properties
83
 
        """
84
 
        self._interfaces_and_properties.update(interfaces_and_properties)
85
 
 
86
 
    def _remove_interfaces(self, interfaces):
87
 
        """
88
 
        Remove interfaces (and properties that they carry).
89
 
 
90
 
        :param interfaces:
91
 
            List of interface names to remove
92
 
        :returns:
93
 
            True if the object is empty and should be removed
94
 
        """
95
 
        for iface_name in interfaces:
96
 
            try:
97
 
                del self._interfaces_and_properties[iface_name]
98
 
            except KeyError:
99
 
                pass
100
 
        return len(self._interfaces_and_properties) == 0
101
 
 
102
 
    def _change_properties(self, iface_name, props_changed, props_invalidated):
103
 
        """
104
 
        Change properties for a particular interface
105
 
 
106
 
        :param iface_name:
107
 
            Name of the interface
108
 
        :param props_changed:
109
 
            A map of properties and their new values
110
 
        :param props_invalidated:
111
 
            A list of properties that were invalidated
112
 
        """
113
 
        self._interfaces_and_properties[iface_name].update(props_changed)
114
 
        for prop_name in props_invalidated:
115
 
            self._interfaes_and_properties[iface_name][prop_name] = Invalidated
116
 
 
117
 
 
118
 
class ObjectManagerClient:
119
 
    """
120
 
    A client that observes and mirrors locally all of the objects managed by
121
 
    one or more manager objects on a given bus name.
122
 
 
123
 
    Using this class an application can reliably maintain a mirror of all the
124
 
    state that some service exposes over DBus and be notified whenever changes
125
 
    occur with a single callback.
126
 
 
127
 
    The callback has at least two arguments: the object manager client instance
128
 
    itself and event name. The following events are supported:
129
 
 
130
 
        "service-lost":
131
 
            This event is provided whenever the owner of the observed bus name
132
 
            goes away. Typically this would happen when the process
133
 
            implementing the DBus service exits or disconnects from the bus.
134
 
        "service-back":
135
 
            This event is provided whenever the owner of the observed bus name
136
 
            is established. If the bus name was already taken at the time the
137
 
            client is initialized then the on_change callback is invoked
138
 
            immediately.
139
 
        "object-added":
140
 
            This event is provided whenever an object is added to the observed
141
 
            bus name. Note that only objects that would have been returned by
142
 
            GetManagedObjects() are reported this way. This also differentiates
143
 
            between objects merely being added to the bus (which is ignored)
144
 
            and objects being added to the managed collection (which is
145
 
            reported here).
146
 
        "object-removed":
147
 
            This event is provided whenever an object is removed from the
148
 
            observed bus name. Same restrictions as to "object-added" above
149
 
            apply.
150
 
        "object-changed":
151
 
            This event is provided whenever one or more properties on an object
152
 
            are changed or invalidated (changed without providing the updated
153
 
            value)
154
 
 
155
 
    The state is mirrored based on the three DBus signals:
156
 
 
157
 
     - org.freedesktop.DBus.ObjectManager.InterfacesAdded
158
 
     - org.freedesktop.DBus.ObjectManager.InterfacesRemoved
159
 
     - org.freedesktop.DBus.Properties.PropertiesChanged
160
 
 
161
 
    The first two signals deliver information about managed objects being
162
 
    added or removed from the bus. The first signal also carries all of the
163
 
    properties exported by such objects. The last signal carries information
164
 
    about updates to properties of already-existing objects.
165
 
 
166
 
    In addition the client listens to the following DBus signal:
167
 
 
168
 
    - org.freedesktop.DBus.NameOwnerChanged
169
 
 
170
 
    This signal is used to discover when the owner of the bus name changes.
171
 
    When the owner goes away the list of objects mirrored locally is reset.
172
 
 
173
 
    This class has the following instance variables:
174
 
 
175
 
    :ivar _connection:
176
 
        A dbus.Connection object
177
 
    :ivar _bus_name:
178
 
        Well-known name of the bus that this client is observing
179
 
    :ivar _event_cb:
180
 
        The callback function invoked whenever an event is produced
181
 
    :ivar _objects:
182
 
        Dictionary mapping from DBus object path to a :class:`MirroredObject`
183
 
    :ivar _watch:
184
 
        A dbus.bus.NameOwnerWatch that corresponds to the NameOwnerChanged
185
 
        signal observed by this object.
186
 
    :ivar _matches:
187
 
        A list of dbus.connection.SignalMatch that correspond to the three
188
 
        data-related signals (InterfacesAdded, InterfacesRemoved,
189
 
        PropertiesChanged) observed by this object.
190
 
    """
191
 
 
192
 
    DBUS_OBJECT_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager'
193
 
    DBUS_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties'
194
 
 
195
 
    def __init__(self, connection, bus_name, event_cb):
196
 
        """
197
 
        Initialize a new ObjectManagerClient
198
 
 
199
 
        :param connection:
200
 
            A dbus.Connection to work with
201
 
        :param bus_name:
202
 
            The DBus bus name of where the ObjectManager objects reside.
203
 
        :param event_cb:
204
 
            A callback invoked whenever an event is produced. The function is
205
 
            called at least two arguments, the instance of the client it was
206
 
            registered on and the string with the name of the event.
207
 
            Additional arguments (and keyword arguments) are provided, specific
208
 
            to each event.
209
 
        """
210
 
        self._connection = connection
211
 
        self._bus_name = bus_name
212
 
        self._event_cb = event_cb
213
 
        self._objects = defaultdict(MirroredObject)
214
 
        self._watch = self._observe_bus_name()
215
 
        self._matches = self._observe_signals()
216
 
 
217
 
    @property
218
 
    def objects(self):
219
 
        """
220
 
        A mapping from object path to :class:`MirroredObject`
221
 
 
222
 
        Initially this may be empty but it will react to changes in the
223
 
        observed service. It can be pre-seeded by calling GetManagedObjects on
224
 
        the appropriate object manager.
225
 
 
226
 
        ..see::
227
 
            :meth:`pre_seed()`
228
 
        """
229
 
        return self._objects
230
 
 
231
 
    def close(self):
232
 
        """
233
 
        Stop observing changes and dispose of this client.
234
 
 
235
 
        Can be safely called multiple times.
236
 
        """
237
 
        if self._watch is not None:
238
 
            self._watch.cancel()
239
 
            for match in self._matches:
240
 
                match.remove()
241
 
            self._matches = []
242
 
            self._watch = None
243
 
 
244
 
    def pre_seed(self, object_path):
245
 
        """
246
 
        Pre-seed known objects with objects managed the specified manager.
247
 
 
248
 
        :param object_path:
249
 
            Path of the object to interrogate.
250
 
 
251
 
        The specified object is interrogated and all of the objects that it
252
 
        knows about are mirrored locally. This method should be called right
253
 
        _after_ invoking :meth:`observe()` in a way that would cover this
254
 
        object. Calling those in the other order introduces a race condition.
255
 
 
256
 
        .. warning::
257
 
            This function does a synchronous (blocking) DBus method call.
258
 
        """
259
 
        proxy = self._connection.get_object(self._bus_name, object_path)
260
 
        managed_objects = proxy.GetManagedObjects(
261
 
            dbus_interface_name='org.freedesktop.DBus.ObjectManager')
262
 
        for object_path, interfaces_and_properties in managed_objects.items():
263
 
            self._objects[object_path]._add_properties(
264
 
                interfaces_and_properties)
265
 
 
266
 
    def _observe_bus_name(self):
267
 
        """
268
 
        Internal method of ObjectManagerClient.
269
 
 
270
 
        Sets up a watch for the owner of the bus we are interested in
271
 
 
272
 
        :returns:
273
 
            A NameOwnerWatch instance that describes the watch
274
 
        """
275
 
        # Setup a watch for the owner of the bus name. This will allow us to
276
 
        # reset internal state when the service goes away and comes back. It
277
 
        # also allows us to start before the service itself becomes available.
278
 
        return self._connection.watch_name_owner(
279
 
            self._bus_name, self._on_name_owner_changed)
280
 
 
281
 
    def _observe_signals(self):
282
 
        """
283
 
        Internal method of ObjectManagerClient
284
 
 
285
 
        Sets up a set of matches that allow the client to observe enough
286
 
        signals to keep internal mirror of the data correct.
287
 
 
288
 
        Tells DBus that we want to see the three essential signals:
289
 
 
290
 
        - org.freedesktop.DBus.ObjectManager.InterfacesAdded
291
 
        - org.freedesktop.DBus.ObjectManager.InterfacesRemoved
292
 
        - org.freedesktop.DBus.Properties.PropertiesChanged
293
 
 
294
 
        NOTE: we want to observe all objects on this bus name, so we have a
295
 
        complete view of all of the objects. This is why we don't filter by
296
 
        object_path.
297
 
 
298
 
        :returns:
299
 
            A tuple of SignalMatch instances that describe observed signals.
300
 
        """
301
 
        match_object_manager = self._connection.add_signal_receiver(
302
 
            handler_function=self._on_signal,
303
 
            signal_name=None,  # match all signals
304
 
            dbus_interface=self.DBUS_OBJECT_MANAGER_IFACE,
305
 
            path=None,  # match all senders
306
 
            bus_name=self._bus_name,
307
 
            # extra keywords, those allow us to get meta-data to handlers
308
 
            byte_arrays=True,
309
 
            path_keyword='object_path',
310
 
            interface_keyword='interface',
311
 
            member_keyword='member')
312
 
        match_properties = self._connection.add_signal_receiver(
313
 
            handler_function=self._on_signal,
314
 
            signal_name="PropertiesChanged",
315
 
            dbus_interface=self.DBUS_PROPERTIES_IFACE,
316
 
            path=None,  # match all senders
317
 
            bus_name=self._bus_name,
318
 
            # extra keywords, those allow us to get meta-data to handlers
319
 
            byte_arrays=True,
320
 
            path_keyword='object_path',
321
 
            interface_keyword='interface',
322
 
            member_keyword='member')
323
 
        return (match_object_manager, match_properties)
324
 
 
325
 
    def _on_signal(self, *args, object_path, interface, member):
326
 
        """
327
 
        Internal method of ObjectManagerClient
328
 
 
329
 
        Dispatches each received signal to a handler function. Doing the
330
 
        dispatch here allows us to register one listener for many signals and
331
 
        then do all the routing inside the application.
332
 
 
333
 
        :param args:
334
 
            Arguments of the original signal
335
 
        :param object_path:
336
 
            Path of the object that sent the signal
337
 
        :param interface:
338
 
            Name of the DBus interface that designates the signal
339
 
        :param member:
340
 
            Name of the DBus signal (without the interface part)
341
 
        """
342
 
        if interface == self.DBUS_OBJECT_MANAGER_IFACE:
343
 
            if member == 'InterfacesAdded':
344
 
                return self._on_interfaces_added(*args)
345
 
            elif member == 'InterfacesRemoved':
346
 
                return self._on_interfaces_removed(*args)
347
 
        elif interface == self.DBUS_PROPERTIES_IFACE:
348
 
            if member == 'PropertiesChanged':
349
 
                return self._on_properties_changed(
350
 
                    *args, object_path=object_path)
351
 
        _logger.warning("Unsupported signal received: %s.%s: %r",
352
 
                        interface, member, args)
353
 
 
354
 
    def _on_name_owner_changed(self, name):
355
 
        """
356
 
        Callback invoked when owner of the observed bus name changes
357
 
        """
358
 
        if name == '':
359
 
            self._objects.clear()
360
 
            self._event_cb(self, 'service-lost')
361
 
        else:
362
 
            self._event_cb(self, 'service-back')
363
 
 
364
 
    def _on_interfaces_added(self, object_path, interfaces_and_properties):
365
 
        """
366
 
        Callback invoked when an object gains one or more interfaces
367
 
 
368
 
        The DBus signature of this signal is: oa{sa{sv}}
369
 
        """
370
 
        # Apply changes
371
 
        self._objects[object_path]._add_properties(interfaces_and_properties)
372
 
        # Notify users
373
 
        self._event_cb(
374
 
            self, 'object-added',
375
 
            object_path, interfaces_and_properties)
376
 
 
377
 
    def _on_interfaces_removed(self, object_path, interfaces):
378
 
        """
379
 
        Callback invoked when an object looses one or more interfaces.
380
 
 
381
 
        The DBus signature of this signal is: oas
382
 
        """
383
 
        # Apply changes
384
 
        if self._objects[object_path]._remove_interfaces(interfaces):
385
 
            del self._objects[object_path]
386
 
        # Notify users
387
 
        self._event_cb(self, 'object-removed', object_path, interfaces)
388
 
 
389
 
    def _on_properties_changed(self, iface_name, props_changed,
390
 
                               props_invalidated, *, object_path):
391
 
        """
392
 
        Callback invoked when an object is modified.
393
 
 
394
 
        The DBus signature of this signal is: as{sv}as
395
 
 
396
 
        .. note::
397
 
            The signal itself does not carry information about which object was
398
 
            modified but the low-level DBus message does. To be able to
399
 
            reliably update our local mirror of the object this callback has to
400
 
            be registered with the `path_keyword` argument to
401
 
            `add_signal_receiver()` equal to 'object_path'.
402
 
        """
403
 
        # Apply changes
404
 
        self._objects[object_path]._change_properties(
405
 
            iface_name, props_changed, props_invalidated)
406
 
        # Notify users
407
 
        self._event_cb(
408
 
            self, 'object-changed',
409
 
            object_path, iface_name, props_changed, props_invalidated)
410
 
 
411
 
 
412
 
class PlainBoxClient(ObjectManagerClient):
413
 
    """
414
 
    A subclass of ObjectManagerClient that additionally observes PlainBox
415
 
    specific DBus signals.
416
 
 
417
 
    There are two new events that can show up on the event callback:
418
 
 
419
 
        "job-result-available":
420
 
            This event is provided whenever a job result becomes available on
421
 
            Vthe bus. It is sent after the state on the bus settles and becomes
422
 
            mirrored locally.
423
 
        "ask-for-outcome":
424
 
            This event is provided whenever a job result with outcome equal to
425
 
            OUTCOME_UNDECIDED was produced by PlainBox and the application
426
 
            needs to ask the user how to proceed.
427
 
 
428
 
    The state is mirrored based on the same signals as in ObjectManagerClient,
429
 
    in addition though, two more signals are being monitored:
430
 
 
431
 
    Both signals are affected by this bug:
432
 
        https://bugs.launchpad.net/checkbox-ihv-ng/+bug/1236322
433
 
 
434
 
     - com.canonical.certification.PlainBox.Service1.JobResultAvailable
435
 
     - com.canonical.certification.PlainBox.Service1.AskForOutcome
436
 
    """
437
 
 
438
 
    BUS_NAME = 'com.canonical.certification.PlainBox1'
439
 
 
440
 
    def __init__(self, connection, event_cb):
441
 
        """
442
 
        Initialize a new PlainBoxClient
443
 
 
444
 
        :param connection:
445
 
            A dbus.Connection to work with
446
 
        :param event_cb:
447
 
            A callback invoked whenever an event is produced. The function is
448
 
            called at least two arguments, the instance of the client it was
449
 
            registered on and the string with the name of the event.
450
 
            Additional arguments are provided, specific to each event.
451
 
        """
452
 
        super(PlainBoxClient, self).__init__(
453
 
            connection, self.BUS_NAME, event_cb)
454
 
 
455
 
    def _observe_signals(self):
456
 
        """
457
 
        Internal method of ObjectManagerClient
458
 
 
459
 
        Unlike in ObjectManagerClient, in PlainBoxClient, it actually tells
460
 
        DBus that we want to look at ALL the signals sent on the particular bus
461
 
        name. This is less racy (one call) and gets us also the two other
462
 
        signals that plainbox currently uses.
463
 
 
464
 
        :returns:
465
 
            A tuple of SignalMatch instances that describes observed signals.
466
 
        """
467
 
        match_everything = self._connection.add_signal_receiver(
468
 
            handler_function=self._on_signal,
469
 
            signal_name=None,  # match all signals
470
 
            dbus_interface=None,  # match all interfaces
471
 
            path=None,  # match all senders
472
 
            bus_name=self._bus_name,
473
 
            # extra keywords, those allow us to get meta-data to handlers
474
 
            byte_arrays=True,
475
 
            path_keyword='object_path',
476
 
            interface_keyword='interface',
477
 
            member_keyword='member')
478
 
        return (match_everything,)
479
 
 
480
 
    def _on_signal(self, *args, object_path, interface, member):
481
 
        """
482
 
        Internal method of ObjectManagerClient
483
 
 
484
 
        Dispatches each received signal to a handler function. Doing the
485
 
        dispatch here allows us to register one listener for many signals and
486
 
        then do all the routing inside the application.
487
 
 
488
 
        The overridden version supports the two PlainBox-specific signals,
489
 
        relying the rest to the base class.
490
 
 
491
 
        :param args:
492
 
            Arguments of the original signal
493
 
        :param object_path:
494
 
            Path of the object that sent the signal
495
 
        :param interface:
496
 
            Name of the DBus interface that designates the signal
497
 
        :param member:
498
 
            Name of the DBus signal (without the interface part)
499
 
        """
500
 
 
501
 
        if member == "JobResultAvailable":
502
 
            return self._on_job_result_available(*args)
503
 
        elif member == "AskForOutcome":
504
 
            return self._on_ask_for_outcome(*args)
505
 
        else:
506
 
            return super(PlainBoxClient, self)._on_signal(
507
 
                *args,
508
 
                object_path=object_path, interface=interface, member=member)
509
 
 
510
 
    def _on_job_result_available(self, job, result):
511
 
        """
512
 
        Callback invoked when JobResultAvailable signal is received
513
 
        """
514
 
        # Notify users
515
 
        self._event_cb(self, 'job-result-available', job, result)
516
 
 
517
 
    def _on_ask_for_outcome(self, runner):
518
 
        """
519
 
        Callback invoked when JobResultAvailable signal is received
520
 
        """
521
 
        # Notify users
522
 
        self._event_cb(self, 'ask-for-outcome', runner)