3
client for DBus services using ObjectManager
4
============================================
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.
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
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.
23
'ObjectManagerClient',
27
from collections import defaultdict
28
from logging import getLogger
31
_logger = getLogger("plainbox.client")
34
class InvalidatedType:
36
Class representing the special property value `Invalidated`
40
Invalidated = InvalidatedType
45
Class representing a mirror of state exposed by an object on DBus.
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)
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.
59
:ivar _interfaces_and_properties:
60
Dictionary mapping interface names to a collection of properties
65
Initialize a new MirroredObject
67
self._interfaces_and_properties = defaultdict(dict)
70
def interfaces_and_properties(self):
72
dictionary mapping interface name to a dictionary mapping property name
75
return self._interfaces_and_properties
77
def _add_properties(self, interfaces_and_properties):
79
Add interfaces (and optionally, properties)
81
:param interfaces_and_properties:
82
Mapping from interface name to a map of properties
84
self._interfaces_and_properties.update(interfaces_and_properties)
86
def _remove_interfaces(self, interfaces):
88
Remove interfaces (and properties that they carry).
91
List of interface names to remove
93
True if the object is empty and should be removed
95
for iface_name in interfaces:
97
del self._interfaces_and_properties[iface_name]
100
return len(self._interfaces_and_properties) == 0
102
def _change_properties(self, iface_name, props_changed, props_invalidated):
104
Change properties for a particular interface
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
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
118
class ObjectManagerClient:
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.
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.
127
The callback has at least two arguments: the object manager client instance
128
itself and event name. The following events are supported:
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.
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
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
147
This event is provided whenever an object is removed from the
148
observed bus name. Same restrictions as to "object-added" above
151
This event is provided whenever one or more properties on an object
152
are changed or invalidated (changed without providing the updated
155
The state is mirrored based on the three DBus signals:
157
- org.freedesktop.DBus.ObjectManager.InterfacesAdded
158
- org.freedesktop.DBus.ObjectManager.InterfacesRemoved
159
- org.freedesktop.DBus.Properties.PropertiesChanged
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.
166
In addition the client listens to the following DBus signal:
168
- org.freedesktop.DBus.NameOwnerChanged
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.
173
This class has the following instance variables:
176
A dbus.Connection object
178
Well-known name of the bus that this client is observing
180
The callback function invoked whenever an event is produced
182
Dictionary mapping from DBus object path to a :class:`MirroredObject`
184
A dbus.bus.NameOwnerWatch that corresponds to the NameOwnerChanged
185
signal observed by this object.
187
A list of dbus.connection.SignalMatch that correspond to the three
188
data-related signals (InterfacesAdded, InterfacesRemoved,
189
PropertiesChanged) observed by this object.
192
DBUS_OBJECT_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager'
193
DBUS_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties'
195
def __init__(self, connection, bus_name, event_cb):
197
Initialize a new ObjectManagerClient
200
A dbus.Connection to work with
202
The DBus bus name of where the ObjectManager objects reside.
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
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()
220
A mapping from object path to :class:`MirroredObject`
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.
233
Stop observing changes and dispose of this client.
235
Can be safely called multiple times.
237
if self._watch is not None:
239
for match in self._matches:
244
def pre_seed(self, object_path):
246
Pre-seed known objects with objects managed the specified manager.
249
Path of the object to interrogate.
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.
257
This function does a synchronous (blocking) DBus method call.
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)
266
def _observe_bus_name(self):
268
Internal method of ObjectManagerClient.
270
Sets up a watch for the owner of the bus we are interested in
273
A NameOwnerWatch instance that describes the watch
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)
281
def _observe_signals(self):
283
Internal method of ObjectManagerClient
285
Sets up a set of matches that allow the client to observe enough
286
signals to keep internal mirror of the data correct.
288
Tells DBus that we want to see the three essential signals:
290
- org.freedesktop.DBus.ObjectManager.InterfacesAdded
291
- org.freedesktop.DBus.ObjectManager.InterfacesRemoved
292
- org.freedesktop.DBus.Properties.PropertiesChanged
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
299
A tuple of SignalMatch instances that describe observed signals.
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
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
320
path_keyword='object_path',
321
interface_keyword='interface',
322
member_keyword='member')
323
return (match_object_manager, match_properties)
325
def _on_signal(self, *args, object_path, interface, member):
327
Internal method of ObjectManagerClient
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.
334
Arguments of the original signal
336
Path of the object that sent the signal
338
Name of the DBus interface that designates the signal
340
Name of the DBus signal (without the interface part)
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)
354
def _on_name_owner_changed(self, name):
356
Callback invoked when owner of the observed bus name changes
359
self._objects.clear()
360
self._event_cb(self, 'service-lost')
362
self._event_cb(self, 'service-back')
364
def _on_interfaces_added(self, object_path, interfaces_and_properties):
366
Callback invoked when an object gains one or more interfaces
368
The DBus signature of this signal is: oa{sa{sv}}
371
self._objects[object_path]._add_properties(interfaces_and_properties)
374
self, 'object-added',
375
object_path, interfaces_and_properties)
377
def _on_interfaces_removed(self, object_path, interfaces):
379
Callback invoked when an object looses one or more interfaces.
381
The DBus signature of this signal is: oas
384
if self._objects[object_path]._remove_interfaces(interfaces):
385
del self._objects[object_path]
387
self._event_cb(self, 'object-removed', object_path, interfaces)
389
def _on_properties_changed(self, iface_name, props_changed,
390
props_invalidated, *, object_path):
392
Callback invoked when an object is modified.
394
The DBus signature of this signal is: as{sv}as
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'.
404
self._objects[object_path]._change_properties(
405
iface_name, props_changed, props_invalidated)
408
self, 'object-changed',
409
object_path, iface_name, props_changed, props_invalidated)
412
class PlainBoxClient(ObjectManagerClient):
414
A subclass of ObjectManagerClient that additionally observes PlainBox
415
specific DBus signals.
417
There are two new events that can show up on the event callback:
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
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.
428
The state is mirrored based on the same signals as in ObjectManagerClient,
429
in addition though, two more signals are being monitored:
431
Both signals are affected by this bug:
432
https://bugs.launchpad.net/checkbox-ihv-ng/+bug/1236322
434
- com.canonical.certification.PlainBox.Service1.JobResultAvailable
435
- com.canonical.certification.PlainBox.Service1.AskForOutcome
438
BUS_NAME = 'com.canonical.certification.PlainBox1'
440
def __init__(self, connection, event_cb):
442
Initialize a new PlainBoxClient
445
A dbus.Connection to work with
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.
452
super(PlainBoxClient, self).__init__(
453
connection, self.BUS_NAME, event_cb)
455
def _observe_signals(self):
457
Internal method of ObjectManagerClient
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.
465
A tuple of SignalMatch instances that describes observed signals.
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
475
path_keyword='object_path',
476
interface_keyword='interface',
477
member_keyword='member')
478
return (match_everything,)
480
def _on_signal(self, *args, object_path, interface, member):
482
Internal method of ObjectManagerClient
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.
488
The overridden version supports the two PlainBox-specific signals,
489
relying the rest to the base class.
492
Arguments of the original signal
494
Path of the object that sent the signal
496
Name of the DBus interface that designates the signal
498
Name of the DBus signal (without the interface part)
501
if member == "JobResultAvailable":
502
return self._on_job_result_available(*args)
503
elif member == "AskForOutcome":
504
return self._on_ask_for_outcome(*args)
506
return super(PlainBoxClient, self)._on_signal(
508
object_path=object_path, interface=interface, member=member)
510
def _on_job_result_available(self, job, result):
512
Callback invoked when JobResultAvailable signal is received
515
self._event_cb(self, 'job-result-available', job, result)
517
def _on_ask_for_outcome(self, runner):
519
Callback invoked when JobResultAvailable signal is received
522
self._event_cb(self, 'ask-for-outcome', runner)