dbus-fast 3.1.2__cp310-cp310-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. dbus_fast/__init__.py +82 -0
  2. dbus_fast/__version__.py +10 -0
  3. dbus_fast/_private/__init__.py +1 -0
  4. dbus_fast/_private/_cython_compat.py +14 -0
  5. dbus_fast/_private/address.cpython-310-darwin.so +0 -0
  6. dbus_fast/_private/address.pxd +15 -0
  7. dbus_fast/_private/address.py +119 -0
  8. dbus_fast/_private/constants.py +20 -0
  9. dbus_fast/_private/marshaller.cpython-310-darwin.so +0 -0
  10. dbus_fast/_private/marshaller.pxd +110 -0
  11. dbus_fast/_private/marshaller.py +231 -0
  12. dbus_fast/_private/unmarshaller.cpython-310-darwin.so +0 -0
  13. dbus_fast/_private/unmarshaller.pxd +261 -0
  14. dbus_fast/_private/unmarshaller.py +904 -0
  15. dbus_fast/_private/util.py +177 -0
  16. dbus_fast/aio/__init__.py +5 -0
  17. dbus_fast/aio/message_bus.py +578 -0
  18. dbus_fast/aio/message_reader.cpython-310-darwin.so +0 -0
  19. dbus_fast/aio/message_reader.pxd +13 -0
  20. dbus_fast/aio/message_reader.py +51 -0
  21. dbus_fast/aio/proxy_object.py +208 -0
  22. dbus_fast/auth.py +125 -0
  23. dbus_fast/constants.py +152 -0
  24. dbus_fast/errors.py +81 -0
  25. dbus_fast/glib/__init__.py +3 -0
  26. dbus_fast/glib/message_bus.py +513 -0
  27. dbus_fast/glib/proxy_object.py +318 -0
  28. dbus_fast/introspection.py +686 -0
  29. dbus_fast/message.cpython-310-darwin.so +0 -0
  30. dbus_fast/message.pxd +76 -0
  31. dbus_fast/message.py +389 -0
  32. dbus_fast/message_bus.cpython-310-darwin.so +0 -0
  33. dbus_fast/message_bus.pxd +75 -0
  34. dbus_fast/message_bus.py +1332 -0
  35. dbus_fast/proxy_object.py +357 -0
  36. dbus_fast/py.typed +0 -0
  37. dbus_fast/send_reply.py +61 -0
  38. dbus_fast/service.cpython-310-darwin.so +0 -0
  39. dbus_fast/service.pxd +50 -0
  40. dbus_fast/service.py +727 -0
  41. dbus_fast/signature.cpython-310-darwin.so +0 -0
  42. dbus_fast/signature.pxd +31 -0
  43. dbus_fast/signature.py +484 -0
  44. dbus_fast/unpack.cpython-310-darwin.so +0 -0
  45. dbus_fast/unpack.pxd +13 -0
  46. dbus_fast/unpack.py +28 -0
  47. dbus_fast/validators.py +199 -0
  48. dbus_fast-3.1.2.dist-info/METADATA +262 -0
  49. dbus_fast-3.1.2.dist-info/RECORD +51 -0
  50. dbus_fast-3.1.2.dist-info/WHEEL +6 -0
  51. dbus_fast-3.1.2.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,1332 @@
1
+ # cython: freethreading_compatible = True
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import io
7
+ import logging
8
+ import socket
9
+ import traceback
10
+ import xml.etree.ElementTree as ET
11
+ from collections.abc import Callable
12
+ from contextlib import ExitStack
13
+ from functools import partial
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from . import introspection as intr
17
+ from ._private.address import get_bus_address, parse_address
18
+ from ._private.util import replace_fds_with_idx, replace_idx_with_fds
19
+ from .constants import (
20
+ BusType,
21
+ ErrorType,
22
+ MessageFlag,
23
+ MessageType,
24
+ NameFlag,
25
+ ReleaseNameReply,
26
+ RequestNameReply,
27
+ )
28
+ from .errors import DBusError, InvalidAddressError
29
+ from .message import Message
30
+ from .proxy_object import BaseProxyObject
31
+ from .send_reply import SendReply
32
+ from .service import HandlerType, ServiceInterface, _Method, _Property
33
+ from .signature import Variant
34
+ from .validators import assert_bus_name_valid, assert_object_path_valid
35
+
36
+ MESSAGE_TYPE_CALL = MessageType.METHOD_CALL
37
+ MESSAGE_TYPE_SIGNAL = MessageType.SIGNAL
38
+ NO_REPLY_EXPECTED_VALUE = MessageFlag.NO_REPLY_EXPECTED.value
39
+ NO_REPLY_EXPECTED = MessageFlag.NO_REPLY_EXPECTED
40
+ NONE = MessageFlag.NONE
41
+ _LOGGER = logging.getLogger(__name__)
42
+
43
+
44
+ _Message = Message
45
+
46
+
47
+ def _expects_reply(msg: _Message) -> bool:
48
+ """Whether a message expects a reply."""
49
+ if msg.flags is NO_REPLY_EXPECTED:
50
+ return False
51
+ if msg.flags is NONE:
52
+ return True
53
+ # Slow check for NO_REPLY_EXPECTED
54
+ flag_value = msg.flags.value
55
+ return not (flag_value & NO_REPLY_EXPECTED_VALUE)
56
+
57
+
58
+ def _block_unexpected_reply(reply: _Message) -> None:
59
+ """Block a reply if it's not expected.
60
+
61
+ Previously we silently ignored replies that were not expected, but this
62
+ lead to implementation errors that were hard to debug. Now we log a
63
+ debug message instead.
64
+ """
65
+ _LOGGER.debug(
66
+ "Blocked attempt to send a reply from handler "
67
+ "that received a message with flag "
68
+ "MessageFlag.NO_REPLY_EXPECTED: %s",
69
+ reply,
70
+ )
71
+
72
+
73
+ BLOCK_UNEXPECTED_REPLY = _block_unexpected_reply
74
+
75
+
76
+ class BaseMessageBus:
77
+ """An abstract class to manage a connection to a DBus message bus.
78
+
79
+ The message bus class is the entry point into all the features of the
80
+ library. It sets up a connection to the DBus daemon and exposes an
81
+ interface to send and receive messages and expose services.
82
+
83
+ This class is not meant to be used directly by users. For more information,
84
+ see the documentation for the implementation of the message bus you plan to
85
+ use.
86
+
87
+ :param bus_type: The type of bus to connect to. Affects the search path for
88
+ the bus address.
89
+ :type bus_type: :class:`BusType <dbus_fast.BusType>`
90
+ :param bus_address: A specific bus address to connect to. Should not be
91
+ used under normal circumstances.
92
+ :type bus_address: str
93
+ :param ProxyObject: The proxy object implementation for this message bus.
94
+ Must be passed in by an implementation that supports the high-level client.
95
+ :type ProxyObject: Type[:class:`BaseProxyObject
96
+ <dbus_fast.proxy_object.BaseProxyObject>`]
97
+
98
+ :ivar unique_name: The unique name of the message bus connection. It will
99
+ be :class:`None` until the message bus connects.
100
+ :vartype unique_name: str
101
+ :ivar connected: True if this message bus is expected to be able to send
102
+ and receive messages.
103
+ :vartype connected: bool
104
+ """
105
+
106
+ __slots__ = (
107
+ "_ProxyObject",
108
+ "_bus_address",
109
+ "_disconnected",
110
+ "_fd",
111
+ "_high_level_client_initialized",
112
+ "_machine_id",
113
+ "_match_rules",
114
+ "_method_return_handlers",
115
+ "_name_owner_match_rule",
116
+ "_name_owners",
117
+ "_negotiate_unix_fd",
118
+ "_path_exports",
119
+ "_serial",
120
+ "_sock",
121
+ "_stream",
122
+ "_user_disconnect",
123
+ "_user_message_handlers",
124
+ "unique_name",
125
+ )
126
+
127
+ def __init__(
128
+ self,
129
+ bus_address: str | None = None,
130
+ bus_type: BusType = BusType.SESSION,
131
+ ProxyObject: type[BaseProxyObject] | None = None,
132
+ negotiate_unix_fd: bool = False,
133
+ ) -> None:
134
+ self.unique_name: str | None = None
135
+ self._disconnected = False
136
+ self._negotiate_unix_fd = negotiate_unix_fd
137
+
138
+ # True if the user disconnected himself, so don't throw errors out of
139
+ # the main loop.
140
+ self._user_disconnect = False
141
+
142
+ self._method_return_handlers: dict[
143
+ int, Callable[[Message | None, Exception | None], None]
144
+ ] = {}
145
+ self._serial = 0
146
+ self._user_message_handlers: list[
147
+ Callable[[Message], Message | bool | None]
148
+ ] = []
149
+ # the key is the name and the value is the unique name of the owner.
150
+ # This cache is kept up to date by the NameOwnerChanged signal and is
151
+ # used to route messages to the correct proxy object. (used for the
152
+ # high level client only)
153
+ self._name_owners: dict[str, str] = {}
154
+ # used for the high level service
155
+ self._path_exports: dict[str, dict[str, ServiceInterface]] = {}
156
+ self._bus_address = (
157
+ parse_address(bus_address)
158
+ if bus_address
159
+ else parse_address(get_bus_address(bus_type))
160
+ )
161
+ # the bus implementations need this rule for the high level client to
162
+ # work correctly.
163
+ self._name_owner_match_rule = "sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',path='/org/freedesktop/DBus',member='NameOwnerChanged'"
164
+ # _match_rules: the keys are match rules and the values are ref counts
165
+ # (used for the high level client only)
166
+ self._match_rules: dict[str, int] = {}
167
+ self._high_level_client_initialized = False
168
+ self._ProxyObject = ProxyObject
169
+
170
+ # machine id is lazy loaded
171
+ self._machine_id: int | None = None
172
+ self._sock: socket.socket | None = None
173
+ self._fd: int | None = None
174
+ self._stream: io.BufferedRWPair | None = None
175
+
176
+ self._setup_socket()
177
+
178
+ @property
179
+ def connected(self) -> bool:
180
+ if self.unique_name is None or self._disconnected or self._user_disconnect:
181
+ return False
182
+ return True
183
+
184
+ def export(self, path: str, interface: ServiceInterface) -> None:
185
+ """Export the service interface on this message bus to make it available
186
+ to other clients.
187
+
188
+ :param path: The object path to export this interface on.
189
+ :type path: str
190
+ :param interface: The service interface to export.
191
+ :type interface: :class:`ServiceInterface
192
+ <dbus_fast.service.ServiceInterface>`
193
+
194
+ :raises:
195
+ - :class:`InvalidObjectPathError <dbus_fast.InvalidObjectPathError>` - If the given object path is not valid.
196
+ - :class:`ValueError` - If an interface with this name is already exported on the message bus at this path
197
+ """
198
+ assert_object_path_valid(path)
199
+ if not isinstance(interface, ServiceInterface):
200
+ raise TypeError("interface must be a ServiceInterface")
201
+
202
+ if path not in self._path_exports:
203
+ self._path_exports[path] = {}
204
+ elif interface.name in self._path_exports[path]:
205
+ raise ValueError(
206
+ f'An interface with this name is already exported on this bus at path "{path}": "{interface.name}"'
207
+ )
208
+
209
+ ServiceInterface._add_bus(interface, self, self._make_method_handler)
210
+ self._path_exports[path][interface.name] = interface
211
+ self._emit_interface_added(path, interface)
212
+
213
+ def unexport(
214
+ self, path: str, interface: ServiceInterface | str | None = None
215
+ ) -> None:
216
+ """Unexport the path or service interface to make it no longer
217
+ available to clients.
218
+
219
+ :param path: The object path to unexport.
220
+ :type path: str
221
+ :param interface: The interface instance or the name of the interface
222
+ to unexport. If ``None``, unexport every interface on the path.
223
+ :type interface: :class:`ServiceInterface
224
+ <dbus_fast.service.ServiceInterface>` or str or None
225
+
226
+ :raises:
227
+ - :class:`InvalidObjectPathError <dbus_fast.InvalidObjectPathError>` - If the given object path is not valid.
228
+ """
229
+ assert_object_path_valid(path)
230
+ interface_name: str | None
231
+ if interface is None:
232
+ interface_name = None
233
+ elif type(interface) is str:
234
+ interface_name = interface
235
+ elif isinstance(interface, ServiceInterface):
236
+ interface_name = interface.name
237
+ else:
238
+ raise TypeError(
239
+ f"interface must be a ServiceInterface or interface name not {type(interface)}"
240
+ )
241
+
242
+ if (interfaces := self._path_exports.get(path)) is None:
243
+ return
244
+ removed_interface_names: list[str] = []
245
+
246
+ if interface_name is not None:
247
+ if (removed_interface := interfaces.pop(interface_name, None)) is None:
248
+ return
249
+ removed_interface_names.append(interface_name)
250
+ if not interfaces:
251
+ del self._path_exports[path]
252
+ ServiceInterface._remove_bus(removed_interface, self)
253
+ else:
254
+ del self._path_exports[path]
255
+ for removed_interface in interfaces.values():
256
+ removed_interface_names.append(removed_interface.name)
257
+ ServiceInterface._remove_bus(removed_interface, self)
258
+
259
+ self._emit_interface_removed(path, removed_interface_names)
260
+
261
+ def introspect(
262
+ self,
263
+ bus_name: str,
264
+ path: str,
265
+ callback: Callable[[intr.Node | None, Exception | None], None],
266
+ check_callback_type: bool = True,
267
+ validate_property_names: bool = True,
268
+ ) -> None:
269
+ """Get introspection data for the node at the given path from the given
270
+ bus name.
271
+
272
+ Calls the standard ``org.freedesktop.DBus.Introspectable.Introspect``
273
+ on the bus for the path.
274
+
275
+ :param bus_name: The name to introspect.
276
+ :type bus_name: str
277
+ :param path: The path to introspect.
278
+ :type path: str
279
+ :param callback: A callback that will be called with the introspection
280
+ data as a :class:`Node <dbus_fast.introspection.Node>`.
281
+ :type callback: :class:`Callable`
282
+ :param check_callback_type: Whether to check callback type or not.
283
+ :type check_callback_type: bool
284
+ :param validate_property_names: Whether to validate property names or not.
285
+ :type validate_property_names: bool
286
+
287
+ :raises:
288
+ - :class:`InvalidObjectPathError <dbus_fast.InvalidObjectPathError>` - If the given object path is not valid.
289
+ - :class:`InvalidBusNameError <dbus_fast.InvalidBusNameError>` - If the given bus name is not valid.
290
+ """
291
+ if check_callback_type:
292
+ BaseMessageBus._check_callback_type(callback)
293
+
294
+ def reply_notify(reply: Message | None, err: Exception | None) -> None:
295
+ try:
296
+ BaseMessageBus._check_method_return(reply, err, "s")
297
+ result = intr.Node.parse(
298
+ reply.body[0], # type: ignore[union-attr]
299
+ validate_property_names=validate_property_names,
300
+ )
301
+ except Exception as e:
302
+ callback(None, e)
303
+ return
304
+
305
+ callback(result, None)
306
+
307
+ self._call(
308
+ Message(
309
+ destination=bus_name,
310
+ path=path,
311
+ interface="org.freedesktop.DBus.Introspectable",
312
+ member="Introspect",
313
+ ),
314
+ reply_notify,
315
+ )
316
+
317
+ def _emit_interface_added(self, path: str, interface: ServiceInterface) -> None:
318
+ """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesAdded`` signal.
319
+
320
+ This signal is intended to be used to alert clients when
321
+ a new interface has been added.
322
+
323
+ :param path: Path of exported object.
324
+ :type path: str
325
+ :param interface: Exported service interface.
326
+ :type interface: :class:`ServiceInterface
327
+ <dbus_fast.service.ServiceInterface>`
328
+ """
329
+ if self._disconnected:
330
+ return
331
+
332
+ def get_properties_callback(
333
+ interface: ServiceInterface,
334
+ result: Any,
335
+ user_data: Any,
336
+ e: Exception | None,
337
+ ) -> None:
338
+ if e is not None:
339
+ try:
340
+ raise e
341
+ except Exception:
342
+ _LOGGER.error(
343
+ "An exception ocurred when emitting ObjectManager.InterfacesAdded for %s. "
344
+ "Some properties will not be included in the signal.",
345
+ interface.name,
346
+ exc_info=True,
347
+ )
348
+
349
+ body = {interface.name: result}
350
+
351
+ self.send(
352
+ Message.new_signal(
353
+ path=path,
354
+ interface="org.freedesktop.DBus.ObjectManager",
355
+ member="InterfacesAdded",
356
+ signature="oa{sa{sv}}",
357
+ body=[path, body],
358
+ )
359
+ )
360
+
361
+ ServiceInterface._get_all_property_values(interface, get_properties_callback)
362
+
363
+ def _emit_interface_removed(self, path: str, removed_interfaces: list[str]) -> None:
364
+ """Emit the ``org.freedesktop.DBus.ObjectManager.InterfacesRemoved` signal.
365
+
366
+ This signal is intended to be used to alert clients when
367
+ a interface has been removed.
368
+
369
+ :param path: Path of removed (unexported) object.
370
+ :type path: str
371
+ :param removed_interfaces: List of unexported service interfaces.
372
+ :type removed_interfaces: list[str]
373
+ """
374
+ if self._disconnected:
375
+ return
376
+
377
+ self.send(
378
+ Message.new_signal(
379
+ path=path,
380
+ interface="org.freedesktop.DBus.ObjectManager",
381
+ member="InterfacesRemoved",
382
+ signature="oas",
383
+ body=[path, removed_interfaces],
384
+ )
385
+ )
386
+
387
+ def request_name(
388
+ self,
389
+ name: str,
390
+ flags: NameFlag = NameFlag.NONE,
391
+ callback: None
392
+ | (Callable[[RequestNameReply | None, Exception | None], None]) = None,
393
+ check_callback_type: bool = True,
394
+ ) -> None:
395
+ """Request that this message bus owns the given name.
396
+
397
+ :param name: The name to request.
398
+ :type name: str
399
+ :param flags: Name flags that affect the behavior of the name request.
400
+ :type flags: :class:`NameFlag <dbus_fast.NameFlag>`
401
+ :param callback: A callback that will be called with the reply of the
402
+ request as a :class:`RequestNameReply <dbus_fast.RequestNameReply>`.
403
+ :type callback: :class:`Callable`
404
+
405
+ :raises:
406
+ - :class:`InvalidBusNameError <dbus_fast.InvalidBusNameError>` - If the given bus name is not valid.
407
+ """
408
+ assert_bus_name_valid(name)
409
+
410
+ if callback is not None and check_callback_type:
411
+ BaseMessageBus._check_callback_type(callback)
412
+
413
+ if type(flags) is not NameFlag:
414
+ flags = NameFlag(flags)
415
+
416
+ message = Message(
417
+ destination="org.freedesktop.DBus",
418
+ path="/org/freedesktop/DBus",
419
+ interface="org.freedesktop.DBus",
420
+ member="RequestName",
421
+ signature="su",
422
+ body=[name, flags],
423
+ )
424
+
425
+ if callback is None:
426
+ self._call(message, None)
427
+ return
428
+
429
+ def reply_notify(reply: Message | None, err: Exception | None) -> None:
430
+ try:
431
+ BaseMessageBus._check_method_return(reply, err, "u")
432
+ result = RequestNameReply(reply.body[0]) # type: ignore[union-attr]
433
+ except Exception as e:
434
+ callback(None, e)
435
+ return
436
+
437
+ callback(result, None)
438
+
439
+ self._call(message, reply_notify)
440
+
441
+ def release_name(
442
+ self,
443
+ name: str,
444
+ callback: None
445
+ | (Callable[[ReleaseNameReply | None, Exception | None], None]) = None,
446
+ check_callback_type: bool = True,
447
+ ) -> None:
448
+ """Request that this message bus release the given name.
449
+
450
+ :param name: The name to release.
451
+ :type name: str
452
+ :param callback: A callback that will be called with the reply of the
453
+ release request as a :class:`ReleaseNameReply
454
+ <dbus_fast.ReleaseNameReply>`.
455
+ :type callback: :class:`Callable`
456
+
457
+ :raises:
458
+ - :class:`InvalidBusNameError <dbus_fast.InvalidBusNameError>` - If the given bus name is not valid.
459
+ """
460
+ assert_bus_name_valid(name)
461
+
462
+ if callback is not None and check_callback_type:
463
+ BaseMessageBus._check_callback_type(callback)
464
+
465
+ message = Message(
466
+ destination="org.freedesktop.DBus",
467
+ path="/org/freedesktop/DBus",
468
+ interface="org.freedesktop.DBus",
469
+ member="ReleaseName",
470
+ signature="s",
471
+ body=[name],
472
+ )
473
+
474
+ if callback is None:
475
+ self._call(message, None)
476
+ return
477
+
478
+ def reply_notify(reply: Message | None, err: Exception | None) -> None:
479
+ try:
480
+ BaseMessageBus._check_method_return(reply, err, "u")
481
+ result = ReleaseNameReply(reply.body[0]) # type: ignore[union-attr]
482
+ except Exception as e:
483
+ callback(None, e)
484
+ return
485
+
486
+ callback(result, None)
487
+
488
+ self._call(message, reply_notify)
489
+
490
+ def get_proxy_object(
491
+ self, bus_name: str, path: str, introspection: intr.Node | str | ET.Element
492
+ ) -> BaseProxyObject:
493
+ """Get a proxy object for the path exported on the bus that owns the
494
+ name. The object is expected to export the interfaces and nodes
495
+ specified in the introspection data.
496
+
497
+ This is the entry point into the high-level client.
498
+
499
+ :param bus_name: The name on the bus to get the proxy object for.
500
+ :type bus_name: str
501
+ :param path: The path on the client for the proxy object.
502
+ :type path: str
503
+ :param introspection: XML introspection data used to build the
504
+ interfaces on the proxy object.
505
+ :type introspection: :class:`Node <dbus_fast.introspection.Node>` or str or :class:`ElementTree`
506
+
507
+ :returns: A proxy object for the given path on the given name.
508
+ :rtype: :class:`BaseProxyObject <dbus_fast.proxy_object.BaseProxyObject>`
509
+
510
+ :raises:
511
+ - :class:`InvalidBusNameError <dbus_fast.InvalidBusNameError>` - If the given bus name is not valid.
512
+ - :class:`InvalidObjectPathError <dbus_fast.InvalidObjectPathError>` - If the given object path is not valid.
513
+ - :class:`InvalidIntrospectionError <dbus_fast.InvalidIntrospectionError>` - If the introspection data for the node is not valid.
514
+ """
515
+ if self._ProxyObject is None:
516
+ raise Exception(
517
+ "the message bus implementation did not provide a proxy object class"
518
+ )
519
+
520
+ self._init_high_level_client()
521
+
522
+ return self._ProxyObject(bus_name, path, introspection, self)
523
+
524
+ def disconnect(self) -> None:
525
+ """Disconnect the message bus by closing the underlying connection asynchronously.
526
+
527
+ All pending and future calls will error with a connection error.
528
+ """
529
+ self._user_disconnect = True
530
+ if self._sock:
531
+ try:
532
+ self._sock.shutdown(socket.SHUT_RDWR)
533
+ except Exception:
534
+ _LOGGER.warning("could not shut down socket", exc_info=True)
535
+
536
+ def next_serial(self) -> int:
537
+ """Get the next serial for this bus. This can be used as the ``serial``
538
+ attribute of a :class:`Message <dbus_fast.Message>` to manually handle
539
+ the serial of messages.
540
+
541
+ :returns: The next serial for the bus.
542
+ :rtype: int
543
+ """
544
+ self._serial += 1
545
+ return self._serial
546
+
547
+ def add_message_handler(
548
+ self, handler: Callable[[Message], Message | bool | None]
549
+ ) -> None:
550
+ """Add a custom message handler for incoming messages.
551
+
552
+ The handler should be a callable that takes a :class:`Message
553
+ <dbus_fast.Message>`. If the message is a method call, you may return
554
+ another Message as a reply and it will be marked as handled. You may
555
+ also return ``True`` to mark the message as handled without sending a
556
+ reply.
557
+
558
+ :param handler: A handler that will be run for every message the bus
559
+ connection received.
560
+ :type handler: :class:`Callable` or None
561
+ """
562
+ error_text = "a message handler must be callable with a single parameter"
563
+ if not callable(handler):
564
+ raise TypeError(error_text)
565
+
566
+ handler_signature = inspect.signature(handler)
567
+ if len(handler_signature.parameters) != 1:
568
+ raise TypeError(error_text)
569
+
570
+ self._user_message_handlers.append(handler)
571
+
572
+ def remove_message_handler(
573
+ self, handler: Callable[[Message], Message | bool | None]
574
+ ) -> None:
575
+ """Remove a message handler that was previously added by
576
+ :func:`add_message_handler()
577
+ <dbus_fast.message_bus.BaseMessageBus.add_message_handler>`.
578
+
579
+ :param handler: A message handler.
580
+ :type handler: :class:`Callable`
581
+ """
582
+ for i, h in enumerate(self._user_message_handlers):
583
+ if h == handler:
584
+ del self._user_message_handlers[i]
585
+ return
586
+
587
+ def send(self, msg: Message) -> None:
588
+ """Asynchronously send a message on the message bus.
589
+
590
+ :param msg: The message to send.
591
+ :type msg: :class:`Message <dbus_fast.Message>`
592
+ """
593
+ raise NotImplementedError(
594
+ 'the "send" method must be implemented in the inheriting class'
595
+ )
596
+
597
+ def _finalize(self, err: Exception | None) -> None:
598
+ """should be called after the socket disconnects with the disconnection
599
+ error to clean up resources and put the bus in a disconnected state"""
600
+ if self._disconnected:
601
+ return
602
+
603
+ self._disconnected = True
604
+
605
+ self._stream.close()
606
+ self._sock.close()
607
+
608
+ for handler in self._method_return_handlers.values():
609
+ try:
610
+ handler(None, err)
611
+ except Exception:
612
+ _LOGGER.warning(
613
+ "a message handler threw an exception on shutdown", exc_info=True
614
+ )
615
+
616
+ self._method_return_handlers.clear()
617
+
618
+ for path in list(self._path_exports):
619
+ self.unexport(path)
620
+
621
+ self._user_message_handlers.clear()
622
+
623
+ def _interface_signal_notify(
624
+ self,
625
+ interface: ServiceInterface,
626
+ interface_name: str,
627
+ member: str,
628
+ signature: str,
629
+ body: list[Any],
630
+ unix_fds: list[int] = [],
631
+ ) -> None:
632
+ path: str | None = None
633
+ for p, ifaces in self._path_exports.items():
634
+ for i in ifaces.values():
635
+ if i is interface:
636
+ path = p
637
+
638
+ if path is None:
639
+ raise Exception(
640
+ "Could not find interface on bus (this is a bug in dbus-fast)"
641
+ )
642
+
643
+ self.send(
644
+ Message.new_signal(
645
+ path=path,
646
+ interface=interface_name,
647
+ member=member,
648
+ signature=signature,
649
+ body=body,
650
+ unix_fds=unix_fds,
651
+ )
652
+ )
653
+
654
+ def _introspect_export_path(self, path: str) -> intr.Node:
655
+ assert_object_path_valid(path)
656
+
657
+ if (interfaces := self._path_exports.get(path)) is not None:
658
+ node = intr.Node.default(path)
659
+ for interface in interfaces.values():
660
+ node.interfaces.append(interface.introspect())
661
+ else:
662
+ node = intr.Node(path)
663
+
664
+ children = set()
665
+
666
+ for export_path in self._path_exports:
667
+ if not export_path.startswith(path):
668
+ continue
669
+
670
+ child_path = export_path.split(path, maxsplit=1)[1]
671
+ if path != "/" and child_path and child_path[0] != "/":
672
+ continue
673
+
674
+ child_path = child_path.lstrip("/")
675
+ child_name = child_path.split("/", maxsplit=1)[0]
676
+
677
+ children.add(child_name)
678
+
679
+ node.nodes = [intr.Node(name) for name in children if name]
680
+
681
+ return node
682
+
683
+ def _setup_socket(self) -> None:
684
+ last_err: Exception | None = None
685
+
686
+ for transport, options in self._bus_address:
687
+ filename: bytes | str | None = None
688
+ ip_addr = ""
689
+ ip_port = 0
690
+
691
+ with ExitStack() as stack:
692
+ if transport == "unix":
693
+ self._sock = stack.enter_context(
694
+ socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
695
+ )
696
+ self._stream = stack.enter_context(self._sock.makefile("rwb"))
697
+ self._fd = self._sock.fileno()
698
+
699
+ if "path" in options:
700
+ filename = options["path"]
701
+ elif "abstract" in options:
702
+ filename = b"\0" + options["abstract"].encode()
703
+ else:
704
+ raise InvalidAddressError(
705
+ "got unix transport with unknown path specifier"
706
+ )
707
+
708
+ try:
709
+ self._sock.connect(filename)
710
+ self._sock.setblocking(False)
711
+ except Exception as e:
712
+ last_err = e
713
+ else:
714
+ stack.pop_all() # responsibility to close sockets is deferred
715
+ return
716
+
717
+ elif transport == "tcp":
718
+ self._sock = stack.enter_context(
719
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM)
720
+ )
721
+ self._stream = stack.enter_context(self._sock.makefile("rwb"))
722
+ self._fd = self._sock.fileno()
723
+
724
+ if "host" in options:
725
+ ip_addr = options["host"]
726
+ if "port" in options:
727
+ ip_port = int(options["port"])
728
+
729
+ try:
730
+ self._sock.connect((ip_addr, ip_port))
731
+ self._sock.setblocking(False)
732
+ except Exception as e:
733
+ last_err = e
734
+ else:
735
+ stack.pop_all()
736
+ return
737
+
738
+ else:
739
+ raise InvalidAddressError(
740
+ f"got unknown address transport: {transport}"
741
+ )
742
+
743
+ if last_err is None: # pragma: no branch
744
+ # Should not normally happen, but just in case
745
+ raise TypeError("empty list of bus addresses given") # pragma: no cover
746
+
747
+ raise last_err
748
+
749
+ def _reply_notify(
750
+ self,
751
+ msg: Message,
752
+ callback: Callable[[Message | None, Exception | None], None],
753
+ reply: Message | None,
754
+ err: Exception | None,
755
+ ) -> None:
756
+ """Callback on reply."""
757
+ if reply and msg.destination and reply.sender:
758
+ self._name_owners[msg.destination] = reply.sender
759
+ callback(reply, err)
760
+
761
+ def _call(
762
+ self,
763
+ msg: Message,
764
+ callback: Callable[[Message | None, Exception | None], None] | None,
765
+ ) -> None:
766
+ if not msg.serial:
767
+ msg.serial = self.next_serial()
768
+
769
+ # Make sure the return reply handler is installed
770
+ # before sending the message to avoid a race condition
771
+ # where the reply is lost in case the backend can
772
+ # send it right away.
773
+ if (reply_expected := _expects_reply(msg)) and callback is not None:
774
+ self._method_return_handlers[msg.serial] = partial(
775
+ self._reply_notify, msg, callback
776
+ )
777
+
778
+ self.send(msg)
779
+
780
+ if not reply_expected and callback is not None:
781
+ callback(None, None)
782
+
783
+ @staticmethod
784
+ def _check_callback_type(callback: Callable) -> None:
785
+ """Raise a TypeError if the user gives an invalid callback as a parameter"""
786
+
787
+ text = "a callback must be callable with two parameters"
788
+
789
+ if not callable(callback):
790
+ raise TypeError(text)
791
+
792
+ fn_signature = inspect.signature(callback)
793
+ if len(fn_signature.parameters) != 2:
794
+ raise TypeError(text)
795
+
796
+ @staticmethod
797
+ def _check_method_return(
798
+ msg: Message | None, err: Exception | None, signature: str
799
+ ) -> None:
800
+ if err:
801
+ raise err
802
+ if msg is None:
803
+ raise DBusError(
804
+ ErrorType.INTERNAL_ERROR, "invalid message type for method call", msg
805
+ )
806
+ if msg.message_type == MessageType.METHOD_RETURN and msg.signature == signature:
807
+ return
808
+ if msg.message_type == MessageType.ERROR:
809
+ raise DBusError._from_message(msg)
810
+ raise DBusError(
811
+ ErrorType.INTERNAL_ERROR, "invalid message type for method call", msg
812
+ )
813
+
814
+ def _process_message(self, msg: _Message) -> None:
815
+ """Process a message received from the message bus."""
816
+ handled = False
817
+ for user_handler in self._user_message_handlers:
818
+ try:
819
+ if result := user_handler(msg):
820
+ if type(result) is Message:
821
+ self.send(result)
822
+ handled = True
823
+ break
824
+ except DBusError as e:
825
+ if msg.message_type is MESSAGE_TYPE_CALL:
826
+ self.send(e._as_message(msg))
827
+ handled = True
828
+ break
829
+ _LOGGER.exception("A message handler raised an exception: %s", e)
830
+ except Exception as e:
831
+ _LOGGER.exception("A message handler raised an exception: %s", e)
832
+ if msg.message_type is MESSAGE_TYPE_CALL:
833
+ self.send(
834
+ Message.new_error(
835
+ msg,
836
+ ErrorType.INTERNAL_ERROR,
837
+ f"An internal error occurred: {e}.\n{traceback.format_exc()}",
838
+ )
839
+ )
840
+ handled = True
841
+ break
842
+
843
+ if msg.message_type is MESSAGE_TYPE_SIGNAL:
844
+ if (
845
+ msg.member == "NameOwnerChanged"
846
+ and msg.sender == "org.freedesktop.DBus"
847
+ and msg.path == "/org/freedesktop/DBus"
848
+ and msg.interface == "org.freedesktop.DBus"
849
+ ):
850
+ name = msg.body[0]
851
+ if new_owner := msg.body[2]:
852
+ self._name_owners[name] = new_owner
853
+ elif name in self._name_owners:
854
+ del self._name_owners[name]
855
+ return
856
+
857
+ if msg.message_type is MESSAGE_TYPE_CALL:
858
+ if not handled:
859
+ handler = self._find_message_handler(msg)
860
+ if not _expects_reply(msg):
861
+ if handler:
862
+ handler(msg, BLOCK_UNEXPECTED_REPLY) # type: ignore[arg-type]
863
+ else:
864
+ _LOGGER.error(
865
+ '"%s.%s" with signature "%s" could not be found',
866
+ msg.interface,
867
+ msg.member,
868
+ msg.signature,
869
+ )
870
+ return
871
+
872
+ send_reply = SendReply(self, msg)
873
+ with send_reply:
874
+ if handler:
875
+ handler(msg, send_reply)
876
+ else:
877
+ send_reply(
878
+ Message.new_error(
879
+ msg,
880
+ ErrorType.UNKNOWN_METHOD,
881
+ f"{msg.interface}.{msg.member} with signature "
882
+ f'"{msg.signature}" could not be found',
883
+ )
884
+ )
885
+ return
886
+
887
+ # An ERROR or a METHOD_RETURN
888
+ return_handler = self._method_return_handlers.get(msg.reply_serial)
889
+ if return_handler is not None:
890
+ if not handled:
891
+ return_handler(msg, None)
892
+ del self._method_return_handlers[msg.reply_serial]
893
+
894
+ def _callback_method_handler(
895
+ self,
896
+ interface: ServiceInterface,
897
+ method: _Method,
898
+ msg: Message,
899
+ send_reply: SendReply,
900
+ ) -> None:
901
+ """This is the callback that will be called when a method call is."""
902
+ args = ServiceInterface._c_msg_body_to_args(msg) if msg.unix_fds else msg.body
903
+ result = method.fn(interface, *args)
904
+ if send_reply is BLOCK_UNEXPECTED_REPLY or _expects_reply(msg) is False:
905
+ return
906
+ body_fds = ServiceInterface._c_fn_result_to_body(
907
+ result,
908
+ method.out_signature_tree,
909
+ self._negotiate_unix_fd,
910
+ )
911
+ send_reply(
912
+ Message(
913
+ message_type=MessageType.METHOD_RETURN,
914
+ reply_serial=msg.serial,
915
+ destination=msg.sender,
916
+ signature=method.out_signature,
917
+ body=body_fds[0],
918
+ unix_fds=body_fds[1],
919
+ )
920
+ )
921
+
922
+ def _make_method_handler(
923
+ self, interface: ServiceInterface, method: _Method
924
+ ) -> HandlerType:
925
+ return partial(self._callback_method_handler, interface, method)
926
+
927
+ def _find_message_handler(self, msg: _Message) -> HandlerType | None:
928
+ """Find the message handler for for METHOD_CALL messages."""
929
+ if TYPE_CHECKING:
930
+ assert msg.member is not None
931
+ assert msg.path is not None
932
+
933
+ if msg.interface is not None and "org.freedesktop.DBus." in msg.interface:
934
+ if (
935
+ msg.interface == "org.freedesktop.DBus.Introspectable"
936
+ and msg.member == "Introspect"
937
+ and msg.signature == ""
938
+ ):
939
+ return self._default_introspect_handler
940
+
941
+ if msg.interface == "org.freedesktop.DBus.Properties":
942
+ return self._default_properties_handler
943
+
944
+ if msg.interface == "org.freedesktop.DBus.Peer":
945
+ if msg.member == "Ping" and msg.signature == "":
946
+ return self._default_ping_handler
947
+ if msg.member == "GetMachineId" and msg.signature == "":
948
+ return self._default_get_machine_id_handler
949
+
950
+ if (
951
+ msg.interface == "org.freedesktop.DBus.ObjectManager"
952
+ and msg.member == "GetManagedObjects"
953
+ ):
954
+ return self._default_get_managed_objects_handler
955
+
956
+ if (interfaces := self._path_exports.get(msg.path)) is None:
957
+ return None
958
+
959
+ if msg.interface is None:
960
+ return self._find_any_message_handler_matching_signature(interfaces, msg)
961
+
962
+ if (interface := interfaces.get(msg.interface)) is not None and (
963
+ handler := ServiceInterface._get_enabled_handler_by_name_signature(
964
+ interface, self, msg.member, msg.signature
965
+ )
966
+ ) is not None:
967
+ return handler
968
+
969
+ return None
970
+
971
+ def _find_any_message_handler_matching_signature(
972
+ self, interfaces: dict[str, ServiceInterface], msg: _Message
973
+ ) -> HandlerType | None:
974
+ # No interface, so we need to search all interfaces for the method
975
+ # with a matching signature
976
+ for interface in interfaces.values():
977
+ if (
978
+ handler := ServiceInterface._get_enabled_handler_by_name_signature(
979
+ interface, self, msg.member, msg.signature
980
+ )
981
+ ) is not None:
982
+ return handler
983
+ return None
984
+
985
+ def _default_introspect_handler(self, msg: Message, send_reply: SendReply) -> None:
986
+ if TYPE_CHECKING:
987
+ assert msg.path is not None
988
+ introspection = self._introspect_export_path(msg.path).tostring()
989
+ send_reply(Message.new_method_return(msg, "s", [introspection]))
990
+
991
+ def _default_ping_handler(self, msg: Message, send_reply: SendReply) -> None:
992
+ send_reply(Message.new_method_return(msg))
993
+
994
+ def _send_machine_id_reply(self, msg: Message, send_reply: SendReply) -> None:
995
+ send_reply(Message.new_method_return(msg, "s", [self._machine_id]))
996
+
997
+ def _default_get_machine_id_handler(
998
+ self, msg: Message, send_reply: SendReply
999
+ ) -> None:
1000
+ if self._machine_id:
1001
+ self._send_machine_id_reply(msg, send_reply)
1002
+ return
1003
+
1004
+ def reply_handler(reply: Message | None, err: Exception | None) -> None:
1005
+ if err or reply is None:
1006
+ # the bus has been disconnected, cannot send a reply
1007
+ return
1008
+
1009
+ if reply.message_type == MessageType.METHOD_RETURN:
1010
+ self._machine_id = reply.body[0]
1011
+ self._send_machine_id_reply(msg, send_reply)
1012
+ elif (
1013
+ reply.message_type == MessageType.ERROR and reply.error_name is not None
1014
+ ):
1015
+ send_reply(Message.new_error(msg, reply.error_name, str(reply.body)))
1016
+ else:
1017
+ send_reply(
1018
+ Message.new_error(msg, ErrorType.FAILED, "could not get machine_id")
1019
+ )
1020
+
1021
+ self._call(
1022
+ Message(
1023
+ destination="org.freedesktop.DBus",
1024
+ path="/org/freedesktop/DBus",
1025
+ interface="org.freedesktop.DBus.Peer",
1026
+ member="GetMachineId",
1027
+ ),
1028
+ reply_handler,
1029
+ )
1030
+
1031
+ def _default_get_managed_objects_handler(
1032
+ self, msg: Message, send_reply: SendReply
1033
+ ) -> None:
1034
+ result_signature = "a{oa{sa{sv}}}"
1035
+ error_handled = False
1036
+
1037
+ def is_result_complete() -> bool:
1038
+ if not result:
1039
+ return True
1040
+ for n, interfaces in result.items():
1041
+ for value in interfaces.values():
1042
+ if value is None:
1043
+ return False
1044
+
1045
+ return True
1046
+
1047
+ if TYPE_CHECKING:
1048
+ assert msg.path is not None
1049
+
1050
+ nodes = [
1051
+ node
1052
+ for node in self._path_exports
1053
+ if msg.path == "/" or node.startswith(msg.path + "/")
1054
+ ]
1055
+
1056
+ # first build up the result object to know when it's complete
1057
+ result: dict[str, dict[str, Any]] = {
1058
+ node: dict.fromkeys(self._path_exports[node]) for node in nodes
1059
+ }
1060
+
1061
+ if is_result_complete():
1062
+ send_reply(Message.new_method_return(msg, result_signature, [result]))
1063
+ return
1064
+
1065
+ def get_all_properties_callback(
1066
+ interface: ServiceInterface, values: Any, node: str, err: Exception | None
1067
+ ) -> None:
1068
+ nonlocal error_handled
1069
+ if err is not None:
1070
+ if not error_handled:
1071
+ error_handled = True
1072
+ send_reply.send_error(err)
1073
+ return
1074
+
1075
+ result[node][interface.name] = values
1076
+
1077
+ if is_result_complete():
1078
+ send_reply(Message.new_method_return(msg, result_signature, [result]))
1079
+
1080
+ for node in nodes:
1081
+ for interface in self._path_exports[node].values():
1082
+ ServiceInterface._get_all_property_values(
1083
+ interface, get_all_properties_callback, node
1084
+ )
1085
+
1086
+ def _default_properties_handler(self, msg: Message, send_reply: SendReply) -> None:
1087
+ methods = {"Get": "ss", "Set": "ssv", "GetAll": "s"}
1088
+ if msg.member not in methods or methods[msg.member] != msg.signature:
1089
+ raise DBusError(
1090
+ ErrorType.UNKNOWN_METHOD,
1091
+ f'properties interface doesn\'t have method "{msg.member}" with signature "{msg.signature}"',
1092
+ )
1093
+
1094
+ interface_name = msg.body[0]
1095
+ if interface_name == "":
1096
+ raise DBusError(
1097
+ ErrorType.NOT_SUPPORTED,
1098
+ "getting and setting properties with an empty interface string is not supported yet",
1099
+ )
1100
+
1101
+ if msg.path not in self._path_exports:
1102
+ raise DBusError(
1103
+ ErrorType.UNKNOWN_OBJECT, f'no interfaces at path: "{msg.path}"'
1104
+ )
1105
+
1106
+ if (interface := self._path_exports[msg.path].get(interface_name)) is None:
1107
+ if interface_name in [
1108
+ "org.freedesktop.DBus.Properties",
1109
+ "org.freedesktop.DBus.Introspectable",
1110
+ "org.freedesktop.DBus.Peer",
1111
+ "org.freedesktop.DBus.ObjectManager",
1112
+ ]:
1113
+ # the standard interfaces do not have properties
1114
+ if msg.member == "Get" or msg.member == "Set":
1115
+ prop_name = msg.body[1]
1116
+ raise DBusError(
1117
+ ErrorType.UNKNOWN_PROPERTY,
1118
+ f'interface "{interface_name}" does not have property "{prop_name}"',
1119
+ )
1120
+ if msg.member == "GetAll":
1121
+ send_reply(Message.new_method_return(msg, "a{sv}", [{}]))
1122
+ return
1123
+ assert False
1124
+ raise DBusError(
1125
+ ErrorType.UNKNOWN_INTERFACE,
1126
+ f'could not find an interface "{interface_name}" at path: "{msg.path}"',
1127
+ )
1128
+
1129
+ properties = ServiceInterface._get_properties(interface)
1130
+
1131
+ if msg.member == "Get" or msg.member == "Set":
1132
+ prop_name = msg.body[1]
1133
+ match = [
1134
+ prop
1135
+ for prop in properties
1136
+ if prop.name == prop_name and not prop.disabled
1137
+ ]
1138
+ if not match:
1139
+ raise DBusError(
1140
+ ErrorType.UNKNOWN_PROPERTY,
1141
+ f'interface "{interface_name}" does not have property "{prop_name}"',
1142
+ )
1143
+
1144
+ prop = match[0]
1145
+ if msg.member == "Get":
1146
+ if not prop.access.readable():
1147
+ raise DBusError(
1148
+ ErrorType.UNKNOWN_PROPERTY,
1149
+ "the property does not have read access",
1150
+ )
1151
+
1152
+ def get_property_callback(
1153
+ interface: ServiceInterface,
1154
+ prop: _Property,
1155
+ prop_value: Any,
1156
+ err: Exception | None,
1157
+ ) -> None:
1158
+ try:
1159
+ if err is not None:
1160
+ send_reply.send_error(err)
1161
+ return
1162
+
1163
+ body, unix_fds = replace_fds_with_idx(
1164
+ prop.signature, [prop_value]
1165
+ )
1166
+
1167
+ send_reply(
1168
+ Message.new_method_return(
1169
+ msg,
1170
+ "v",
1171
+ [Variant(prop.signature, body[0])],
1172
+ unix_fds=unix_fds,
1173
+ )
1174
+ )
1175
+ except Exception as e:
1176
+ send_reply.send_error(e)
1177
+
1178
+ ServiceInterface._get_property_value(
1179
+ interface, prop, get_property_callback
1180
+ )
1181
+
1182
+ elif msg.member == "Set":
1183
+ if not prop.access.writable():
1184
+ raise DBusError(
1185
+ ErrorType.PROPERTY_READ_ONLY, "the property is readonly"
1186
+ )
1187
+ value = msg.body[2]
1188
+ if value.signature != prop.signature:
1189
+ raise DBusError(
1190
+ ErrorType.INVALID_SIGNATURE,
1191
+ f'wrong signature for property. expected "{prop.signature}"',
1192
+ )
1193
+ assert prop.prop_setter
1194
+
1195
+ def set_property_callback(
1196
+ interface: ServiceInterface, prop: _Property, err: Exception | None
1197
+ ) -> None:
1198
+ if err is not None:
1199
+ send_reply.send_error(err)
1200
+ return
1201
+ send_reply(Message.new_method_return(msg))
1202
+
1203
+ body = replace_idx_with_fds(
1204
+ value.signature, [value.value], msg.unix_fds
1205
+ )
1206
+ ServiceInterface._set_property_value(
1207
+ interface, prop, body[0], set_property_callback
1208
+ )
1209
+
1210
+ elif msg.member == "GetAll":
1211
+
1212
+ def get_all_properties_callback(
1213
+ interface: ServiceInterface,
1214
+ values: Any,
1215
+ user_data: Any,
1216
+ err: Exception | None,
1217
+ ) -> None:
1218
+ if err is not None:
1219
+ send_reply.send_error(err)
1220
+ return
1221
+ body, unix_fds = replace_fds_with_idx("a{sv}", [values])
1222
+ send_reply(
1223
+ Message.new_method_return(msg, "a{sv}", body, unix_fds=unix_fds)
1224
+ )
1225
+
1226
+ ServiceInterface._get_all_property_values(
1227
+ interface, get_all_properties_callback
1228
+ )
1229
+
1230
+ else:
1231
+ assert False
1232
+
1233
+ def _init_high_level_client(self) -> None:
1234
+ """The high level client is initialized when the first proxy object is
1235
+ gotten. Currently just sets up the match rules for the name owner cache
1236
+ so signals can be routed to the right objects."""
1237
+ if self._high_level_client_initialized:
1238
+ return
1239
+ self._high_level_client_initialized = True
1240
+
1241
+ def add_match_notify(msg: Message | None, err: Exception | None) -> None:
1242
+ if err:
1243
+ _LOGGER.error(
1244
+ f'add match request failed. match="{self._name_owner_match_rule}", {err}'
1245
+ )
1246
+ elif msg is not None and msg.message_type == MessageType.ERROR:
1247
+ _LOGGER.error(
1248
+ f'add match request failed. match="{self._name_owner_match_rule}", {msg.body[0]}'
1249
+ )
1250
+
1251
+ self._call(
1252
+ Message(
1253
+ destination="org.freedesktop.DBus",
1254
+ interface="org.freedesktop.DBus",
1255
+ path="/org/freedesktop/DBus",
1256
+ member="AddMatch",
1257
+ signature="s",
1258
+ body=[self._name_owner_match_rule],
1259
+ ),
1260
+ add_match_notify,
1261
+ )
1262
+
1263
+ def _add_match_rule(self, match_rule: str) -> None:
1264
+ """Add a match rule. Match rules added by this function are refcounted
1265
+ and must be removed by _remove_match_rule(). This is for use in the
1266
+ high level client only."""
1267
+ if match_rule == self._name_owner_match_rule:
1268
+ return
1269
+
1270
+ if match_rule in self._match_rules:
1271
+ self._match_rules[match_rule] += 1
1272
+ return
1273
+
1274
+ self._match_rules[match_rule] = 1
1275
+
1276
+ def add_match_notify(msg: Message | None, err: Exception | None) -> None:
1277
+ if err:
1278
+ _LOGGER.error(f'add match request failed. match="{match_rule}", {err}')
1279
+ elif msg is not None and msg.message_type == MessageType.ERROR:
1280
+ _LOGGER.error(
1281
+ f'add match request failed. match="{match_rule}", {msg.body[0]}'
1282
+ )
1283
+
1284
+ self._call(
1285
+ Message(
1286
+ destination="org.freedesktop.DBus",
1287
+ interface="org.freedesktop.DBus",
1288
+ path="/org/freedesktop/DBus",
1289
+ member="AddMatch",
1290
+ signature="s",
1291
+ body=[match_rule],
1292
+ ),
1293
+ add_match_notify,
1294
+ )
1295
+
1296
+ def _remove_match_rule(self, match_rule: str) -> None:
1297
+ """Remove a match rule added with _add_match_rule(). This is for use in
1298
+ the high level client only."""
1299
+ if match_rule == self._name_owner_match_rule:
1300
+ return
1301
+
1302
+ if match_rule in self._match_rules:
1303
+ self._match_rules[match_rule] -= 1
1304
+ if self._match_rules[match_rule] > 0:
1305
+ return
1306
+
1307
+ del self._match_rules[match_rule]
1308
+
1309
+ def remove_match_notify(msg: Message | None, err: Exception | None) -> None:
1310
+ if self._disconnected:
1311
+ return
1312
+
1313
+ if err:
1314
+ _LOGGER.error(
1315
+ f'remove match request failed. match="{match_rule}", {err}'
1316
+ )
1317
+ elif msg is not None and msg.message_type == MessageType.ERROR:
1318
+ _LOGGER.error(
1319
+ f'remove match request failed. match="{match_rule}", {msg.body[0]}'
1320
+ )
1321
+
1322
+ self._call(
1323
+ Message(
1324
+ destination="org.freedesktop.DBus",
1325
+ interface="org.freedesktop.DBus",
1326
+ path="/org/freedesktop/DBus",
1327
+ member="RemoveMatch",
1328
+ signature="s",
1329
+ body=[match_rule],
1330
+ ),
1331
+ remove_match_notify,
1332
+ )