chore: initial port
This commit is contained in:
@@ -1 +1,39 @@
|
||||
__version__ = "1.0.0"
|
||||
from . import aio, glib, introspection, message_bus, proxy_object, service
|
||||
from .constants import (
|
||||
ArgDirection,
|
||||
BusType,
|
||||
ErrorType,
|
||||
MessageFlag,
|
||||
MessageType,
|
||||
NameFlag,
|
||||
PropertyAccess,
|
||||
ReleaseNameReply,
|
||||
RequestNameReply,
|
||||
)
|
||||
from .errors import (
|
||||
AuthError,
|
||||
DBusError,
|
||||
InterfaceNotFoundError,
|
||||
InvalidAddressError,
|
||||
InvalidBusNameError,
|
||||
InvalidInterfaceNameError,
|
||||
InvalidIntrospectionError,
|
||||
InvalidMemberNameError,
|
||||
InvalidMessageError,
|
||||
InvalidObjectPathError,
|
||||
InvalidSignatureError,
|
||||
SignalDisabledError,
|
||||
SignatureBodyMismatchError,
|
||||
)
|
||||
from .message import Message
|
||||
from .signature import SignatureTree, SignatureType, Variant
|
||||
from .validators import (
|
||||
assert_bus_name_valid,
|
||||
assert_interface_name_valid,
|
||||
assert_member_name_valid,
|
||||
assert_object_path_valid,
|
||||
is_bus_name_valid,
|
||||
is_interface_name_valid,
|
||||
is_member_name_valid,
|
||||
is_object_path_valid,
|
||||
)
|
||||
|
||||
8
src/dbus_fast/__version__.py
Normal file
8
src/dbus_fast/__version__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
__title__ = "dbus_next"
|
||||
__description__ = "A zero-dependency DBus library for Python with asyncio support"
|
||||
__url__ = "https://github.com/altdesktop/python-dbus-next"
|
||||
__version__ = "0.2.3"
|
||||
__author__ = "Tony Crisci"
|
||||
__author_email__ = "tony@dubstepdish.com"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2019 Tony Crisci"
|
||||
0
src/dbus_fast/_private/__init__.py
Normal file
0
src/dbus_fast/_private/__init__.py
Normal file
106
src/dbus_fast/_private/address.py
Normal file
106
src/dbus_fast/_private/address.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
import re
|
||||
from urllib.parse import unquote
|
||||
|
||||
from ..constants import BusType
|
||||
from ..errors import InvalidAddressError
|
||||
|
||||
invalid_address_chars_re = re.compile(r"[^-0-9A-Za-z_/.%]")
|
||||
|
||||
|
||||
def parse_address(address_str):
|
||||
addresses = []
|
||||
|
||||
for address in filter(lambda a: a, address_str.split(";")):
|
||||
if address.find(":") == -1:
|
||||
raise InvalidAddressError("address did not contain a transport")
|
||||
|
||||
transport, opt_string = address.split(":", 1)
|
||||
options = {}
|
||||
|
||||
for kv in filter(lambda s: s, opt_string.split(",")):
|
||||
if kv.find("=") == -1:
|
||||
raise InvalidAddressError("address option did not contain a value")
|
||||
k, v = kv.split("=", 1)
|
||||
if invalid_address_chars_re.search(v):
|
||||
raise InvalidAddressError("address contains invalid characters")
|
||||
# XXX the actual unquote rules are simpler than this
|
||||
v = unquote(v)
|
||||
options[k] = v
|
||||
|
||||
addresses.append((transport, options))
|
||||
|
||||
if not addresses:
|
||||
raise InvalidAddressError(
|
||||
f'address string contained no addresses: "{address_str}"'
|
||||
)
|
||||
|
||||
return addresses
|
||||
|
||||
|
||||
def get_system_bus_address():
|
||||
if "DBUS_SYSTEM_BUS_ADDRESS" in os.environ:
|
||||
return os.environ["DBUS_SYSTEM_BUS_ADDRESS"]
|
||||
else:
|
||||
return "unix:path=/var/run/dbus/system_bus_socket"
|
||||
|
||||
|
||||
display_re = re.compile(r".*:([0-9]+)\.?.*")
|
||||
remove_quotes_re = re.compile(r"""^['"]?(.*?)['"]?$""")
|
||||
|
||||
|
||||
def get_session_bus_address():
|
||||
if "DBUS_SESSION_BUS_ADDRESS" in os.environ:
|
||||
return os.environ["DBUS_SESSION_BUS_ADDRESS"]
|
||||
|
||||
home = os.environ["HOME"]
|
||||
if "DISPLAY" not in os.environ:
|
||||
raise InvalidAddressError(
|
||||
"DBUS_SESSION_BUS_ADDRESS not set and could not get DISPLAY environment variable to get bus address"
|
||||
)
|
||||
|
||||
display = os.environ["DISPLAY"]
|
||||
try:
|
||||
display = display_re.search(display).group(1)
|
||||
except Exception:
|
||||
raise InvalidAddressError(
|
||||
f"DBUS_SESSION_BUS_ADDRESS not set and could not parse DISPLAY environment variable to get bus address: {display}"
|
||||
)
|
||||
|
||||
# XXX: this will block but they're very small files and fs operations
|
||||
# should be fairly reliable. fix this by passing in an async func to read
|
||||
# the file for each io backend.
|
||||
machine_id = None
|
||||
with open("/var/lib/dbus/machine-id") as f:
|
||||
machine_id = f.read().rstrip()
|
||||
|
||||
dbus_info_file_name = f"{home}/.dbus/session-bus/{machine_id}-{display}"
|
||||
dbus_info = None
|
||||
try:
|
||||
with open(dbus_info_file_name) as f:
|
||||
dbus_info = f.read().rstrip()
|
||||
except Exception:
|
||||
raise InvalidAddressError(
|
||||
f"could not open dbus info file: {dbus_info_file_name}"
|
||||
)
|
||||
|
||||
for line in dbus_info.split("\n"):
|
||||
if line.strip().startswith("DBUS_SESSION_BUS_ADDRESS="):
|
||||
_, addr = line.split("=", 1)
|
||||
if not addr:
|
||||
raise InvalidAddressError(
|
||||
f"DBUS_SESSION_BUS_ADDRESS variable not set correctly in dbus info file: {dbus_info_file_name}"
|
||||
)
|
||||
addr = remove_quotes_re.search(addr).group(1)
|
||||
return addr
|
||||
|
||||
raise InvalidAddressError("could not find dbus session bus address")
|
||||
|
||||
|
||||
def get_bus_address(bus_type):
|
||||
if bus_type == BusType.SESSION:
|
||||
return get_session_bus_address()
|
||||
elif bus_type == BusType.SYSTEM:
|
||||
return get_system_bus_address()
|
||||
else:
|
||||
raise Exception("got unknown bus type: {bus_type}")
|
||||
18
src/dbus_fast/_private/constants.py
Normal file
18
src/dbus_fast/_private/constants.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from enum import Enum
|
||||
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
LITTLE_ENDIAN = ord("l")
|
||||
BIG_ENDIAN = ord("B")
|
||||
|
||||
|
||||
class HeaderField(Enum):
|
||||
PATH = 1
|
||||
INTERFACE = 2
|
||||
MEMBER = 3
|
||||
ERROR_NAME = 4
|
||||
REPLY_SERIAL = 5
|
||||
DESTINATION = 6
|
||||
SENDER = 7
|
||||
SIGNATURE = 8
|
||||
UNIX_FDS = 9
|
||||
161
src/dbus_fast/_private/marshaller.py
Normal file
161
src/dbus_fast/_private/marshaller.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from struct import pack
|
||||
|
||||
from ..signature import SignatureTree
|
||||
|
||||
|
||||
class Marshaller:
|
||||
def __init__(self, signature, body):
|
||||
self.signature_tree = SignatureTree._get(signature)
|
||||
self.signature_tree.verify(body)
|
||||
self.buffer = bytearray()
|
||||
self.body = body
|
||||
|
||||
self.writers = {
|
||||
"y": self.write_byte,
|
||||
"b": self.write_boolean,
|
||||
"n": self.write_int16,
|
||||
"q": self.write_uint16,
|
||||
"i": self.write_int32,
|
||||
"u": self.write_uint32,
|
||||
"x": self.write_int64,
|
||||
"t": self.write_uint64,
|
||||
"d": self.write_double,
|
||||
"h": self.write_uint32,
|
||||
"o": self.write_string,
|
||||
"s": self.write_string,
|
||||
"g": self.write_signature,
|
||||
"a": self.write_array,
|
||||
"(": self.write_struct,
|
||||
"{": self.write_dict_entry,
|
||||
"v": self.write_variant,
|
||||
}
|
||||
|
||||
def align(self, n):
|
||||
offset = n - len(self.buffer) % n
|
||||
if offset == 0 or offset == n:
|
||||
return 0
|
||||
self.buffer.extend(bytes(offset))
|
||||
return offset
|
||||
|
||||
def write_byte(self, byte, _=None):
|
||||
self.buffer.append(byte)
|
||||
return 1
|
||||
|
||||
def write_boolean(self, boolean, _=None):
|
||||
if boolean:
|
||||
return self.write_uint32(1)
|
||||
else:
|
||||
return self.write_uint32(0)
|
||||
|
||||
def write_int16(self, int16, _=None):
|
||||
written = self.align(2)
|
||||
self.buffer.extend(pack("<h", int16))
|
||||
return written + 2
|
||||
|
||||
def write_uint16(self, uint16, _=None):
|
||||
written = self.align(2)
|
||||
self.buffer.extend(pack("<H", uint16))
|
||||
return written + 2
|
||||
|
||||
def write_int32(self, int32, _):
|
||||
written = self.align(4)
|
||||
self.buffer.extend(pack("<i", int32))
|
||||
return written + 4
|
||||
|
||||
def write_uint32(self, uint32, _=None):
|
||||
written = self.align(4)
|
||||
self.buffer.extend(pack("<I", uint32))
|
||||
return written + 4
|
||||
|
||||
def write_int64(self, int64, _=None):
|
||||
written = self.align(8)
|
||||
self.buffer.extend(pack("<q", int64))
|
||||
return written + 8
|
||||
|
||||
def write_uint64(self, uint64, _=None):
|
||||
written = self.align(8)
|
||||
self.buffer.extend(pack("<Q", uint64))
|
||||
return written + 8
|
||||
|
||||
def write_double(self, double, _=None):
|
||||
written = self.align(8)
|
||||
self.buffer.extend(pack("<d", double))
|
||||
return written + 8
|
||||
|
||||
def write_signature(self, signature, _=None):
|
||||
signature = signature.encode()
|
||||
signature_len = len(signature)
|
||||
self.buffer.append(signature_len)
|
||||
self.buffer.extend(signature)
|
||||
self.buffer.append(0)
|
||||
return signature_len + 2
|
||||
|
||||
def write_string(self, value, _=None):
|
||||
value = value.encode()
|
||||
value_len = len(value)
|
||||
written = self.write_uint32(value_len)
|
||||
self.buffer.extend(value)
|
||||
written += value_len
|
||||
self.buffer.append(0)
|
||||
written += 1
|
||||
return written
|
||||
|
||||
def write_variant(self, variant, _=None):
|
||||
written = self.write_signature(variant.signature)
|
||||
written += self.write_single(variant.type, variant.value)
|
||||
return written
|
||||
|
||||
def write_array(self, array, type_):
|
||||
# TODO max array size is 64MiB (67108864 bytes)
|
||||
written = self.align(4)
|
||||
# length placeholder
|
||||
offset = len(self.buffer)
|
||||
written += self.write_uint32(0)
|
||||
child_type = type_.children[0]
|
||||
|
||||
if child_type.token in "xtd{(":
|
||||
# the first alignment is not included in array size
|
||||
written += self.align(8)
|
||||
|
||||
array_len = 0
|
||||
if child_type.token == "{":
|
||||
for key, value in array.items():
|
||||
array_len += self.write_dict_entry([key, value], child_type)
|
||||
elif child_type.token == "y":
|
||||
array_len = len(array)
|
||||
self.buffer.extend(array)
|
||||
else:
|
||||
for value in array:
|
||||
array_len += self.write_single(child_type, value)
|
||||
|
||||
array_len_packed = pack("<I", array_len)
|
||||
for i in range(offset, offset + 4):
|
||||
self.buffer[i] = array_len_packed[i - offset]
|
||||
|
||||
return written + array_len
|
||||
|
||||
def write_struct(self, array, type_):
|
||||
written = self.align(8)
|
||||
for i, value in enumerate(array):
|
||||
written += self.write_single(type_.children[i], value)
|
||||
return written
|
||||
|
||||
def write_dict_entry(self, dict_entry, type_):
|
||||
written = self.align(8)
|
||||
written += self.write_single(type_.children[0], dict_entry[0])
|
||||
written += self.write_single(type_.children[1], dict_entry[1])
|
||||
return written
|
||||
|
||||
def write_single(self, type_, body):
|
||||
t = type_.token
|
||||
|
||||
if t not in self.writers:
|
||||
raise NotImplementedError(f'type isnt implemented yet: "{t}"')
|
||||
|
||||
return self.writers[t](body, type_)
|
||||
|
||||
def marshall(self):
|
||||
self.buffer.clear()
|
||||
for i, type_ in enumerate(self.signature_tree.types):
|
||||
self.write_single(type_, self.body[i])
|
||||
return self.buffer
|
||||
315
src/dbus_fast/_private/unmarshaller.py
Normal file
315
src/dbus_fast/_private/unmarshaller.py
Normal file
@@ -0,0 +1,315 @@
|
||||
import array
|
||||
import socket
|
||||
from codecs import decode
|
||||
from struct import unpack_from
|
||||
|
||||
from ..constants import MessageFlag, MessageType
|
||||
from ..errors import InvalidMessageError
|
||||
from ..message import Message
|
||||
from ..signature import SignatureTree, Variant
|
||||
from .constants import BIG_ENDIAN, LITTLE_ENDIAN, PROTOCOL_VERSION, HeaderField
|
||||
|
||||
MAX_UNIX_FDS = 16
|
||||
|
||||
|
||||
class MarshallerStreamEndError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Unmarshaller:
|
||||
def __init__(self, stream, sock=None):
|
||||
self.unix_fds = []
|
||||
self.buf = bytearray()
|
||||
self.offset = 0
|
||||
self.stream = stream
|
||||
self.sock = sock
|
||||
self.endian = None
|
||||
self.message = None
|
||||
|
||||
self.readers = {
|
||||
"y": self.read_byte,
|
||||
"b": self.read_boolean,
|
||||
"n": self.read_int16,
|
||||
"q": self.read_uint16,
|
||||
"i": self.read_int32,
|
||||
"u": self.read_uint32,
|
||||
"x": self.read_int64,
|
||||
"t": self.read_uint64,
|
||||
"d": self.read_double,
|
||||
"h": self.read_uint32,
|
||||
"o": self.read_string,
|
||||
"s": self.read_string,
|
||||
"g": self.read_signature,
|
||||
"a": self.read_array,
|
||||
"(": self.read_struct,
|
||||
"{": self.read_dict_entry,
|
||||
"v": self.read_variant,
|
||||
}
|
||||
|
||||
def read(self, n, prefetch=False):
|
||||
"""
|
||||
Read from underlying socket into buffer and advance offset accordingly.
|
||||
|
||||
:arg n:
|
||||
Number of bytes to read. If not enough bytes are available in the
|
||||
buffer, read more from it.
|
||||
:arg prefetch:
|
||||
Do not update current offset after reading.
|
||||
|
||||
:returns:
|
||||
Previous offset (before reading). To get the actual read bytes,
|
||||
use the returned value and self.buf.
|
||||
"""
|
||||
|
||||
def read_sock(length):
|
||||
"""reads from the socket, storing any fds sent and handling errors
|
||||
from the read itself"""
|
||||
if self.sock is not None:
|
||||
unix_fd_list = array.array("i")
|
||||
|
||||
try:
|
||||
msg, ancdata, *_ = self.sock.recvmsg(
|
||||
length, socket.CMSG_LEN(MAX_UNIX_FDS * unix_fd_list.itemsize)
|
||||
)
|
||||
except BlockingIOError:
|
||||
raise MarshallerStreamEndError()
|
||||
|
||||
for level, type_, data in ancdata:
|
||||
if not (level == socket.SOL_SOCKET and type_ == socket.SCM_RIGHTS):
|
||||
continue
|
||||
unix_fd_list.frombytes(
|
||||
data[: len(data) - (len(data) % unix_fd_list.itemsize)]
|
||||
)
|
||||
self.unix_fds.extend(list(unix_fd_list))
|
||||
|
||||
return msg
|
||||
else:
|
||||
return self.stream.read(length)
|
||||
|
||||
# store previously read data in a buffer so we can resume on socket
|
||||
# interruptions
|
||||
missing_bytes = n - (len(self.buf) - self.offset)
|
||||
if missing_bytes > 0:
|
||||
data = read_sock(missing_bytes)
|
||||
if data == b"":
|
||||
raise EOFError()
|
||||
elif data is None:
|
||||
raise MarshallerStreamEndError()
|
||||
self.buf.extend(data)
|
||||
if len(data) != missing_bytes:
|
||||
raise MarshallerStreamEndError()
|
||||
prev = self.offset
|
||||
if not prefetch:
|
||||
self.offset += n
|
||||
return prev
|
||||
|
||||
@staticmethod
|
||||
def _padding(offset, align):
|
||||
"""
|
||||
Get padding bytes to get to the next align bytes mark.
|
||||
|
||||
For any align value, the correct padding formula is:
|
||||
|
||||
(align - (offset % align)) % align
|
||||
|
||||
However, if align is a power of 2 (always the case here), the slow MOD
|
||||
operator can be replaced by a bitwise AND:
|
||||
|
||||
(align - (offset & (align - 1))) & (align - 1)
|
||||
|
||||
Which can be simplified to:
|
||||
|
||||
(-offset) & (align - 1)
|
||||
"""
|
||||
return (-offset) & (align - 1)
|
||||
|
||||
def align(self, n):
|
||||
padding = self._padding(self.offset, n)
|
||||
if padding > 0:
|
||||
self.read(padding)
|
||||
|
||||
def read_byte(self, _=None):
|
||||
return self.buf[self.read(1)]
|
||||
|
||||
def read_boolean(self, _=None):
|
||||
data = self.read_uint32()
|
||||
if data:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def read_int16(self, _=None):
|
||||
return self.read_ctype("h", 2)
|
||||
|
||||
def read_uint16(self, _=None):
|
||||
return self.read_ctype("H", 2)
|
||||
|
||||
def read_int32(self, _=None):
|
||||
return self.read_ctype("i", 4)
|
||||
|
||||
def read_uint32(self, _=None):
|
||||
return self.read_ctype("I", 4)
|
||||
|
||||
def read_int64(self, _=None):
|
||||
return self.read_ctype("q", 8)
|
||||
|
||||
def read_uint64(self, _=None):
|
||||
return self.read_ctype("Q", 8)
|
||||
|
||||
def read_double(self, _=None):
|
||||
return self.read_ctype("d", 8)
|
||||
|
||||
def read_ctype(self, fmt, size):
|
||||
self.align(size)
|
||||
if self.endian == LITTLE_ENDIAN:
|
||||
fmt = "<" + fmt
|
||||
else:
|
||||
fmt = ">" + fmt
|
||||
o = self.read(size)
|
||||
return unpack_from(fmt, self.buf, o)[0]
|
||||
|
||||
def read_string(self, _=None):
|
||||
str_length = self.read_uint32()
|
||||
o = self.read(str_length + 1) # read terminating '\0' byte as well
|
||||
# avoid buffer copies when slicing
|
||||
str_mem_slice = memoryview(self.buf)[o : o + str_length]
|
||||
return decode(str_mem_slice)
|
||||
|
||||
def read_signature(self, _=None):
|
||||
signature_len = self.read_byte()
|
||||
o = self.read(signature_len + 1) # read terminating '\0' byte as well
|
||||
# avoid buffer copies when slicing
|
||||
sig_mem_slice = memoryview(self.buf)[o : o + signature_len]
|
||||
return decode(sig_mem_slice)
|
||||
|
||||
def read_variant(self, _=None):
|
||||
signature = self.read_signature()
|
||||
signature_tree = SignatureTree._get(signature)
|
||||
value = self.read_argument(signature_tree.types[0])
|
||||
return Variant(signature_tree, value)
|
||||
|
||||
def read_struct(self, type_):
|
||||
self.align(8)
|
||||
|
||||
result = []
|
||||
for child_type in type_.children:
|
||||
result.append(self.read_argument(child_type))
|
||||
|
||||
return result
|
||||
|
||||
def read_dict_entry(self, type_):
|
||||
self.align(8)
|
||||
|
||||
key = self.read_argument(type_.children[0])
|
||||
value = self.read_argument(type_.children[1])
|
||||
|
||||
return key, value
|
||||
|
||||
def read_array(self, type_):
|
||||
self.align(4)
|
||||
array_length = self.read_uint32()
|
||||
|
||||
child_type = type_.children[0]
|
||||
if child_type.token in "xtd{(":
|
||||
# the first alignment is not included in the array size
|
||||
self.align(8)
|
||||
|
||||
beginning_offset = self.offset
|
||||
|
||||
result = None
|
||||
if child_type.token == "{":
|
||||
result = {}
|
||||
while self.offset - beginning_offset < array_length:
|
||||
key, value = self.read_dict_entry(child_type)
|
||||
result[key] = value
|
||||
elif child_type.token == "y":
|
||||
o = self.read(array_length)
|
||||
# avoid buffer copies when slicing
|
||||
array_mem_slice = memoryview(self.buf)[o : o + array_length]
|
||||
result = array_mem_slice.tobytes()
|
||||
else:
|
||||
result = []
|
||||
while self.offset - beginning_offset < array_length:
|
||||
result.append(self.read_argument(child_type))
|
||||
|
||||
return result
|
||||
|
||||
def read_argument(self, type_):
|
||||
t = type_.token
|
||||
|
||||
if t not in self.readers:
|
||||
raise Exception(f'dont know how to read yet: "{t}"')
|
||||
|
||||
return self.readers[t](type_)
|
||||
|
||||
def _unmarshall(self):
|
||||
self.offset = 0
|
||||
self.read(16, prefetch=True)
|
||||
self.endian = self.read_byte()
|
||||
if self.endian != LITTLE_ENDIAN and self.endian != BIG_ENDIAN:
|
||||
raise InvalidMessageError("Expecting endianness as the first byte")
|
||||
message_type = MessageType(self.read_byte())
|
||||
flags = MessageFlag(self.read_byte())
|
||||
|
||||
protocol_version = self.read_byte()
|
||||
|
||||
if protocol_version != PROTOCOL_VERSION:
|
||||
raise InvalidMessageError(
|
||||
f"got unknown protocol version: {protocol_version}"
|
||||
)
|
||||
|
||||
body_len = self.read_uint32()
|
||||
serial = self.read_uint32()
|
||||
|
||||
header_len = self.read_uint32()
|
||||
msg_len = header_len + self._padding(header_len, 8) + body_len
|
||||
self.read(msg_len, prefetch=True)
|
||||
# backtrack offset since header array length needs to be read again
|
||||
self.offset -= 4
|
||||
|
||||
header_fields = {}
|
||||
for field_struct in self.read_argument(SignatureTree._get("a(yv)").types[0]):
|
||||
field = HeaderField(field_struct[0])
|
||||
header_fields[field.name] = field_struct[1].value
|
||||
|
||||
self.align(8)
|
||||
|
||||
path = header_fields.get(HeaderField.PATH.name)
|
||||
interface = header_fields.get(HeaderField.INTERFACE.name)
|
||||
member = header_fields.get(HeaderField.MEMBER.name)
|
||||
error_name = header_fields.get(HeaderField.ERROR_NAME.name)
|
||||
reply_serial = header_fields.get(HeaderField.REPLY_SERIAL.name)
|
||||
destination = header_fields.get(HeaderField.DESTINATION.name)
|
||||
sender = header_fields.get(HeaderField.SENDER.name)
|
||||
signature = header_fields.get(HeaderField.SIGNATURE.name, "")
|
||||
signature_tree = SignatureTree._get(signature)
|
||||
# unix_fds = header_fields.get(HeaderField.UNIX_FDS.name, 0)
|
||||
|
||||
body = []
|
||||
|
||||
if body_len:
|
||||
for type_ in signature_tree.types:
|
||||
body.append(self.read_argument(type_))
|
||||
|
||||
self.message = Message(
|
||||
destination=destination,
|
||||
path=path,
|
||||
interface=interface,
|
||||
member=member,
|
||||
message_type=message_type,
|
||||
flags=flags,
|
||||
error_name=error_name,
|
||||
reply_serial=reply_serial,
|
||||
sender=sender,
|
||||
unix_fds=self.unix_fds,
|
||||
signature=signature_tree,
|
||||
body=body,
|
||||
serial=serial,
|
||||
)
|
||||
|
||||
def unmarshall(self):
|
||||
try:
|
||||
self._unmarshall()
|
||||
return self.message
|
||||
except MarshallerStreamEndError:
|
||||
return None
|
||||
172
src/dbus_fast/_private/util.py
Normal file
172
src/dbus_fast/_private/util.py
Normal file
@@ -0,0 +1,172 @@
|
||||
import ast
|
||||
import inspect
|
||||
from typing import Any, List, Union
|
||||
|
||||
from ..signature import SignatureTree, Variant
|
||||
|
||||
|
||||
def signature_contains_type(
|
||||
signature: Union[str, SignatureTree], body: List[Any], token: str
|
||||
) -> bool:
|
||||
"""For a given signature and body, check to see if it contains any members
|
||||
with the given token"""
|
||||
if type(signature) is str:
|
||||
signature = SignatureTree._get(signature)
|
||||
|
||||
queue = []
|
||||
contains_variants = False
|
||||
for st in signature.types:
|
||||
queue.append(st)
|
||||
|
||||
while True:
|
||||
if not queue:
|
||||
break
|
||||
st = queue.pop()
|
||||
if st.token == token:
|
||||
return True
|
||||
elif st.token == "v":
|
||||
contains_variants = True
|
||||
queue.extend(st.children)
|
||||
|
||||
if not contains_variants:
|
||||
return False
|
||||
|
||||
for member in body:
|
||||
queue.append(member)
|
||||
|
||||
while True:
|
||||
if not queue:
|
||||
return False
|
||||
member = queue.pop()
|
||||
if type(member) is Variant and signature_contains_type(
|
||||
member.signature, [member.value], token
|
||||
):
|
||||
return True
|
||||
elif type(member) is list:
|
||||
queue.extend(member)
|
||||
elif type(member) is dict:
|
||||
queue.extend(member.values())
|
||||
|
||||
|
||||
def replace_fds_with_idx(
|
||||
signature: Union[str, SignatureTree], body: List[Any]
|
||||
) -> (List[Any], List[int]):
|
||||
"""Take the high level body format and convert it into the low level body
|
||||
format. Type 'h' refers directly to the fd in the body. Replace that with
|
||||
an index and return the corresponding list of unix fds that can be set on
|
||||
the Message"""
|
||||
if type(signature) is str:
|
||||
signature = SignatureTree._get(signature)
|
||||
|
||||
if not signature_contains_type(signature, body, "h"):
|
||||
return body, []
|
||||
|
||||
unix_fds = []
|
||||
|
||||
def _replace(fd):
|
||||
try:
|
||||
return unix_fds.index(fd)
|
||||
except ValueError:
|
||||
unix_fds.append(fd)
|
||||
return len(unix_fds) - 1
|
||||
|
||||
_replace_fds(body, signature.types, _replace)
|
||||
|
||||
return body, unix_fds
|
||||
|
||||
|
||||
def replace_idx_with_fds(
|
||||
signature: Union[str, SignatureTree], body: List[Any], unix_fds: List[int]
|
||||
) -> List[Any]:
|
||||
"""Take the low level body format and return the high level body format.
|
||||
Type 'h' refers to an index in the unix_fds array. Replace those with the
|
||||
actual file descriptor or `None` if one does not exist."""
|
||||
if type(signature) is str:
|
||||
signature = SignatureTree._get(signature)
|
||||
|
||||
if not signature_contains_type(signature, body, "h"):
|
||||
return body
|
||||
|
||||
def _replace(idx):
|
||||
try:
|
||||
return unix_fds[idx]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
_replace_fds(body, signature.types, _replace)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def parse_annotation(annotation: str) -> str:
|
||||
"""
|
||||
Because of PEP 563, if `from __future__ import annotations` is used in code
|
||||
or on Python version >=3.10 where this is the default, return annotations
|
||||
from the `inspect` module will return annotations as "forward definitions".
|
||||
In this case, we must eval the result which we do only when given a string
|
||||
constant.
|
||||
"""
|
||||
|
||||
def raise_value_error():
|
||||
raise ValueError(
|
||||
f"service annotations must be a string constant (got {annotation})"
|
||||
)
|
||||
|
||||
if not annotation or annotation is inspect.Signature.empty:
|
||||
return ""
|
||||
if type(annotation) is not str:
|
||||
raise_value_error()
|
||||
try:
|
||||
body = ast.parse(annotation).body
|
||||
if len(body) == 1 and type(body[0].value) is ast.Constant:
|
||||
if type(body[0].value.value) is not str:
|
||||
raise_value_error()
|
||||
return body[0].value.value
|
||||
except SyntaxError:
|
||||
pass
|
||||
|
||||
return annotation
|
||||
|
||||
|
||||
def _replace_fds(body_obj: List[Any], children, replace_fn):
|
||||
"""Replace any type 'h' with the value returned by replace_fn() given the
|
||||
value of the fd field. This is used by the high level interfaces which
|
||||
allow type 'h' to be the fd directly instead of an index in an external
|
||||
array such as in the spec."""
|
||||
for index, st in enumerate(children):
|
||||
if not any(sig in st.signature for sig in "hv"):
|
||||
continue
|
||||
if st.signature == "h":
|
||||
body_obj[index] = replace_fn(body_obj[index])
|
||||
elif st.token == "a":
|
||||
if st.children[0].token == "{":
|
||||
_replace_fds(body_obj[index], st.children, replace_fn)
|
||||
else:
|
||||
for i, child in enumerate(body_obj[index]):
|
||||
if st.signature == "ah":
|
||||
body_obj[index][i] = replace_fn(child)
|
||||
else:
|
||||
_replace_fds([child], st.children, replace_fn)
|
||||
elif st.token in "(":
|
||||
_replace_fds(body_obj[index], st.children, replace_fn)
|
||||
elif st.token in "{":
|
||||
for key, value in list(body_obj.items()):
|
||||
body_obj.pop(key)
|
||||
if st.children[0].signature == "h":
|
||||
key = replace_fn(key)
|
||||
if st.children[1].signature == "h":
|
||||
value = replace_fn(value)
|
||||
else:
|
||||
_replace_fds([value], [st.children[1]], replace_fn)
|
||||
body_obj[key] = value
|
||||
|
||||
elif st.signature == "v":
|
||||
if body_obj[index].signature == "h":
|
||||
body_obj[index].value = replace_fn(body_obj[index].value)
|
||||
else:
|
||||
_replace_fds(
|
||||
[body_obj[index].value], [body_obj[index].type], replace_fn
|
||||
)
|
||||
|
||||
elif st.children:
|
||||
_replace_fds(body_obj[index], st.children, replace_fn)
|
||||
2
src/dbus_fast/aio/__init__.py
Normal file
2
src/dbus_fast/aio/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .message_bus import MessageBus
|
||||
from .proxy_object import ProxyInterface, ProxyObject
|
||||
463
src/dbus_fast/aio/message_bus.py
Normal file
463
src/dbus_fast/aio/message_bus.py
Normal file
@@ -0,0 +1,463 @@
|
||||
import array
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
from asyncio import Queue
|
||||
from copy import copy
|
||||
from typing import Optional
|
||||
|
||||
from .. import introspection as intr
|
||||
from .._private.unmarshaller import Unmarshaller
|
||||
from ..auth import Authenticator, AuthExternal
|
||||
from ..constants import (
|
||||
BusType,
|
||||
MessageFlag,
|
||||
MessageType,
|
||||
NameFlag,
|
||||
ReleaseNameReply,
|
||||
RequestNameReply,
|
||||
)
|
||||
from ..errors import AuthError
|
||||
from ..message import Message
|
||||
from ..message_bus import BaseMessageBus
|
||||
from ..service import ServiceInterface
|
||||
from .proxy_object import ProxyObject
|
||||
|
||||
|
||||
def _future_set_exception(fut, exc):
|
||||
if fut is not None and not fut.done():
|
||||
fut.set_exception(exc)
|
||||
|
||||
|
||||
def _future_set_result(fut, result):
|
||||
if fut is not None and not fut.done():
|
||||
fut.set_result(result)
|
||||
|
||||
|
||||
class _MessageWriter:
|
||||
def __init__(self, bus):
|
||||
self.messages = Queue()
|
||||
self.negotiate_unix_fd = bus._negotiate_unix_fd
|
||||
self.bus = bus
|
||||
self.sock = bus._sock
|
||||
self.loop = bus._loop
|
||||
self.buf = None
|
||||
self.fd = bus._fd
|
||||
self.offset = 0
|
||||
self.unix_fds = None
|
||||
self.fut = None
|
||||
|
||||
def write_callback(self):
|
||||
try:
|
||||
while True:
|
||||
if self.buf is None:
|
||||
if self.messages.qsize() == 0:
|
||||
# nothing more to write
|
||||
self.loop.remove_writer(self.fd)
|
||||
return
|
||||
buf, unix_fds, fut = self.messages.get_nowait()
|
||||
self.unix_fds = unix_fds
|
||||
self.buf = memoryview(buf)
|
||||
self.offset = 0
|
||||
self.fut = fut
|
||||
|
||||
if self.unix_fds and self.negotiate_unix_fd:
|
||||
ancdata = [
|
||||
(
|
||||
socket.SOL_SOCKET,
|
||||
socket.SCM_RIGHTS,
|
||||
array.array("i", self.unix_fds),
|
||||
)
|
||||
]
|
||||
self.offset += self.sock.sendmsg([self.buf[self.offset :]], ancdata)
|
||||
self.unix_fds = None
|
||||
else:
|
||||
self.offset += self.sock.send(self.buf[self.offset :])
|
||||
|
||||
if self.offset >= len(self.buf):
|
||||
# finished writing
|
||||
self.buf = None
|
||||
_future_set_result(self.fut, None)
|
||||
else:
|
||||
# wait for writable
|
||||
return
|
||||
except Exception as e:
|
||||
_future_set_exception(self.fut, e)
|
||||
self.bus._finalize(e)
|
||||
|
||||
def buffer_message(self, msg: Message, future=None):
|
||||
self.messages.put_nowait(
|
||||
(
|
||||
msg._marshall(negotiate_unix_fd=self.negotiate_unix_fd),
|
||||
copy(msg.unix_fds),
|
||||
future,
|
||||
)
|
||||
)
|
||||
|
||||
def schedule_write(self, msg: Message = None, future=None):
|
||||
if msg is not None:
|
||||
self.buffer_message(msg, future)
|
||||
if self.bus.unique_name:
|
||||
# don't run the writer until the bus is ready to send messages
|
||||
self.loop.add_writer(self.fd, self.write_callback)
|
||||
|
||||
|
||||
class MessageBus(BaseMessageBus):
|
||||
"""The message bus implementation for use with asyncio.
|
||||
|
||||
The message bus class is the entry point into all the features of the
|
||||
library. It sets up a connection to the DBus daemon and exposes an
|
||||
interface to send and receive messages and expose services.
|
||||
|
||||
You must call :func:`connect() <dbus_next.aio.MessageBus.connect>` before
|
||||
using this message bus.
|
||||
|
||||
:param bus_type: The type of bus to connect to. Affects the search path for
|
||||
the bus address.
|
||||
:type bus_type: :class:`BusType <dbus_next.BusType>`
|
||||
:param bus_address: A specific bus address to connect to. Should not be
|
||||
used under normal circumstances.
|
||||
:param auth: The authenticator to use, defaults to an instance of
|
||||
:class:`AuthExternal <dbus_next.auth.AuthExternal>`.
|
||||
:type auth: :class:`Authenticator <dbus_next.auth.Authenticator>`
|
||||
:param negotiate_unix_fd: Allow the bus to send and receive Unix file
|
||||
descriptors (DBus type 'h'). This must be supported by the transport.
|
||||
:type negotiate_unix_fd: bool
|
||||
|
||||
:ivar unique_name: The unique name of the message bus connection. It will
|
||||
be :class:`None` until the message bus connects.
|
||||
:vartype unique_name: str
|
||||
:ivar connected: True if this message bus is expected to be able to send
|
||||
and receive messages.
|
||||
:vartype connected: bool
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bus_address: str = None,
|
||||
bus_type: BusType = BusType.SESSION,
|
||||
auth: Authenticator = None,
|
||||
negotiate_unix_fd=False,
|
||||
):
|
||||
super().__init__(bus_address, bus_type, ProxyObject)
|
||||
self._negotiate_unix_fd = negotiate_unix_fd
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self._unmarshaller = self._create_unmarshaller()
|
||||
|
||||
self._writer = _MessageWriter(self)
|
||||
|
||||
if auth is None:
|
||||
self._auth = AuthExternal()
|
||||
else:
|
||||
self._auth = auth
|
||||
|
||||
self._disconnect_future = self._loop.create_future()
|
||||
|
||||
async def connect(self) -> "MessageBus":
|
||||
"""Connect this message bus to the DBus daemon.
|
||||
|
||||
This method must be called before the message bus can be used.
|
||||
|
||||
:returns: This message bus for convenience.
|
||||
:rtype: :class:`MessageBus <dbus_next.aio.MessageBus>`
|
||||
|
||||
:raises:
|
||||
- :class:`AuthError <dbus_next.AuthError>` - If authorization to \
|
||||
the DBus daemon failed.
|
||||
- :class:`Exception` - If there was a connection error.
|
||||
"""
|
||||
await self._authenticate()
|
||||
|
||||
future = self._loop.create_future()
|
||||
|
||||
self._loop.add_reader(self._fd, self._message_reader)
|
||||
|
||||
def on_hello(reply, err):
|
||||
try:
|
||||
if err:
|
||||
raise err
|
||||
self.unique_name = reply.body[0]
|
||||
self._writer.schedule_write()
|
||||
_future_set_result(future, self)
|
||||
except Exception as e:
|
||||
_future_set_exception(future, e)
|
||||
self.disconnect()
|
||||
self._finalize(err)
|
||||
|
||||
hello_msg = Message(
|
||||
destination="org.freedesktop.DBus",
|
||||
path="/org/freedesktop/DBus",
|
||||
interface="org.freedesktop.DBus",
|
||||
member="Hello",
|
||||
serial=self.next_serial(),
|
||||
)
|
||||
|
||||
self._method_return_handlers[hello_msg.serial] = on_hello
|
||||
self._stream.write(hello_msg._marshall())
|
||||
self._stream.flush()
|
||||
|
||||
return await future
|
||||
|
||||
async def introspect(
|
||||
self, bus_name: str, path: str, timeout: float = 30.0
|
||||
) -> intr.Node:
|
||||
"""Get introspection data for the node at the given path from the given
|
||||
bus name.
|
||||
|
||||
Calls the standard ``org.freedesktop.DBus.Introspectable.Introspect``
|
||||
on the bus for the path.
|
||||
|
||||
:param bus_name: The name to introspect.
|
||||
:type bus_name: str
|
||||
:param path: The path to introspect.
|
||||
:type path: str
|
||||
:param timeout: The timeout to introspect.
|
||||
:type timeout: float
|
||||
|
||||
:returns: The introspection data for the name at the path.
|
||||
:rtype: :class:`Node <dbus_next.introspection.Node>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidObjectPathError <dbus_next.InvalidObjectPathError>` \
|
||||
- If the given object path is not valid.
|
||||
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
|
||||
the given bus name is not valid.
|
||||
- :class:`DBusError <dbus_next.DBusError>` - If the service threw \
|
||||
an error for the method call or returned an invalid result.
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
- :class:`asyncio.TimeoutError` - Waited for future but time run out.
|
||||
"""
|
||||
future = self._loop.create_future()
|
||||
|
||||
def reply_handler(reply, err):
|
||||
if err:
|
||||
_future_set_exception(future, err)
|
||||
else:
|
||||
_future_set_result(future, reply)
|
||||
|
||||
super().introspect(bus_name, path, reply_handler)
|
||||
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
|
||||
async def request_name(
|
||||
self, name: str, flags: NameFlag = NameFlag.NONE
|
||||
) -> RequestNameReply:
|
||||
"""Request that this message bus owns the given name.
|
||||
|
||||
:param name: The name to request.
|
||||
:type name: str
|
||||
:param flags: Name flags that affect the behavior of the name request.
|
||||
:type flags: :class:`NameFlag <dbus_next.NameFlag>`
|
||||
|
||||
:returns: The reply to the name request.
|
||||
:rtype: :class:`RequestNameReply <dbus_next.RequestNameReply>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
|
||||
the given bus name is not valid.
|
||||
- :class:`DBusError <dbus_next.DBusError>` - If the service threw \
|
||||
an error for the method call or returned an invalid result.
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
"""
|
||||
future = self._loop.create_future()
|
||||
|
||||
def reply_handler(reply, err):
|
||||
if err:
|
||||
_future_set_exception(future, err)
|
||||
else:
|
||||
_future_set_result(future, reply)
|
||||
|
||||
super().request_name(name, flags, reply_handler)
|
||||
|
||||
return await future
|
||||
|
||||
async def release_name(self, name: str) -> ReleaseNameReply:
|
||||
"""Request that this message bus release the given name.
|
||||
|
||||
:param name: The name to release.
|
||||
:type name: str
|
||||
|
||||
:returns: The reply to the release request.
|
||||
:rtype: :class:`ReleaseNameReply <dbus_next.ReleaseNameReply>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
|
||||
the given bus name is not valid.
|
||||
- :class:`DBusError <dbus_next.DBusError>` - If the service threw \
|
||||
an error for the method call or returned an invalid result.
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
"""
|
||||
future = self._loop.create_future()
|
||||
|
||||
def reply_handler(reply, err):
|
||||
if err:
|
||||
_future_set_exception(future, err)
|
||||
else:
|
||||
_future_set_result(future, reply)
|
||||
|
||||
super().release_name(name, reply_handler)
|
||||
|
||||
return await future
|
||||
|
||||
async def call(self, msg: Message) -> Optional[Message]:
|
||||
"""Send a method call and wait for a reply from the DBus daemon.
|
||||
|
||||
:param msg: The method call message to send.
|
||||
:type msg: :class:`Message <dbus_next.Message>`
|
||||
|
||||
:returns: A message in reply to the message sent. If the message does
|
||||
not expect a reply based on the message flags or type, returns
|
||||
``None`` after the message is sent.
|
||||
:rtype: :class:`Message <dbus_next.Message>` or :class:`None` if no reply is expected.
|
||||
|
||||
:raises:
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
"""
|
||||
if (
|
||||
msg.flags & MessageFlag.NO_REPLY_EXPECTED
|
||||
or msg.message_type is not MessageType.METHOD_CALL
|
||||
):
|
||||
await self.send(msg)
|
||||
return None
|
||||
|
||||
future = self._loop.create_future()
|
||||
|
||||
def reply_handler(reply, err):
|
||||
if not future.done():
|
||||
if err:
|
||||
_future_set_exception(future, err)
|
||||
else:
|
||||
_future_set_result(future, reply)
|
||||
|
||||
self._call(msg, reply_handler)
|
||||
|
||||
await future
|
||||
|
||||
return future.result()
|
||||
|
||||
def send(self, msg: Message):
|
||||
"""Asynchronously send a message on the message bus.
|
||||
|
||||
.. note:: This method may change to a couroutine function in the 1.0
|
||||
release of the library.
|
||||
|
||||
:param msg: The message to send.
|
||||
:type msg: :class:`Message <dbus_next.Message>`
|
||||
|
||||
:returns: A future that resolves when the message is sent or a
|
||||
connection error occurs.
|
||||
:rtype: :class:`Future <asyncio.Future>`
|
||||
"""
|
||||
if not msg.serial:
|
||||
msg.serial = self.next_serial()
|
||||
|
||||
future = self._loop.create_future()
|
||||
self._writer.schedule_write(msg, future)
|
||||
return future
|
||||
|
||||
def get_proxy_object(
|
||||
self, bus_name: str, path: str, introspection: intr.Node
|
||||
) -> ProxyObject:
|
||||
return super().get_proxy_object(bus_name, path, introspection)
|
||||
|
||||
async def wait_for_disconnect(self):
|
||||
"""Wait for the message bus to disconnect.
|
||||
|
||||
:returns: :class:`None` when the message bus has disconnected.
|
||||
:rtype: :class:`None`
|
||||
|
||||
:raises:
|
||||
- :class:`Exception` - If connection was terminated unexpectedly or \
|
||||
an internal error occurred in the library.
|
||||
"""
|
||||
return await self._disconnect_future
|
||||
|
||||
def _make_method_handler(self, interface, method):
|
||||
if not asyncio.iscoroutinefunction(method.fn):
|
||||
return super()._make_method_handler(interface, method)
|
||||
|
||||
def handler(msg, send_reply):
|
||||
def done(fut):
|
||||
with send_reply:
|
||||
result = fut.result()
|
||||
body, unix_fds = ServiceInterface._fn_result_to_body(
|
||||
result, method.out_signature_tree
|
||||
)
|
||||
send_reply(
|
||||
Message.new_method_return(
|
||||
msg, method.out_signature, body, unix_fds
|
||||
)
|
||||
)
|
||||
|
||||
args = ServiceInterface._msg_body_to_args(msg)
|
||||
fut = asyncio.ensure_future(method.fn(interface, *args))
|
||||
fut.add_done_callback(done)
|
||||
|
||||
return handler
|
||||
|
||||
def _message_reader(self):
|
||||
try:
|
||||
while True:
|
||||
if self._unmarshaller.unmarshall():
|
||||
self._on_message(self._unmarshaller.message)
|
||||
self._unmarshaller = self._create_unmarshaller()
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
self._finalize(e)
|
||||
|
||||
async def _auth_readline(self):
|
||||
buf = b""
|
||||
while buf[-2:] != b"\r\n":
|
||||
buf += await self._loop.sock_recv(self._sock, 2)
|
||||
return buf[:-2].decode()
|
||||
|
||||
async def _authenticate(self):
|
||||
await self._loop.sock_sendall(self._sock, b"\0")
|
||||
|
||||
first_line = self._auth._authentication_start(
|
||||
negotiate_unix_fd=self._negotiate_unix_fd
|
||||
)
|
||||
|
||||
if first_line is not None:
|
||||
if type(first_line) is not str:
|
||||
raise AuthError("authenticator gave response not type str")
|
||||
await self._loop.sock_sendall(
|
||||
self._sock, Authenticator._format_line(first_line)
|
||||
)
|
||||
|
||||
while True:
|
||||
response = self._auth._receive_line(await self._auth_readline())
|
||||
if response is not None:
|
||||
await self._loop.sock_sendall(
|
||||
self._sock, Authenticator._format_line(response)
|
||||
)
|
||||
self._stream.flush()
|
||||
if response == "BEGIN":
|
||||
break
|
||||
|
||||
def _create_unmarshaller(self):
|
||||
sock = None
|
||||
if self._negotiate_unix_fd:
|
||||
sock = self._sock
|
||||
return Unmarshaller(self._stream, sock)
|
||||
|
||||
def _finalize(self, err=None):
|
||||
try:
|
||||
self._loop.remove_reader(self._fd)
|
||||
except Exception:
|
||||
logging.warning("could not remove message reader", exc_info=True)
|
||||
try:
|
||||
self._loop.remove_writer(self._fd)
|
||||
except Exception:
|
||||
logging.warning("could not remove message writer", exc_info=True)
|
||||
|
||||
super()._finalize(err)
|
||||
|
||||
if self._disconnect_future.done():
|
||||
return
|
||||
|
||||
if err and not self._user_disconnect:
|
||||
_future_set_exception(self._disconnect_future, err)
|
||||
else:
|
||||
_future_set_result(self._disconnect_future, None)
|
||||
183
src/dbus_fast/aio/proxy_object.py
Normal file
183
src/dbus_fast/aio/proxy_object.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List, Union
|
||||
|
||||
from .. import introspection as intr
|
||||
from .._private.util import replace_fds_with_idx, replace_idx_with_fds
|
||||
from ..constants import ErrorType
|
||||
from ..errors import DBusError
|
||||
from ..message import Message, MessageFlag
|
||||
from ..message_bus import BaseMessageBus
|
||||
from ..proxy_object import BaseProxyInterface, BaseProxyObject
|
||||
from ..signature import Variant
|
||||
|
||||
|
||||
class ProxyInterface(BaseProxyInterface):
|
||||
"""A class representing a proxy to an interface exported on the bus by
|
||||
another client for the asyncio :class:`MessageBus
|
||||
<dbus_next.aio.MessageBus>` implementation.
|
||||
|
||||
This class is not meant to be constructed directly by the user. Use
|
||||
:func:`ProxyObject.get_interface()
|
||||
<dbus_next.aio.ProxyObject.get_interface>` on a asyncio proxy object to get
|
||||
a proxy interface.
|
||||
|
||||
This class exposes methods to call DBus methods, listen to signals, and get
|
||||
and set properties on the interface that are created dynamically based on
|
||||
the introspection data passed to the proxy object that made this proxy
|
||||
interface.
|
||||
|
||||
A *method call* takes this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
result = await interface.call_[METHOD](*args)
|
||||
|
||||
Where ``METHOD`` is the name of the method converted to snake case.
|
||||
|
||||
DBus methods are exposed as coroutines that take arguments that correpond
|
||||
to the *in args* of the interface method definition and return a ``result``
|
||||
that corresponds to the *out arg*. If the method has more than one out arg,
|
||||
they are returned within a :class:`list`.
|
||||
|
||||
To *listen to a signal* use this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
interface.on_[SIGNAL](callback)
|
||||
|
||||
To *stop listening to a signal* use this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
interface.off_[SIGNAL](callback)
|
||||
|
||||
Where ``SIGNAL`` is the name of the signal converted to snake case.
|
||||
|
||||
DBus signals are exposed with an event-callback interface. The provided
|
||||
``callback`` will be called when the signal is emitted with arguments that
|
||||
correspond to the *out args* of the interface signal definition.
|
||||
|
||||
To *get or set a property* use this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
value = await interface.get_[PROPERTY]()
|
||||
await interface.set_[PROPERTY](value)
|
||||
|
||||
Where ``PROPERTY`` is the name of the property converted to snake case.
|
||||
|
||||
DBus property getters and setters are exposed as coroutines. The ``value``
|
||||
must correspond to the type of the property in the interface definition.
|
||||
|
||||
If the service returns an error for a DBus call, a :class:`DBusError
|
||||
<dbus_next.DBusError>` will be raised with information about the error.
|
||||
"""
|
||||
|
||||
def _add_method(self, intr_method):
|
||||
async def method_fn(*args, flags=MessageFlag.NONE):
|
||||
input_body, unix_fds = replace_fds_with_idx(
|
||||
intr_method.in_signature, list(args)
|
||||
)
|
||||
|
||||
msg = await self.bus.call(
|
||||
Message(
|
||||
destination=self.bus_name,
|
||||
path=self.path,
|
||||
interface=self.introspection.name,
|
||||
member=intr_method.name,
|
||||
signature=intr_method.in_signature,
|
||||
body=input_body,
|
||||
flags=flags,
|
||||
unix_fds=unix_fds,
|
||||
)
|
||||
)
|
||||
|
||||
if flags & MessageFlag.NO_REPLY_EXPECTED:
|
||||
return None
|
||||
|
||||
BaseProxyInterface._check_method_return(msg, intr_method.out_signature)
|
||||
|
||||
out_len = len(intr_method.out_args)
|
||||
|
||||
body = replace_idx_with_fds(msg.signature_tree, msg.body, msg.unix_fds)
|
||||
|
||||
if not out_len:
|
||||
return None
|
||||
elif out_len == 1:
|
||||
return body[0]
|
||||
else:
|
||||
return body
|
||||
|
||||
method_name = f"call_{BaseProxyInterface._to_snake_case(intr_method.name)}"
|
||||
setattr(self, method_name, method_fn)
|
||||
|
||||
def _add_property(self, intr_property):
|
||||
async def property_getter():
|
||||
msg = await self.bus.call(
|
||||
Message(
|
||||
destination=self.bus_name,
|
||||
path=self.path,
|
||||
interface="org.freedesktop.DBus.Properties",
|
||||
member="Get",
|
||||
signature="ss",
|
||||
body=[self.introspection.name, intr_property.name],
|
||||
)
|
||||
)
|
||||
|
||||
BaseProxyInterface._check_method_return(msg, "v")
|
||||
variant = msg.body[0]
|
||||
if variant.signature != intr_property.signature:
|
||||
raise DBusError(
|
||||
ErrorType.CLIENT_ERROR,
|
||||
f'property returned unexpected signature "{variant.signature}"',
|
||||
msg,
|
||||
)
|
||||
|
||||
return replace_idx_with_fds("v", msg.body, msg.unix_fds)[0].value
|
||||
|
||||
async def property_setter(val):
|
||||
variant = Variant(intr_property.signature, val)
|
||||
|
||||
body, unix_fds = replace_fds_with_idx(
|
||||
"ssv", [self.introspection.name, intr_property.name, variant]
|
||||
)
|
||||
|
||||
msg = await self.bus.call(
|
||||
Message(
|
||||
destination=self.bus_name,
|
||||
path=self.path,
|
||||
interface="org.freedesktop.DBus.Properties",
|
||||
member="Set",
|
||||
signature="ssv",
|
||||
body=body,
|
||||
unix_fds=unix_fds,
|
||||
)
|
||||
)
|
||||
|
||||
BaseProxyInterface._check_method_return(msg)
|
||||
|
||||
snake_case = BaseProxyInterface._to_snake_case(intr_property.name)
|
||||
setattr(self, f"get_{snake_case}", property_getter)
|
||||
setattr(self, f"set_{snake_case}", property_setter)
|
||||
|
||||
|
||||
class ProxyObject(BaseProxyObject):
|
||||
"""The proxy object implementation for the GLib :class:`MessageBus <dbus_next.glib.MessageBus>`.
|
||||
|
||||
For more information, see the :class:`BaseProxyObject <dbus_next.proxy_object.BaseProxyObject>`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bus_name: str,
|
||||
path: str,
|
||||
introspection: Union[intr.Node, str, ET.Element],
|
||||
bus: BaseMessageBus,
|
||||
):
|
||||
super().__init__(bus_name, path, introspection, bus, ProxyInterface)
|
||||
|
||||
def get_interface(self, name: str) -> ProxyInterface:
|
||||
return super().get_interface(name)
|
||||
|
||||
def get_children(self) -> List["ProxyObject"]:
|
||||
return super().get_children()
|
||||
108
src/dbus_fast/auth.py
Normal file
108
src/dbus_fast/auth.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import enum
|
||||
import os
|
||||
|
||||
from .errors import AuthError
|
||||
|
||||
# The auth interface here is unstable. I would like to eventually open this up
|
||||
# for people to define their own custom authentication protocols, but I'm not
|
||||
# familiar with what's needed for that exactly. To work with any message bus
|
||||
# implementation would require abstracting out all the IO. Async operations
|
||||
# might be challenging because different IO backends have different ways of
|
||||
# doing that. I might just end up giving the raw socket and leaving it all up
|
||||
# to the user, but it would be nice to have a little guidance in the interface
|
||||
# since a lot of it is strongly specified. If you have a need for this, contact
|
||||
# the project maintainer to help stabalize this interface.
|
||||
|
||||
|
||||
class _AuthResponse(enum.Enum):
|
||||
OK = "OK"
|
||||
REJECTED = "REJECTED"
|
||||
DATA = "DATA"
|
||||
ERROR = "ERROR"
|
||||
AGREE_UNIX_FD = "AGREE_UNIX_FD"
|
||||
|
||||
@classmethod
|
||||
def parse(klass, line):
|
||||
args = line.split(" ")
|
||||
response = klass(args[0])
|
||||
return response, args[1:]
|
||||
|
||||
|
||||
# UNSTABLE
|
||||
class Authenticator:
|
||||
"""The base class for authenticators for :class:`MessageBus <dbus_next.message_bus.BaseMessageBus>` authentication.
|
||||
|
||||
In the future, the library may allow extending this class for custom authentication protocols.
|
||||
|
||||
:seealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol
|
||||
"""
|
||||
|
||||
def _authentication_start(self, negotiate_unix_fd=False):
|
||||
raise NotImplementedError(
|
||||
"authentication_start() must be implemented in the inheriting class"
|
||||
)
|
||||
|
||||
def _receive_line(self, line):
|
||||
raise NotImplementedError(
|
||||
"receive_line() must be implemented in the inheriting class"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_line(line):
|
||||
return f"{line}\r\n".encode()
|
||||
|
||||
|
||||
class AuthExternal(Authenticator):
|
||||
"""An authenticator class for the external auth protocol for use with the
|
||||
:class:`MessageBus <dbus_next.message_bus.BaseMessageBus>`.
|
||||
|
||||
:sealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.negotiate_unix_fd = False
|
||||
self.negotiating_fds = False
|
||||
|
||||
def _authentication_start(self, negotiate_unix_fd=False) -> str:
|
||||
self.negotiate_unix_fd = negotiate_unix_fd
|
||||
hex_uid = str(os.getuid()).encode().hex()
|
||||
return f"AUTH EXTERNAL {hex_uid}"
|
||||
|
||||
def _receive_line(self, line: str):
|
||||
response, args = _AuthResponse.parse(line)
|
||||
|
||||
if response is _AuthResponse.OK:
|
||||
if self.negotiate_unix_fd:
|
||||
self.negotiating_fds = True
|
||||
return "NEGOTIATE_UNIX_FD"
|
||||
else:
|
||||
return "BEGIN"
|
||||
|
||||
if response is _AuthResponse.AGREE_UNIX_FD:
|
||||
return "BEGIN"
|
||||
|
||||
raise AuthError(f"authentication failed: {response.value}: {args}")
|
||||
|
||||
|
||||
class AuthAnnonymous(Authenticator):
|
||||
"""An authenticator class for the annonymous auth protocol for use with the
|
||||
:class:`MessageBus <dbus_next.message_bus.BaseMessageBus>`.
|
||||
|
||||
:sealso: https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol
|
||||
"""
|
||||
|
||||
def _authentication_start(self, negotiate_unix_fd=False) -> str:
|
||||
if negotiate_unix_fd:
|
||||
raise AuthError(
|
||||
"annonymous authentication does not support negotiating unix fds right now"
|
||||
)
|
||||
|
||||
return "AUTH ANONYMOUS"
|
||||
|
||||
def _receive_line(self, line: str) -> str:
|
||||
response, args = _AuthResponse.parse(line)
|
||||
|
||||
if response != _AuthResponse.OK:
|
||||
raise AuthError(f"authentication failed: {response.value}: {args}")
|
||||
|
||||
return "BEGIN"
|
||||
123
src/dbus_fast/constants.py
Normal file
123
src/dbus_fast/constants.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from enum import Enum, IntFlag
|
||||
|
||||
|
||||
class BusType(Enum):
|
||||
"""An enum that indicates a type of bus. On most systems, there are
|
||||
normally two different kinds of buses running.
|
||||
"""
|
||||
|
||||
SESSION = 1 #: A bus for the current graphical user session.
|
||||
SYSTEM = 2 #: A persistent bus for the whole machine.
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
"""An enum that indicates a type of message."""
|
||||
|
||||
METHOD_CALL = 1 #: An outgoing method call.
|
||||
METHOD_RETURN = 2 #: A return to a previously sent method call
|
||||
ERROR = 3 #: A return to a method call that has failed
|
||||
SIGNAL = 4 #: A broadcast signal to subscribed connections
|
||||
|
||||
|
||||
class MessageFlag(IntFlag):
|
||||
"""Flags that affect the behavior of sent and received messages"""
|
||||
|
||||
NONE = 0
|
||||
NO_REPLY_EXPECTED = 1 #: The method call does not expect a method return.
|
||||
NO_AUTOSTART = 2
|
||||
ALLOW_INTERACTIVE_AUTHORIZATION = 4
|
||||
|
||||
|
||||
class NameFlag(IntFlag):
|
||||
"""A flag that affects the behavior of a name request."""
|
||||
|
||||
NONE = 0
|
||||
ALLOW_REPLACEMENT = 1 #: If another client requests this name, let them have it.
|
||||
REPLACE_EXISTING = 2 #: If another client owns this name, try to take it.
|
||||
DO_NOT_QUEUE = 4 #: Name requests normally queue and wait for the owner to release the name. Do not enter this queue.
|
||||
|
||||
|
||||
class RequestNameReply(Enum):
|
||||
"""An enum that describes the result of a name request."""
|
||||
|
||||
PRIMARY_OWNER = 1 #: The bus owns the name.
|
||||
IN_QUEUE = 2 #: The bus is in a queue and may receive the name after it is relased by the primary owner.
|
||||
EXISTS = 3 #: The name has an owner and NameFlag.DO_NOT_QUEUE was given.
|
||||
ALREADY_OWNER = 4 #: The bus already owns the name.
|
||||
|
||||
|
||||
class ReleaseNameReply(Enum):
|
||||
"""An enum that describes the result of a name release request"""
|
||||
|
||||
RELEASED = 1
|
||||
NON_EXISTENT = 2
|
||||
NOT_OWNER = 3
|
||||
|
||||
|
||||
class PropertyAccess(Enum):
|
||||
"""An enum that describes whether a DBus property can be gotten or set with
|
||||
the ``org.freedesktop.DBus.Properties`` interface.
|
||||
"""
|
||||
|
||||
READ = "read" #: The property is readonly.
|
||||
WRITE = "write" #: The property is writeonly.
|
||||
READWRITE = "readwrite" #: The property can be read or written to.
|
||||
|
||||
def readable(self):
|
||||
"""Get whether the property can be read."""
|
||||
return self == PropertyAccess.READ or self == PropertyAccess.READWRITE
|
||||
|
||||
def writable(self):
|
||||
"""Get whether the property can be written to."""
|
||||
return self == PropertyAccess.WRITE or self == PropertyAccess.READWRITE
|
||||
|
||||
|
||||
class ArgDirection(Enum):
|
||||
"""For an introspected argument, indicates whether it is an input parameter or a return value."""
|
||||
|
||||
IN = "in"
|
||||
OUT = "out"
|
||||
|
||||
|
||||
class ErrorType(Enum):
|
||||
"""An enum for the type of an error for a message reply.
|
||||
|
||||
:seealso: http://man7.org/linux/man-pages/man3/sd-bus-errors.3.html
|
||||
"""
|
||||
|
||||
SERVICE_ERROR = "com.dubstepdish.dbus.next.ServiceError" #: A custom error to indicate an exported service threw an exception.
|
||||
INTERNAL_ERROR = "com.dubstepdish.dbus.next.InternalError" #: A custom error to indicate something went wrong with the library.
|
||||
CLIENT_ERROR = "com.dubstepdish.dbus.next.ClientError" #: A custom error to indicate something went wrong with the client.
|
||||
|
||||
FAILED = "org.freedesktop.DBus.Error.Failed"
|
||||
NO_MEMORY = "org.freedesktop.DBus.Error.NoMemory"
|
||||
SERVICE_UNKNOWN = "org.freedesktop.DBus.Error.ServiceUnknown"
|
||||
NAME_HAS_NO_OWNER = "org.freedesktop.DBus.Error.NameHasNoOwner"
|
||||
NO_REPLY = "org.freedesktop.DBus.Error.NoReply"
|
||||
IO_ERROR = "org.freedesktop.DBus.Error.IOError"
|
||||
BAD_ADDRESS = "org.freedesktop.DBus.Error.BadAddress"
|
||||
NOT_SUPPORTED = "org.freedesktop.DBus.Error.NotSupported"
|
||||
LIMITS_EXCEEDED = "org.freedesktop.DBus.Error.LimitsExceeded"
|
||||
ACCESS_DENIED = "org.freedesktop.DBus.Error.AccessDenied"
|
||||
AUTH_FAILED = "org.freedesktop.DBus.Error.AuthFailed"
|
||||
NO_SERVER = "org.freedesktop.DBus.Error.NoServer"
|
||||
TIMEOUT = "org.freedesktop.DBus.Error.Timeout"
|
||||
NO_NETWORK = "org.freedesktop.DBus.Error.NoNetwork"
|
||||
ADDRESS_IN_USE = "org.freedesktop.DBus.Error.AddressInUse"
|
||||
DISCONNECTED = "org.freedesktop.DBus.Error.Disconnected"
|
||||
INVALID_ARGS = "org.freedesktop.DBus.Error.InvalidArgs"
|
||||
FILE_NOT_FOUND = "org.freedesktop.DBus.Error.FileNotFound"
|
||||
FILE_EXISTS = "org.freedesktop.DBus.Error.FileExists"
|
||||
UNKNOWN_METHOD = "org.freedesktop.DBus.Error.UnknownMethod"
|
||||
UNKNOWN_OBJECT = "org.freedesktop.DBus.Error.UnknownObject"
|
||||
UNKNOWN_INTERFACE = "org.freedesktop.DBus.Error.UnknownInterface"
|
||||
UNKNOWN_PROPERTY = "org.freedesktop.DBus.Error.UnknownProperty"
|
||||
PROPERTY_READ_ONLY = "org.freedesktop.DBus.Error.PropertyReadOnly"
|
||||
UNIX_PROCESS_ID_UNKNOWN = "org.freedesktop.DBus.Error.UnixProcessIdUnknown"
|
||||
INVALID_SIGNATURE = "org.freedesktop.DBus.Error.InvalidSignature"
|
||||
INCONSISTENT_MESSAGE = "org.freedesktop.DBus.Error.InconsistentMessage"
|
||||
MATCH_RULE_NOT_FOUND = "org.freedesktop.DBus.Error.MatchRuleNotFound"
|
||||
MATCH_RULE_INVALID = "org.freedesktop.DBus.Error.MatchRuleInvalid"
|
||||
INTERACTIVE_AUTHORIZATION_REQUIRED = (
|
||||
"org.freedesktop.DBus.Error.InteractiveAuthorizationRequired"
|
||||
)
|
||||
79
src/dbus_fast/errors.py
Normal file
79
src/dbus_fast/errors.py
Normal file
@@ -0,0 +1,79 @@
|
||||
class SignatureBodyMismatchError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSignatureError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAddressError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidMessageError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidIntrospectionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SignalDisabledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidBusNameError(TypeError):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"invalid bus name: {name}")
|
||||
|
||||
|
||||
class InvalidObjectPathError(TypeError):
|
||||
def __init__(self, path):
|
||||
super().__init__(f"invalid object path: {path}")
|
||||
|
||||
|
||||
class InvalidInterfaceNameError(TypeError):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"invalid interface name: {name}")
|
||||
|
||||
|
||||
class InvalidMemberNameError(TypeError):
|
||||
def __init__(self, member):
|
||||
super().__init__(f"invalid member name: {member}")
|
||||
|
||||
|
||||
from .constants import ErrorType, MessageType
|
||||
from .message import Message
|
||||
from .validators import assert_interface_name_valid
|
||||
|
||||
|
||||
class DBusError(Exception):
|
||||
def __init__(self, type_, text, reply=None):
|
||||
super().__init__(text)
|
||||
|
||||
if type(type_) is ErrorType:
|
||||
type_ = type_.value
|
||||
|
||||
assert_interface_name_valid(type_)
|
||||
if reply is not None and type(reply) is not Message:
|
||||
raise TypeError("reply must be of type Message")
|
||||
|
||||
self.type = type_
|
||||
self.text = text
|
||||
self.reply = reply
|
||||
|
||||
@staticmethod
|
||||
def _from_message(msg):
|
||||
assert msg.message_type == MessageType.ERROR
|
||||
return DBusError(msg.error_name, msg.body[0], reply=msg)
|
||||
|
||||
def _as_message(self, msg):
|
||||
return Message.new_error(msg, self.type, self.text)
|
||||
2
src/dbus_fast/glib/__init__.py
Normal file
2
src/dbus_fast/glib/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .message_bus import MessageBus
|
||||
from .proxy_object import ProxyInterface, ProxyObject
|
||||
496
src/dbus_fast/glib/message_bus.py
Normal file
496
src/dbus_fast/glib/message_bus.py
Normal file
@@ -0,0 +1,496 @@
|
||||
import io
|
||||
from typing import Callable, Optional
|
||||
|
||||
from .. import introspection as intr
|
||||
from .._private.unmarshaller import Unmarshaller
|
||||
from ..auth import Authenticator, AuthExternal
|
||||
from ..constants import (
|
||||
BusType,
|
||||
MessageFlag,
|
||||
MessageType,
|
||||
NameFlag,
|
||||
ReleaseNameReply,
|
||||
RequestNameReply,
|
||||
)
|
||||
from ..errors import AuthError
|
||||
from ..message import Message
|
||||
from ..message_bus import BaseMessageBus
|
||||
from .proxy_object import ProxyObject
|
||||
|
||||
# glib is optional
|
||||
_import_error = None
|
||||
try:
|
||||
from gi.repository import GLib
|
||||
|
||||
_GLibSource = GLib.Source
|
||||
except ImportError as e:
|
||||
_import_error = e
|
||||
|
||||
class _GLibSource:
|
||||
pass
|
||||
|
||||
|
||||
class _MessageSource(_GLibSource):
|
||||
def __init__(self, bus):
|
||||
self.unmarshaller = None
|
||||
self.bus = bus
|
||||
|
||||
def prepare(self):
|
||||
return (False, -1)
|
||||
|
||||
def check(self):
|
||||
return False
|
||||
|
||||
def dispatch(self, callback, user_data):
|
||||
try:
|
||||
while self.bus._stream.readable():
|
||||
if not self.unmarshaller:
|
||||
self.unmarshaller = Unmarshaller(self.bus._stream)
|
||||
|
||||
if self.unmarshaller.unmarshall():
|
||||
callback(self.unmarshaller.message)
|
||||
self.unmarshaller = None
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
self.bus.disconnect()
|
||||
self.bus._finalize(e)
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
|
||||
class _MessageWritableSource(_GLibSource):
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
self.buf = b""
|
||||
self.message_stream = None
|
||||
self.chunk_size = 128
|
||||
|
||||
def prepare(self):
|
||||
return (False, -1)
|
||||
|
||||
def check(self):
|
||||
return False
|
||||
|
||||
def dispatch(self, callback, user_data):
|
||||
try:
|
||||
if self.buf:
|
||||
self.bus._stream.write(self.buf)
|
||||
self.buf = b""
|
||||
|
||||
if self.message_stream:
|
||||
while True:
|
||||
self.buf = self.message_stream.read(self.chunk_size)
|
||||
if self.buf == b"":
|
||||
break
|
||||
self.bus._stream.write(self.buf)
|
||||
if len(self.buf) < self.chunk_size:
|
||||
self.buf = b""
|
||||
break
|
||||
self.buf = b""
|
||||
|
||||
self.bus._stream.flush()
|
||||
|
||||
if not self.bus._buffered_messages:
|
||||
return GLib.SOURCE_REMOVE
|
||||
else:
|
||||
message = self.bus._buffered_messages.pop(0)
|
||||
self.message_stream = io.BytesIO(message._marshall())
|
||||
return GLib.SOURCE_CONTINUE
|
||||
except BlockingIOError:
|
||||
return GLib.SOURCE_CONTINUE
|
||||
except Exception as e:
|
||||
self.bus._finalize(e)
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
|
||||
class _AuthLineSource(_GLibSource):
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.buf = b""
|
||||
|
||||
def prepare(self):
|
||||
return (False, -1)
|
||||
|
||||
def check(self):
|
||||
return False
|
||||
|
||||
def dispatch(self, callback, user_data):
|
||||
self.buf += self.stream.read()
|
||||
if self.buf[-2:] == b"\r\n":
|
||||
resp = callback(self.buf.decode()[:-2])
|
||||
if resp:
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
|
||||
class MessageBus(BaseMessageBus):
|
||||
"""The message bus implementation for use with the GLib main loop.
|
||||
|
||||
The message bus class is the entry point into all the features of the
|
||||
library. It sets up a connection to the DBus daemon and exposes an
|
||||
interface to send and receive messages and expose services.
|
||||
|
||||
You must call :func:`connect() <dbus_next.glib.MessageBus.connect>` or
|
||||
:func:`connect_sync() <dbus_next.glib.MessageBus.connect_sync>` before
|
||||
using this message bus.
|
||||
|
||||
:param bus_type: The type of bus to connect to. Affects the search path for
|
||||
the bus address.
|
||||
:type bus_type: :class:`BusType <dbus_next.BusType>`
|
||||
:param bus_address: A specific bus address to connect to. Should not be
|
||||
used under normal circumstances.
|
||||
:param auth: The authenticator to use, defaults to an instance of
|
||||
:class:`AuthExternal <dbus_next.auth.AuthExternal>`.
|
||||
:type auth: :class:`Authenticator <dbus_next.auth.Authenticator>`
|
||||
|
||||
:ivar connected: True if this message bus is expected to be able to send
|
||||
and receive messages.
|
||||
:vartype connected: bool
|
||||
:ivar unique_name: The unique name of the message bus connection. It will
|
||||
be :class:`None` until the message bus connects.
|
||||
:vartype unique_name: str
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bus_address: str = None,
|
||||
bus_type: BusType = BusType.SESSION,
|
||||
auth: Authenticator = None,
|
||||
):
|
||||
if _import_error:
|
||||
raise _import_error
|
||||
|
||||
super().__init__(bus_address, bus_type, ProxyObject)
|
||||
self._main_context = GLib.main_context_default()
|
||||
# buffer messages until connect
|
||||
self._buffered_messages = []
|
||||
|
||||
if auth is None:
|
||||
self._auth = AuthExternal()
|
||||
else:
|
||||
self._auth = auth
|
||||
|
||||
def connect(
|
||||
self, connect_notify: Callable[["MessageBus", Optional[Exception]], None] = None
|
||||
):
|
||||
"""Connect this message bus to the DBus daemon.
|
||||
|
||||
This method or the synchronous version must be called before the
|
||||
message bus can be used.
|
||||
|
||||
:param connect_notify: A callback that will be called with this message
|
||||
bus. May return an :class:`Exception` on connection errors or
|
||||
:class:`AuthError <dbus_next.AuthError>` on authorization errors.
|
||||
:type callback: :class:`Callable`
|
||||
"""
|
||||
|
||||
def authenticate_notify(exc):
|
||||
if exc is not None:
|
||||
if connect_notify is not None:
|
||||
connect_notify(None, exc)
|
||||
return
|
||||
self.message_source = _MessageSource(self)
|
||||
self.message_source.set_callback(self._on_message)
|
||||
self.message_source.attach(self._main_context)
|
||||
|
||||
self.writable_source = None
|
||||
|
||||
self.message_source.add_unix_fd(self._fd, GLib.IO_IN)
|
||||
|
||||
def on_hello(reply, err):
|
||||
if err:
|
||||
if connect_notify:
|
||||
connect_notify(reply, err)
|
||||
return
|
||||
|
||||
self.unique_name = reply.body[0]
|
||||
|
||||
for m in self._buffered_messages:
|
||||
self.send(m)
|
||||
|
||||
if connect_notify:
|
||||
connect_notify(self, err)
|
||||
|
||||
hello_msg = Message(
|
||||
destination="org.freedesktop.DBus",
|
||||
path="/org/freedesktop/DBus",
|
||||
interface="org.freedesktop.DBus",
|
||||
member="Hello",
|
||||
serial=self.next_serial(),
|
||||
)
|
||||
|
||||
self._method_return_handlers[hello_msg.serial] = on_hello
|
||||
self._stream.write(hello_msg._marshall())
|
||||
self._stream.flush()
|
||||
|
||||
self._authenticate(authenticate_notify)
|
||||
|
||||
def connect_sync(self) -> "MessageBus":
|
||||
"""Connect this message bus to the DBus daemon.
|
||||
|
||||
This method or the asynchronous version must be called before the
|
||||
message bus can be used.
|
||||
|
||||
:returns: This message bus for convenience.
|
||||
:rtype: :class:`MessageBus <dbus_next.glib.MessageBus>`
|
||||
|
||||
:raises:
|
||||
- :class:`AuthError <dbus_next.AuthError>` - If authorization to \
|
||||
the DBus daemon failed.
|
||||
- :class:`Exception` - If there was a connection error.
|
||||
"""
|
||||
main = GLib.MainLoop()
|
||||
connection_error = None
|
||||
|
||||
def connect_notify(bus, err):
|
||||
nonlocal connection_error
|
||||
connection_error = err
|
||||
main.quit()
|
||||
|
||||
self.connect(connect_notify)
|
||||
main.run()
|
||||
|
||||
if connection_error:
|
||||
raise connection_error
|
||||
|
||||
return self
|
||||
|
||||
def call(
|
||||
self,
|
||||
msg: Message,
|
||||
reply_notify: Callable[[Optional[Message], Optional[Exception]], None] = None,
|
||||
):
|
||||
"""Send a method call and asynchronously wait for a reply from the DBus
|
||||
daemon.
|
||||
|
||||
:param msg: The method call message to send.
|
||||
:type msg: :class:`Message <dbus_next.Message>`
|
||||
:param reply_notify: A callback that will be called with the reply to
|
||||
this message. May return an :class:`Exception` on connection errors.
|
||||
:type reply_notify: Callable
|
||||
"""
|
||||
self._call(msg, reply_notify)
|
||||
|
||||
def call_sync(self, msg: Message) -> Optional[Message]:
|
||||
"""Send a method call and synchronously wait for a reply from the DBus
|
||||
daemon.
|
||||
|
||||
:param msg: The method call message to send.
|
||||
:type msg: :class:`Message <dbus_next.Message>`
|
||||
|
||||
:returns: A message in reply to the message sent. If the message does
|
||||
not expect a reply based on the message flags or type, returns
|
||||
``None`` immediately.
|
||||
:rtype: :class:`Message <dbus_next.Message>`
|
||||
|
||||
:raises:
|
||||
- :class:`DBusError <dbus_next.DBusError>` - If the service threw \
|
||||
an error for the method call or returned an invalid result.
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
"""
|
||||
if (
|
||||
msg.flags & MessageFlag.NO_REPLY_EXPECTED
|
||||
or msg.message_type is not MessageType.METHOD_CALL
|
||||
):
|
||||
self.send(msg)
|
||||
return None
|
||||
|
||||
if not msg.serial:
|
||||
msg.serial = self.next_serial()
|
||||
|
||||
main = GLib.MainLoop()
|
||||
handler_reply = None
|
||||
connection_error = None
|
||||
|
||||
def reply_handler(reply, err):
|
||||
nonlocal handler_reply
|
||||
nonlocal connection_error
|
||||
|
||||
handler_reply = reply
|
||||
connection_error = err
|
||||
|
||||
main.quit()
|
||||
|
||||
self._method_return_handlers[msg.serial] = reply_handler
|
||||
self.send(msg)
|
||||
main.run()
|
||||
|
||||
if connection_error:
|
||||
raise connection_error
|
||||
|
||||
return handler_reply
|
||||
|
||||
def introspect_sync(self, bus_name: str, path: str) -> intr.Node:
|
||||
"""Get introspection data for the node at the given path from the given
|
||||
bus name.
|
||||
|
||||
Calls the standard ``org.freedesktop.DBus.Introspectable.Introspect``
|
||||
on the bus for the path.
|
||||
|
||||
:param bus_name: The name to introspect.
|
||||
:type bus_name: str
|
||||
:param path: The path to introspect.
|
||||
:type path: str
|
||||
|
||||
:returns: The introspection data for the name at the path.
|
||||
:rtype: :class:`Node <dbus_next.introspection.Node>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidObjectPathError <dbus_next.InvalidObjectPathError>` \
|
||||
- If the given object path is not valid.
|
||||
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
|
||||
the given bus name is not valid.
|
||||
- :class:`DBusError <dbus_next.DBusError>` - If the service threw \
|
||||
an error for the method call or returned an invalid result.
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
"""
|
||||
main = GLib.MainLoop()
|
||||
request_result = None
|
||||
request_error = None
|
||||
|
||||
def reply_notify(result, err):
|
||||
nonlocal request_result
|
||||
nonlocal request_error
|
||||
|
||||
request_result = result
|
||||
request_error = err
|
||||
|
||||
main.quit()
|
||||
|
||||
super().introspect(bus_name, path, reply_notify)
|
||||
main.run()
|
||||
|
||||
if request_error:
|
||||
raise request_error
|
||||
|
||||
return request_result
|
||||
|
||||
def request_name_sync(
|
||||
self, name: str, flags: NameFlag = NameFlag.NONE
|
||||
) -> RequestNameReply:
|
||||
"""Request that this message bus owns the given name.
|
||||
|
||||
:param name: The name to request.
|
||||
:type name: str
|
||||
:param flags: Name flags that affect the behavior of the name request.
|
||||
:type flags: :class:`NameFlag <dbus_next.NameFlag>`
|
||||
|
||||
:returns: The reply to the name request.
|
||||
:rtype: :class:`RequestNameReply <dbus_next.RequestNameReply>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
|
||||
the given bus name is not valid.
|
||||
- :class:`DBusError <dbus_next.DBusError>` - If the service threw \
|
||||
an error for the method call or returned an invalid result.
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
"""
|
||||
main = GLib.MainLoop()
|
||||
request_result = None
|
||||
request_error = None
|
||||
|
||||
def reply_notify(result, err):
|
||||
nonlocal request_result
|
||||
nonlocal request_error
|
||||
|
||||
request_result = result
|
||||
request_error = err
|
||||
|
||||
main.quit()
|
||||
|
||||
super().request_name(name, flags, reply_notify)
|
||||
main.run()
|
||||
|
||||
if request_error:
|
||||
raise request_error
|
||||
|
||||
return request_result
|
||||
|
||||
def release_name_sync(self, name: str) -> ReleaseNameReply:
|
||||
"""Request that this message bus release the given name.
|
||||
|
||||
:param name: The name to release.
|
||||
:type name: str
|
||||
|
||||
:returns: The reply to the release request.
|
||||
:rtype: :class:`ReleaseNameReply <dbus_next.ReleaseNameReply>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If \
|
||||
the given bus name is not valid.
|
||||
- :class:`DBusError <dbus_next.DBusError>` - If the service threw \
|
||||
an error for the method call or returned an invalid result.
|
||||
- :class:`Exception` - If a connection error occurred.
|
||||
"""
|
||||
main = GLib.MainLoop()
|
||||
release_result = None
|
||||
release_error = None
|
||||
|
||||
def reply_notify(result, err):
|
||||
nonlocal release_result
|
||||
nonlocal release_error
|
||||
|
||||
release_result = result
|
||||
release_error = err
|
||||
|
||||
main.quit()
|
||||
|
||||
super().release_name(name, reply_notify)
|
||||
main.run()
|
||||
|
||||
if release_error:
|
||||
raise release_error
|
||||
|
||||
return release_result
|
||||
|
||||
def send(self, msg: Message):
|
||||
if not msg.serial:
|
||||
msg.serial = self.next_serial()
|
||||
|
||||
self._buffered_messages.append(msg)
|
||||
|
||||
if self.unique_name:
|
||||
self._schedule_write()
|
||||
|
||||
def get_proxy_object(
|
||||
self, bus_name: str, path: str, introspection: intr.Node
|
||||
) -> ProxyObject:
|
||||
return super().get_proxy_object(bus_name, path, introspection)
|
||||
|
||||
def _schedule_write(self):
|
||||
if self.writable_source is None or self.writable_source.is_destroyed():
|
||||
self.writable_source = _MessageWritableSource(self)
|
||||
self.writable_source.attach(self._main_context)
|
||||
self.writable_source.add_unix_fd(self._fd, GLib.IO_OUT)
|
||||
|
||||
def _authenticate(self, authenticate_notify):
|
||||
self._stream.write(b"\0")
|
||||
first_line = self._auth._authentication_start()
|
||||
if first_line is not None:
|
||||
if type(first_line) is not str:
|
||||
raise AuthError("authenticator gave response not type str")
|
||||
self._stream.write(f"{first_line}\r\n".encode())
|
||||
self._stream.flush()
|
||||
|
||||
def line_notify(line):
|
||||
try:
|
||||
resp = self._auth._receive_line(line)
|
||||
self._stream.write(Authenticator._format_line(resp))
|
||||
self._stream.flush()
|
||||
if resp == "BEGIN":
|
||||
self._readline_source = None
|
||||
authenticate_notify(None)
|
||||
return True
|
||||
except Exception as e:
|
||||
authenticate_notify(e)
|
||||
return True
|
||||
|
||||
readline_source = _AuthLineSource(self._stream)
|
||||
readline_source.set_callback(line_notify)
|
||||
readline_source.add_unix_fd(self._fd, GLib.IO_IN)
|
||||
readline_source.attach(self._main_context)
|
||||
# make sure it doesnt get cleaned up
|
||||
self._readline_source = readline_source
|
||||
309
src/dbus_fast/glib/proxy_object.py
Normal file
309
src/dbus_fast/glib/proxy_object.py
Normal file
@@ -0,0 +1,309 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List, Union
|
||||
|
||||
from .. import introspection as intr
|
||||
from ..constants import ErrorType
|
||||
from ..errors import DBusError
|
||||
from ..message import Message
|
||||
from ..message_bus import BaseMessageBus
|
||||
from ..proxy_object import BaseProxyInterface, BaseProxyObject
|
||||
from ..signature import Variant
|
||||
|
||||
# glib is optional
|
||||
try:
|
||||
from gi.repository import GLib
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class ProxyInterface(BaseProxyInterface):
|
||||
"""A class representing a proxy to an interface exported on the bus by
|
||||
another client for the GLib :class:`MessageBus <dbus_next.glib.MessageBus>`
|
||||
implementation.
|
||||
|
||||
This class is not meant to be constructed directly by the user. Use
|
||||
:func:`ProxyObject.get_interface()
|
||||
<dbus_next.glib.ProxyObject.get_interface>` on a GLib proxy
|
||||
object to get a proxy interface.
|
||||
|
||||
This class exposes methods to call DBus methods, listen to signals, and get
|
||||
and set properties on the interface that are created dynamically based on
|
||||
the introspection data passed to the proxy object that made this proxy
|
||||
interface.
|
||||
|
||||
A *method call* takes this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
def callback(error: Exception, result: list(Any)):
|
||||
pass
|
||||
|
||||
interface.call_[METHOD](*args, callback)
|
||||
result = interface.call_[METHOD]_sync(*args)
|
||||
|
||||
Where ``METHOD`` is the name of the method converted to snake case.
|
||||
|
||||
To call a method, provide ``*args`` that correspond to the *in args* of the
|
||||
introspection method definition.
|
||||
|
||||
To *asynchronously* call a method, provide a callback that takes an error
|
||||
as the first argument and a list as the second argument. If the call
|
||||
completed successfully, ``error`` will be :class:`None`. If the service
|
||||
returns an error, it will be a :class:`DBusError <dbus_next.DBusError>`
|
||||
with information about the error returned from the bus. The result will be
|
||||
a list of values that correspond to the *out args* of the introspection
|
||||
method definition.
|
||||
|
||||
To *synchronously* call a method, use the ``call_[METHOD]_sync()`` form.
|
||||
The ``result`` corresponds to the *out arg* of the introspection method
|
||||
definition. If the method has more than one otu arg, they are returned
|
||||
within a :class:`list`.
|
||||
|
||||
To *listen to a signal* use this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
interface.on_[SIGNAL](callback)
|
||||
|
||||
To *stop listening to a signal* use this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
interface.off_[SIGNAL](callback)
|
||||
|
||||
Where ``SIGNAL`` is the name of the signal converted to snake case.
|
||||
|
||||
DBus signals are exposed with an event-callback interface. The provided
|
||||
``callback`` will be called when the signal is emitted with arguments that
|
||||
correspond to the *out args* of the interface signal definition.
|
||||
|
||||
To *get or set a property* use this form:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
def get_callback(error: Exception, value: Any):
|
||||
pass
|
||||
|
||||
def set_callback(error: Exception)
|
||||
pass
|
||||
|
||||
interface.get_[PROPERTY](get_callback)
|
||||
value: Any = interface.get_[PROPERTY]_sync()
|
||||
|
||||
interface.set_[PROPERTY](set_callback)
|
||||
interface.set_[PROPERTY]_sync(value)
|
||||
|
||||
Where ``PROPERTY`` is the name of the property converted to snake case.
|
||||
|
||||
The ``value`` must correspond to the type of the property in the interface
|
||||
definition.
|
||||
|
||||
To asynchronously get or set a property, provide a callback that takes an
|
||||
:class:`Exception` as the first argument. If the call completed
|
||||
successfully, ``error`` will be :class:`None`. If the service returns an
|
||||
error, it will be a :class:`DBusError <dbus_next.DBusError>` with
|
||||
information about the error returned from the bus.
|
||||
|
||||
If the service returns an error for a synchronous DBus call, a
|
||||
:class:`DBusError <dbus_next.DBusError>` will be raised with information
|
||||
about the error.
|
||||
"""
|
||||
|
||||
def _add_method(self, intr_method):
|
||||
in_len = len(intr_method.in_args)
|
||||
out_len = len(intr_method.out_args)
|
||||
|
||||
def method_fn(*args):
|
||||
if len(args) != in_len + 1:
|
||||
raise TypeError(
|
||||
f"method {intr_method.name} expects {in_len} arguments and a callback (got {len(args)} args)"
|
||||
)
|
||||
|
||||
args = list(args)
|
||||
# TODO type check: this callback takes two parameters
|
||||
# (MessageBus.check_callback(cb))
|
||||
callback = args.pop()
|
||||
|
||||
def call_notify(msg, err):
|
||||
if err:
|
||||
callback([], err)
|
||||
return
|
||||
|
||||
try:
|
||||
BaseProxyInterface._check_method_return(
|
||||
msg, intr_method.out_signature
|
||||
)
|
||||
except DBusError as e:
|
||||
err = e
|
||||
|
||||
callback(msg.body, err)
|
||||
|
||||
self.bus.call(
|
||||
Message(
|
||||
destination=self.bus_name,
|
||||
path=self.path,
|
||||
interface=self.introspection.name,
|
||||
member=intr_method.name,
|
||||
signature=intr_method.in_signature,
|
||||
body=list(args),
|
||||
),
|
||||
call_notify,
|
||||
)
|
||||
|
||||
def method_fn_sync(*args):
|
||||
main = GLib.MainLoop()
|
||||
call_error = None
|
||||
call_body = None
|
||||
|
||||
def callback(body, err):
|
||||
nonlocal call_error
|
||||
nonlocal call_body
|
||||
call_error = err
|
||||
call_body = body
|
||||
main.quit()
|
||||
|
||||
method_fn(*args, callback)
|
||||
|
||||
main.run()
|
||||
|
||||
if call_error:
|
||||
raise call_error
|
||||
|
||||
if not out_len:
|
||||
return None
|
||||
elif out_len == 1:
|
||||
return call_body[0]
|
||||
else:
|
||||
return call_body
|
||||
|
||||
method_name = f"call_{BaseProxyInterface._to_snake_case(intr_method.name)}"
|
||||
method_name_sync = f"{method_name}_sync"
|
||||
|
||||
setattr(self, method_name, method_fn)
|
||||
setattr(self, method_name_sync, method_fn_sync)
|
||||
|
||||
def _add_property(self, intr_property):
|
||||
def property_getter(callback):
|
||||
def call_notify(msg, err):
|
||||
if err:
|
||||
callback(None, err)
|
||||
return
|
||||
|
||||
try:
|
||||
BaseProxyInterface._check_method_return(msg)
|
||||
except Exception as e:
|
||||
callback(None, e)
|
||||
return
|
||||
|
||||
variant = msg.body[0]
|
||||
if variant.signature != intr_property.signature:
|
||||
err = DBusError(
|
||||
ErrorType.CLIENT_ERROR,
|
||||
'property returned unexpected signature "{variant.signature}"',
|
||||
msg,
|
||||
)
|
||||
callback(None, err)
|
||||
return
|
||||
|
||||
callback(variant.value, None)
|
||||
|
||||
self.bus.call(
|
||||
Message(
|
||||
destination=self.bus_name,
|
||||
path=self.path,
|
||||
interface="org.freedesktop.DBus.Properties",
|
||||
member="Get",
|
||||
signature="ss",
|
||||
body=[self.introspection.name, intr_property.name],
|
||||
),
|
||||
call_notify,
|
||||
)
|
||||
|
||||
def property_getter_sync():
|
||||
property_value = None
|
||||
reply_error = None
|
||||
|
||||
main = GLib.MainLoop()
|
||||
|
||||
def callback(value, err):
|
||||
nonlocal property_value
|
||||
nonlocal reply_error
|
||||
property_value = value
|
||||
reply_error = err
|
||||
main.quit()
|
||||
|
||||
property_getter(callback)
|
||||
main.run()
|
||||
if reply_error:
|
||||
raise reply_error
|
||||
return property_value
|
||||
|
||||
def property_setter(value, callback):
|
||||
def call_notify(msg, err):
|
||||
if err:
|
||||
callback(None, err)
|
||||
return
|
||||
try:
|
||||
BaseProxyInterface._check_method_return(msg)
|
||||
except Exception as e:
|
||||
callback(None, e)
|
||||
return
|
||||
|
||||
return callback(None, None)
|
||||
|
||||
variant = Variant(intr_property.signature, value)
|
||||
self.bus.call(
|
||||
Message(
|
||||
destination=self.bus_name,
|
||||
path=self.path,
|
||||
interface="org.freedesktop.DBus.Properties",
|
||||
member="Set",
|
||||
signature="ssv",
|
||||
body=[self.introspection.name, intr_property.name, variant],
|
||||
),
|
||||
call_notify,
|
||||
)
|
||||
|
||||
def property_setter_sync(val):
|
||||
reply_error = None
|
||||
|
||||
main = GLib.MainLoop()
|
||||
|
||||
def callback(value, err):
|
||||
nonlocal reply_error
|
||||
reply_error = err
|
||||
main.quit()
|
||||
|
||||
property_setter(val, callback)
|
||||
main.run()
|
||||
if reply_error:
|
||||
raise reply_error
|
||||
return None
|
||||
|
||||
snake_case = super()._to_snake_case(intr_property.name)
|
||||
setattr(self, f"get_{snake_case}", property_getter)
|
||||
setattr(self, f"get_{snake_case}_sync", property_getter_sync)
|
||||
setattr(self, f"set_{snake_case}", property_setter)
|
||||
setattr(self, f"set_{snake_case}_sync", property_setter_sync)
|
||||
|
||||
|
||||
class ProxyObject(BaseProxyObject):
|
||||
"""The proxy object implementation for the asyncio :class:`MessageBus <dbus_next.aio.MessageBus>`.
|
||||
|
||||
For more information, see the :class:`BaseProxyObject <dbus_next.proxy_object.BaseProxyObject>`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bus_name: str,
|
||||
path: str,
|
||||
introspection: Union[intr.Node, str, ET.Element],
|
||||
bus: BaseMessageBus,
|
||||
):
|
||||
super().__init__(bus_name, path, introspection, bus, ProxyInterface)
|
||||
|
||||
def get_interface(self, name: str) -> ProxyInterface:
|
||||
return super().get_interface(name)
|
||||
|
||||
def get_children(self) -> List["ProxyObject"]:
|
||||
return super().get_children()
|
||||
594
src/dbus_fast/introspection.py
Normal file
594
src/dbus_fast/introspection.py
Normal file
@@ -0,0 +1,594 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List, Union
|
||||
|
||||
from .constants import ArgDirection, PropertyAccess
|
||||
from .errors import InvalidIntrospectionError
|
||||
from .signature import SignatureTree, SignatureType
|
||||
from .validators import assert_interface_name_valid, assert_member_name_valid
|
||||
|
||||
# https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format
|
||||
# TODO annotations
|
||||
|
||||
|
||||
class Arg:
|
||||
"""A class that represents an input or output argument to a signal or a method.
|
||||
|
||||
:ivar name: The name of this arg.
|
||||
:vartype name: str
|
||||
:ivar direction: Whether this is an input or an output argument.
|
||||
:vartype direction: :class:`ArgDirection <dbus_next.ArgDirection>`
|
||||
:ivar type: The parsed signature type of this argument.
|
||||
:vartype type: :class:`SignatureType <dbus_next.SignatureType>`
|
||||
:ivar signature: The signature string of this argument.
|
||||
:vartype signature: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidMemberNameError <dbus_next.InvalidMemberNameError>` - If the name of the arg is not valid.
|
||||
- :class:`InvalidSignatureError <dbus_next.InvalidSignatureError>` - If the signature is not valid.
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the signature is not a single complete type.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
signature: Union[SignatureType, str],
|
||||
direction: List[ArgDirection] = None,
|
||||
name: str = None,
|
||||
):
|
||||
if name is not None:
|
||||
assert_member_name_valid(name)
|
||||
|
||||
type_ = None
|
||||
if type(signature) is SignatureType:
|
||||
type_ = signature
|
||||
signature = signature.signature
|
||||
else:
|
||||
tree = SignatureTree._get(signature)
|
||||
if len(tree.types) != 1:
|
||||
raise InvalidIntrospectionError(
|
||||
f"an argument must have a single complete type. (has {len(tree.types)} types)"
|
||||
)
|
||||
type_ = tree.types[0]
|
||||
|
||||
self.type = type_
|
||||
self.signature = signature
|
||||
self.name = name
|
||||
self.direction = direction
|
||||
|
||||
def from_xml(element: ET.Element, direction: ArgDirection) -> "Arg":
|
||||
"""Convert a :class:`xml.etree.ElementTree.Element` into a
|
||||
:class:`Arg`.
|
||||
|
||||
The element must be valid DBus introspection XML for an ``arg``.
|
||||
|
||||
:param element: The parsed XML element.
|
||||
:type element: :class:`xml.etree.ElementTree.Element`
|
||||
:param direction: The direction of this arg. Must be specified because it can default to different values depending on if it's in a method or signal.
|
||||
:type direction: :class:`ArgDirection <dbus_next.ArgDirection>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the XML tree is not valid introspection data.
|
||||
"""
|
||||
name = element.attrib.get("name")
|
||||
signature = element.attrib.get("type")
|
||||
|
||||
if not signature:
|
||||
raise InvalidIntrospectionError(
|
||||
'a method argument must have a "type" attribute'
|
||||
)
|
||||
|
||||
return Arg(signature, direction, name)
|
||||
|
||||
def to_xml(self) -> ET.Element:
|
||||
"""Convert this :class:`Arg` into an :class:`xml.etree.ElementTree.Element`."""
|
||||
element = ET.Element("arg")
|
||||
if self.name:
|
||||
element.set("name", self.name)
|
||||
|
||||
if self.direction:
|
||||
element.set("direction", self.direction.value)
|
||||
element.set("type", self.signature)
|
||||
|
||||
return element
|
||||
|
||||
|
||||
class Signal:
|
||||
"""A class that represents a signal exposed on an interface.
|
||||
|
||||
:ivar name: The name of this signal
|
||||
:vartype name: str
|
||||
:ivar args: A list of output arguments for this signal.
|
||||
:vartype args: list(Arg)
|
||||
:ivar signature: The collected signature of the output arguments.
|
||||
:vartype signature: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidMemberNameError <dbus_next.InvalidMemberNameError>` - If the name of the signal is not a valid member name.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, args: List[Arg] = None):
|
||||
if name is not None:
|
||||
assert_member_name_valid(name)
|
||||
|
||||
self.name = name
|
||||
self.args = args or []
|
||||
self.signature = "".join(arg.signature for arg in self.args)
|
||||
|
||||
def from_xml(element):
|
||||
"""Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Signal`.
|
||||
|
||||
The element must be valid DBus introspection XML for a ``signal``.
|
||||
|
||||
:param element: The parsed XML element.
|
||||
:type element: :class:`xml.etree.ElementTree.Element`
|
||||
:param is_root: Whether this is the root node
|
||||
:type is_root: bool
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the XML tree is not valid introspection data.
|
||||
"""
|
||||
name = element.attrib.get("name")
|
||||
if not name:
|
||||
raise InvalidIntrospectionError('signals must have a "name" attribute')
|
||||
|
||||
args = []
|
||||
for child in element:
|
||||
if child.tag == "arg":
|
||||
args.append(Arg.from_xml(child, ArgDirection.OUT))
|
||||
|
||||
signal = Signal(name, args)
|
||||
|
||||
return signal
|
||||
|
||||
def to_xml(self) -> ET.Element:
|
||||
"""Convert this :class:`Signal` into an :class:`xml.etree.ElementTree.Element`."""
|
||||
element = ET.Element("signal")
|
||||
element.set("name", self.name)
|
||||
|
||||
for arg in self.args:
|
||||
element.append(arg.to_xml())
|
||||
|
||||
return element
|
||||
|
||||
|
||||
class Method:
|
||||
"""A class that represents a method exposed on an :class:`Interface`.
|
||||
|
||||
:ivar name: The name of this method.
|
||||
:vartype name: str
|
||||
:ivar in_args: A list of input arguments to this method.
|
||||
:vartype in_args: list(Arg)
|
||||
:ivar out_args: A list of output arguments to this method.
|
||||
:vartype out_args: list(Arg)
|
||||
:ivar in_signature: The collected signature string of the input arguments.
|
||||
:vartype in_signature: str
|
||||
:ivar out_signature: The collected signature string of the output arguments.
|
||||
:vartype out_signature: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidMemberNameError <dbus_next.InvalidMemberNameError>` - If the name of this method is not valid.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, in_args: List[Arg] = [], out_args: List[Arg] = []):
|
||||
assert_member_name_valid(name)
|
||||
|
||||
self.name = name
|
||||
self.in_args = in_args
|
||||
self.out_args = out_args
|
||||
self.in_signature = "".join(arg.signature for arg in in_args)
|
||||
self.out_signature = "".join(arg.signature for arg in out_args)
|
||||
|
||||
def from_xml(element: ET.Element) -> "Method":
|
||||
"""Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Method`.
|
||||
|
||||
The element must be valid DBus introspection XML for a ``method``.
|
||||
|
||||
:param element: The parsed XML element.
|
||||
:type element: :class:`xml.etree.ElementTree.Element`
|
||||
:param is_root: Whether this is the root node
|
||||
:type is_root: bool
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the XML tree is not valid introspection data.
|
||||
"""
|
||||
name = element.attrib.get("name")
|
||||
if not name:
|
||||
raise InvalidIntrospectionError('interfaces must have a "name" attribute')
|
||||
|
||||
in_args = []
|
||||
out_args = []
|
||||
|
||||
for child in element:
|
||||
if child.tag == "arg":
|
||||
direction = ArgDirection(child.attrib.get("direction", "in"))
|
||||
arg = Arg.from_xml(child, direction)
|
||||
if direction == ArgDirection.IN:
|
||||
in_args.append(arg)
|
||||
elif direction == ArgDirection.OUT:
|
||||
out_args.append(arg)
|
||||
|
||||
return Method(name, in_args, out_args)
|
||||
|
||||
def to_xml(self) -> ET.Element:
|
||||
"""Convert this :class:`Method` into an :class:`xml.etree.ElementTree.Element`."""
|
||||
element = ET.Element("method")
|
||||
element.set("name", self.name)
|
||||
|
||||
for arg in self.in_args:
|
||||
element.append(arg.to_xml())
|
||||
for arg in self.out_args:
|
||||
element.append(arg.to_xml())
|
||||
|
||||
return element
|
||||
|
||||
|
||||
class Property:
|
||||
"""A class that represents a DBus property exposed on an
|
||||
:class:`Interface`.
|
||||
|
||||
:ivar name: The name of this property.
|
||||
:vartype name: str
|
||||
:ivar signature: The signature string for this property. Must be a single complete type.
|
||||
:vartype signature: str
|
||||
:ivar access: Whether this property is readable and writable.
|
||||
:vartype access: :class:`PropertyAccess <dbus_next.PropertyAccess>`
|
||||
:ivar type: The parsed type of this property.
|
||||
:vartype type: :class:`SignatureType <dbus_next.SignatureType>`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the property is not a single complete type.
|
||||
- :class `InvalidSignatureError <dbus_next.InvalidSignatureError>` - If the given signature is not valid.
|
||||
- :class: `InvalidMemberNameError <dbus_next.InvalidMemberNameError>` - If the member name is not valid.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
signature: str,
|
||||
access: PropertyAccess = PropertyAccess.READWRITE,
|
||||
):
|
||||
assert_member_name_valid(name)
|
||||
|
||||
tree = SignatureTree._get(signature)
|
||||
if len(tree.types) != 1:
|
||||
raise InvalidIntrospectionError(
|
||||
f"properties must have a single complete type. (has {len(tree.types)} types)"
|
||||
)
|
||||
|
||||
self.name = name
|
||||
self.signature = signature
|
||||
self.access = access
|
||||
self.type = tree.types[0]
|
||||
|
||||
def from_xml(element):
|
||||
"""Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Property`.
|
||||
|
||||
The element must be valid DBus introspection XML for a ``property``.
|
||||
|
||||
:param element: The parsed XML element.
|
||||
:type element: :class:`xml.etree.ElementTree.Element`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the XML tree is not valid introspection data.
|
||||
"""
|
||||
name = element.attrib.get("name")
|
||||
signature = element.attrib.get("type")
|
||||
access = PropertyAccess(element.attrib.get("access", "readwrite"))
|
||||
|
||||
if not name:
|
||||
raise InvalidIntrospectionError('properties must have a "name" attribute')
|
||||
if not signature:
|
||||
raise InvalidIntrospectionError('properties must have a "type" attribute')
|
||||
|
||||
return Property(name, signature, access)
|
||||
|
||||
def to_xml(self) -> ET.Element:
|
||||
"""Convert this :class:`Property` into an :class:`xml.etree.ElementTree.Element`."""
|
||||
element = ET.Element("property")
|
||||
element.set("name", self.name)
|
||||
element.set("type", self.signature)
|
||||
element.set("access", self.access.value)
|
||||
return element
|
||||
|
||||
|
||||
class Interface:
|
||||
"""A class that represents a DBus interface exported on on object path.
|
||||
|
||||
Contains information about the methods, signals, and properties exposed on
|
||||
this interface.
|
||||
|
||||
:ivar name: The name of this interface.
|
||||
:vartype name: str
|
||||
:ivar methods: A list of methods exposed on this interface.
|
||||
:vartype methods: list(:class:`Method`)
|
||||
:ivar signals: A list of signals exposed on this interface.
|
||||
:vartype signals: list(:class:`Signal`)
|
||||
:ivar properties: A list of properties exposed on this interface.
|
||||
:vartype properties: list(:class:`Property`)
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidInterfaceNameError <dbus_next.InvalidInterfaceNameError>` - If the name is not a valid interface name.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
methods: List[Method] = None,
|
||||
signals: List[Signal] = None,
|
||||
properties: List[Property] = None,
|
||||
):
|
||||
assert_interface_name_valid(name)
|
||||
|
||||
self.name = name
|
||||
self.methods = methods if methods is not None else []
|
||||
self.signals = signals if signals is not None else []
|
||||
self.properties = properties if properties is not None else []
|
||||
|
||||
@staticmethod
|
||||
def from_xml(element: ET.Element) -> "Interface":
|
||||
"""Convert a :class:`xml.etree.ElementTree.Element` into a
|
||||
:class:`Interface`.
|
||||
|
||||
The element must be valid DBus introspection XML for an ``interface``.
|
||||
|
||||
:param element: The parsed XML element.
|
||||
:type element: :class:`xml.etree.ElementTree.Element`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the XML tree is not valid introspection data.
|
||||
"""
|
||||
name = element.attrib.get("name")
|
||||
if not name:
|
||||
raise InvalidIntrospectionError('interfaces must have a "name" attribute')
|
||||
|
||||
interface = Interface(name)
|
||||
|
||||
for child in element:
|
||||
if child.tag == "method":
|
||||
interface.methods.append(Method.from_xml(child))
|
||||
elif child.tag == "signal":
|
||||
interface.signals.append(Signal.from_xml(child))
|
||||
elif child.tag == "property":
|
||||
interface.properties.append(Property.from_xml(child))
|
||||
|
||||
return interface
|
||||
|
||||
def to_xml(self) -> ET.Element:
|
||||
"""Convert this :class:`Interface` into an :class:`xml.etree.ElementTree.Element`."""
|
||||
element = ET.Element("interface")
|
||||
element.set("name", self.name)
|
||||
|
||||
for method in self.methods:
|
||||
element.append(method.to_xml())
|
||||
for signal in self.signals:
|
||||
element.append(signal.to_xml())
|
||||
for prop in self.properties:
|
||||
element.append(prop.to_xml())
|
||||
|
||||
return element
|
||||
|
||||
|
||||
class Node:
|
||||
"""A class that represents a node in an object path in introspection data.
|
||||
|
||||
A node contains information about interfaces exported on this path and
|
||||
child nodes. A node can be converted to and from introspection XML exposed
|
||||
through the ``org.freedesktop.DBus.Introspectable`` standard DBus
|
||||
interface.
|
||||
|
||||
This class is an essential building block for a high-level DBus interface.
|
||||
This is the underlying data structure for the :class:`ProxyObject
|
||||
<dbus_next.proxy_object.BaseProxyInterface>`. A :class:`ServiceInterface
|
||||
<dbus_next.service.ServiceInterface>` definition is converted to this class
|
||||
to expose XML on the introspectable interface.
|
||||
|
||||
:ivar interfaces: A list of interfaces exposed on this node.
|
||||
:vartype interfaces: list(:class:`Interface <dbus_next.introspection.Interface>`)
|
||||
:ivar nodes: A list of child nodes.
|
||||
:vartype nodes: list(:class:`Node`)
|
||||
:ivar name: The object path of this node.
|
||||
:vartype name: str
|
||||
:ivar is_root: Whether this is the root node. False if it is a child node.
|
||||
:vartype is_root: bool
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the name is not a valid node name.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, name: str = None, interfaces: List[Interface] = None, is_root: bool = True
|
||||
):
|
||||
if not is_root and not name:
|
||||
raise InvalidIntrospectionError('child nodes must have a "name" attribute')
|
||||
|
||||
self.interfaces = interfaces if interfaces is not None else []
|
||||
self.nodes = []
|
||||
self.name = name
|
||||
self.is_root = is_root
|
||||
|
||||
@staticmethod
|
||||
def from_xml(element: ET.Element, is_root: bool = False):
|
||||
"""Convert an :class:`xml.etree.ElementTree.Element` to a :class:`Node`.
|
||||
|
||||
The element must be valid DBus introspection XML for a ``node``.
|
||||
|
||||
:param element: The parsed XML element.
|
||||
:type element: :class:`xml.etree.ElementTree.Element`
|
||||
:param is_root: Whether this is the root node
|
||||
:type is_root: bool
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the XML tree is not valid introspection data.
|
||||
"""
|
||||
node = Node(element.attrib.get("name"), is_root=is_root)
|
||||
|
||||
for child in element:
|
||||
if child.tag == "interface":
|
||||
node.interfaces.append(Interface.from_xml(child))
|
||||
elif child.tag == "node":
|
||||
node.nodes.append(Node.from_xml(child))
|
||||
|
||||
return node
|
||||
|
||||
@staticmethod
|
||||
def parse(data: str) -> "Node":
|
||||
"""Parse XML data as a string into a :class:`Node`.
|
||||
|
||||
The string must be valid DBus introspection XML.
|
||||
|
||||
:param data: The XMl string.
|
||||
:type data: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the string is not valid introspection data.
|
||||
"""
|
||||
element = ET.fromstring(data)
|
||||
if element.tag != "node":
|
||||
raise InvalidIntrospectionError(
|
||||
'introspection data must have a "node" for the root element'
|
||||
)
|
||||
|
||||
return Node.from_xml(element, is_root=True)
|
||||
|
||||
def to_xml(self) -> ET.Element:
|
||||
"""Convert this :class:`Node` into an :class:`xml.etree.ElementTree.Element`."""
|
||||
element = ET.Element("node")
|
||||
|
||||
if self.name:
|
||||
element.set("name", self.name)
|
||||
|
||||
for interface in self.interfaces:
|
||||
element.append(interface.to_xml())
|
||||
for node in self.nodes:
|
||||
element.append(node.to_xml())
|
||||
|
||||
return element
|
||||
|
||||
def tostring(self) -> str:
|
||||
"""Convert this :class:`Node` into a DBus introspection XML string."""
|
||||
header = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"\n"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">\n'
|
||||
|
||||
def indent(elem, level=0):
|
||||
i = "\n" + level * " "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level + 1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
xml = self.to_xml()
|
||||
indent(xml)
|
||||
return header + ET.tostring(xml, encoding="unicode").rstrip()
|
||||
|
||||
@staticmethod
|
||||
def default(name: str = None) -> "Node":
|
||||
"""Create a :class:`Node` with the default interfaces supported by this library.
|
||||
|
||||
The default interfaces include:
|
||||
|
||||
* ``org.freedesktop.DBus.Introspectable``
|
||||
* ``org.freedesktop.DBus.Peer``
|
||||
* ``org.freedesktop.DBus.Properties``
|
||||
* ``org.freedesktop.DBus.ObjectManager``
|
||||
"""
|
||||
return Node(
|
||||
name,
|
||||
is_root=True,
|
||||
interfaces=[
|
||||
Interface(
|
||||
"org.freedesktop.DBus.Introspectable",
|
||||
methods=[
|
||||
Method(
|
||||
"Introspect", out_args=[Arg("s", ArgDirection.OUT, "data")]
|
||||
)
|
||||
],
|
||||
),
|
||||
Interface(
|
||||
"org.freedesktop.DBus.Peer",
|
||||
methods=[
|
||||
Method(
|
||||
"GetMachineId",
|
||||
out_args=[Arg("s", ArgDirection.OUT, "machine_uuid")],
|
||||
),
|
||||
Method("Ping"),
|
||||
],
|
||||
),
|
||||
Interface(
|
||||
"org.freedesktop.DBus.Properties",
|
||||
methods=[
|
||||
Method(
|
||||
"Get",
|
||||
in_args=[
|
||||
Arg("s", ArgDirection.IN, "interface_name"),
|
||||
Arg("s", ArgDirection.IN, "property_name"),
|
||||
],
|
||||
out_args=[Arg("v", ArgDirection.OUT, "value")],
|
||||
),
|
||||
Method(
|
||||
"Set",
|
||||
in_args=[
|
||||
Arg("s", ArgDirection.IN, "interface_name"),
|
||||
Arg("s", ArgDirection.IN, "property_name"),
|
||||
Arg("v", ArgDirection.IN, "value"),
|
||||
],
|
||||
),
|
||||
Method(
|
||||
"GetAll",
|
||||
in_args=[Arg("s", ArgDirection.IN, "interface_name")],
|
||||
out_args=[Arg("a{sv}", ArgDirection.OUT, "props")],
|
||||
),
|
||||
],
|
||||
signals=[
|
||||
Signal(
|
||||
"PropertiesChanged",
|
||||
args=[
|
||||
Arg("s", ArgDirection.OUT, "interface_name"),
|
||||
Arg("a{sv}", ArgDirection.OUT, "changed_properties"),
|
||||
Arg("as", ArgDirection.OUT, "invalidated_properties"),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
Interface(
|
||||
"org.freedesktop.DBus.ObjectManager",
|
||||
methods=[
|
||||
Method(
|
||||
"GetManagedObjects",
|
||||
out_args=[
|
||||
Arg(
|
||||
"a{oa{sa{sv}}}",
|
||||
ArgDirection.OUT,
|
||||
"objpath_interfaces_and_properties",
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
signals=[
|
||||
Signal(
|
||||
"InterfacesAdded",
|
||||
args=[
|
||||
Arg("o", ArgDirection.OUT, "object_path"),
|
||||
Arg(
|
||||
"a{sa{sv}}",
|
||||
ArgDirection.OUT,
|
||||
"interfaces_and_properties",
|
||||
),
|
||||
],
|
||||
),
|
||||
Signal(
|
||||
"InterfacesRemoved",
|
||||
args=[
|
||||
Arg("o", ArgDirection.OUT, "object_path"),
|
||||
Arg("as", ArgDirection.OUT, "interfaces"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
282
src/dbus_fast/message.py
Normal file
282
src/dbus_fast/message.py
Normal file
@@ -0,0 +1,282 @@
|
||||
from typing import Any, List
|
||||
|
||||
from ._private.constants import LITTLE_ENDIAN, PROTOCOL_VERSION, HeaderField
|
||||
from ._private.marshaller import Marshaller
|
||||
from .constants import ErrorType, MessageFlag, MessageType
|
||||
from .errors import InvalidMessageError
|
||||
from .signature import SignatureTree, Variant
|
||||
from .validators import (
|
||||
assert_bus_name_valid,
|
||||
assert_interface_name_valid,
|
||||
assert_member_name_valid,
|
||||
assert_object_path_valid,
|
||||
)
|
||||
|
||||
|
||||
class Message:
|
||||
"""A class for sending and receiving messages through the
|
||||
:class:`MessageBus <dbus_next.message_bus.BaseMessageBus>` with the
|
||||
low-level api.
|
||||
|
||||
A ``Message`` can be constructed by the user to send over the message bus.
|
||||
When messages are received, such as from method calls or signal emissions,
|
||||
they will use this class as well.
|
||||
|
||||
:ivar destination: The address of the client for which this message is intended.
|
||||
:vartype destination: str
|
||||
:ivar path: The intended object path exported on the destination bus.
|
||||
:vartype path: str
|
||||
:ivar interface: The intended interface on the object path.
|
||||
:vartype interface: str
|
||||
:ivar member: The intended member on the interface.
|
||||
:vartype member: str
|
||||
:ivar message_type: The type of this message. A method call, signal, method return, or error.
|
||||
:vartype message_type: :class:`MessageType`
|
||||
:ivar flags: Flags that affect the behavior of this message.
|
||||
:vartype flags: :class:`MessageFlag`
|
||||
:ivar error_name: If this message is an error, the name of this error. Must be a valid interface name.
|
||||
:vartype error_name: str
|
||||
:ivar reply_serial: If this is a return type, the serial this message is in reply to.
|
||||
:vartype reply_serial: int
|
||||
:ivar sender: The address of the sender of this message. Will be a unique name.
|
||||
:vartype sender: str
|
||||
:ivar unix_fds: A list of unix fds that were sent in the header of this message.
|
||||
:vartype unix_fds: list(int)
|
||||
:ivar signature: The signature of the body of this message.
|
||||
:vartype signature: str
|
||||
:ivar signature_tree: The signature parsed as a signature tree.
|
||||
:vartype signature_tree: :class:`SignatureTree`
|
||||
:ivar body: The body of this message. Must match the signature.
|
||||
:vartype body: list(Any)
|
||||
:ivar serial: The serial of the message. Will be automatically set during message sending if not present. Use the ``new_serial()`` method of the bus to generate a serial.
|
||||
:vartype serial: int
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidMessageError` - If the message is malformed or missing fields for the message type.
|
||||
- :class:`InvalidSignatureError` - If the given signature is not valid.
|
||||
- :class:`InvalidObjectPathError` - If ``path`` is not a valid object path.
|
||||
- :class:`InvalidBusNameError` - If ``destination`` is not a valid bus name.
|
||||
- :class:`InvalidMemberNameError` - If ``member`` is not a valid member name.
|
||||
- :class:`InvalidInterfaceNameError` - If ``error_name`` or ``interface`` is not a valid interface name.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
destination: str = None,
|
||||
path: str = None,
|
||||
interface: str = None,
|
||||
member: str = None,
|
||||
message_type: MessageType = MessageType.METHOD_CALL,
|
||||
flags: MessageFlag = MessageFlag.NONE,
|
||||
error_name: str = None,
|
||||
reply_serial: int = None,
|
||||
sender: str = None,
|
||||
unix_fds: List[int] = [],
|
||||
signature: str = "",
|
||||
body: List[Any] = [],
|
||||
serial: int = 0,
|
||||
):
|
||||
self.destination = destination
|
||||
self.path = path
|
||||
self.interface = interface
|
||||
self.member = member
|
||||
self.message_type = message_type
|
||||
self.flags = (
|
||||
flags if type(flags) is MessageFlag else MessageFlag(bytes([flags]))
|
||||
)
|
||||
self.error_name = (
|
||||
error_name if type(error_name) is not ErrorType else error_name.value
|
||||
)
|
||||
self.reply_serial = reply_serial
|
||||
self.sender = sender
|
||||
self.unix_fds = unix_fds
|
||||
self.signature = (
|
||||
signature.signature if type(signature) is SignatureTree else signature
|
||||
)
|
||||
self.signature_tree = (
|
||||
signature
|
||||
if type(signature) is SignatureTree
|
||||
else SignatureTree._get(signature)
|
||||
)
|
||||
self.body = body
|
||||
self.serial = serial
|
||||
|
||||
if self.destination is not None:
|
||||
assert_bus_name_valid(self.destination)
|
||||
if self.interface is not None:
|
||||
assert_interface_name_valid(self.interface)
|
||||
if self.path is not None:
|
||||
assert_object_path_valid(self.path)
|
||||
if self.member is not None:
|
||||
assert_member_name_valid(self.member)
|
||||
if self.error_name is not None:
|
||||
assert_interface_name_valid(self.error_name)
|
||||
|
||||
def require_fields(*fields):
|
||||
for field in fields:
|
||||
if not getattr(self, field):
|
||||
raise InvalidMessageError(f"missing required field: {field}")
|
||||
|
||||
if self.message_type == MessageType.METHOD_CALL:
|
||||
require_fields("path", "member")
|
||||
elif self.message_type == MessageType.SIGNAL:
|
||||
require_fields("path", "member", "interface")
|
||||
elif self.message_type == MessageType.ERROR:
|
||||
require_fields("error_name", "reply_serial")
|
||||
elif self.message_type == MessageType.METHOD_RETURN:
|
||||
require_fields("reply_serial")
|
||||
else:
|
||||
raise InvalidMessageError(f"got unknown message type: {self.message_type}")
|
||||
|
||||
@staticmethod
|
||||
def new_error(msg: "Message", error_name: str, error_text: str) -> "Message":
|
||||
"""A convenience constructor to create an error message in reply to the given message.
|
||||
|
||||
:param msg: The message this error is in reply to.
|
||||
:type msg: :class:`Message`
|
||||
:param error_name: The name of this error. Must be a valid interface name.
|
||||
:type error_name: str
|
||||
:param error_text: Human-readable text for the error.
|
||||
|
||||
:returns: The error message.
|
||||
:rtype: :class:`Message`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidInterfaceNameError` - If the error_name is not a valid interface name.
|
||||
"""
|
||||
return Message(
|
||||
message_type=MessageType.ERROR,
|
||||
reply_serial=msg.serial,
|
||||
destination=msg.sender,
|
||||
error_name=error_name,
|
||||
signature="s",
|
||||
body=[error_text],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def new_method_return(
|
||||
msg: "Message",
|
||||
signature: str = "",
|
||||
body: List[Any] = [],
|
||||
unix_fds: List[int] = [],
|
||||
) -> "Message":
|
||||
"""A convenience constructor to create a method return to the given method call message.
|
||||
|
||||
:param msg: The method call message this is a reply to.
|
||||
:type msg: :class:`Message`
|
||||
:param signature: The signature for the message body.
|
||||
:type signature: str
|
||||
:param body: The body of this message. Must match the signature.
|
||||
:type body: list(Any)
|
||||
:param unix_fds: List integer file descriptors to send with this message.
|
||||
:type body: list(int)
|
||||
|
||||
:returns: The method return message
|
||||
:rtype: :class:`Message`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidSignatureError` - If the signature is not a valid signature.
|
||||
"""
|
||||
return Message(
|
||||
message_type=MessageType.METHOD_RETURN,
|
||||
reply_serial=msg.serial,
|
||||
destination=msg.sender,
|
||||
signature=signature,
|
||||
body=body,
|
||||
unix_fds=unix_fds,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def new_signal(
|
||||
path: str,
|
||||
interface: str,
|
||||
member: str,
|
||||
signature: str = "",
|
||||
body: List[Any] = None,
|
||||
unix_fds: List[int] = None,
|
||||
) -> "Message":
|
||||
"""A convenience constructor to create a new signal message.
|
||||
|
||||
:param path: The path of this signal.
|
||||
:type path: str
|
||||
:param interface: The interface of this signal.
|
||||
:type interface: str
|
||||
:param member: The member name of this signal.
|
||||
:type member: str
|
||||
:param signature: The signature of the signal body.
|
||||
:type signature: str
|
||||
:param body: The body of this signal message.
|
||||
:type body: list(Any)
|
||||
:param unix_fds: List integer file descriptors to send with this message.
|
||||
:type body: list(int)
|
||||
|
||||
:returns: The signal message.
|
||||
:rtype: :class:`Message`
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidSignatureError` - If the signature is not a valid signature.
|
||||
- :class:`InvalidObjectPathError` - If ``path`` is not a valid object path.
|
||||
- :class:`InvalidInterfaceNameError` - If ``interface`` is not a valid interface name.
|
||||
- :class:`InvalidMemberNameError` - If ``member`` is not a valid member name.
|
||||
"""
|
||||
body = body if body else []
|
||||
return Message(
|
||||
message_type=MessageType.SIGNAL,
|
||||
interface=interface,
|
||||
path=path,
|
||||
member=member,
|
||||
signature=signature,
|
||||
body=body,
|
||||
unix_fds=unix_fds,
|
||||
)
|
||||
|
||||
def _matches(self, **kwargs):
|
||||
for attr, val in kwargs.items():
|
||||
if getattr(self, attr) != val:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _marshall(self, negotiate_unix_fd=False):
|
||||
# TODO maximum message size is 134217728 (128 MiB)
|
||||
body_block = Marshaller(self.signature, self.body)
|
||||
body_block.marshall()
|
||||
|
||||
fields = []
|
||||
|
||||
if self.path:
|
||||
fields.append([HeaderField.PATH.value, Variant("o", self.path)])
|
||||
if self.interface:
|
||||
fields.append([HeaderField.INTERFACE.value, Variant("s", self.interface)])
|
||||
if self.member:
|
||||
fields.append([HeaderField.MEMBER.value, Variant("s", self.member)])
|
||||
if self.error_name:
|
||||
fields.append([HeaderField.ERROR_NAME.value, Variant("s", self.error_name)])
|
||||
if self.reply_serial:
|
||||
fields.append(
|
||||
[HeaderField.REPLY_SERIAL.value, Variant("u", self.reply_serial)]
|
||||
)
|
||||
if self.destination:
|
||||
fields.append(
|
||||
[HeaderField.DESTINATION.value, Variant("s", self.destination)]
|
||||
)
|
||||
if self.signature:
|
||||
fields.append([HeaderField.SIGNATURE.value, Variant("g", self.signature)])
|
||||
if self.unix_fds and negotiate_unix_fd:
|
||||
fields.append(
|
||||
[HeaderField.UNIX_FDS.value, Variant("u", len(self.unix_fds))]
|
||||
)
|
||||
|
||||
header_body = [
|
||||
LITTLE_ENDIAN,
|
||||
self.message_type.value,
|
||||
self.flags.value,
|
||||
PROTOCOL_VERSION,
|
||||
len(body_block.buffer),
|
||||
self.serial,
|
||||
fields,
|
||||
]
|
||||
header_block = Marshaller("yyyyuua(yv)", header_body)
|
||||
header_block.marshall()
|
||||
header_block.align(8)
|
||||
return header_block.buffer + body_block.buffer
|
||||
1183
src/dbus_fast/message_bus.py
Normal file
1183
src/dbus_fast/message_bus.py
Normal file
File diff suppressed because it is too large
Load Diff
295
src/dbus_fast/proxy_object.py
Normal file
295
src/dbus_fast/proxy_object.py
Normal file
@@ -0,0 +1,295 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Coroutine, List, Type, Union
|
||||
|
||||
from . import introspection as intr
|
||||
from . import message_bus
|
||||
from ._private.util import replace_idx_with_fds
|
||||
from .constants import ErrorType, MessageType
|
||||
from .errors import DBusError, InterfaceNotFoundError
|
||||
from .message import Message
|
||||
from .validators import assert_bus_name_valid, assert_object_path_valid
|
||||
|
||||
|
||||
class BaseProxyInterface:
|
||||
"""An abstract class representing a proxy to an interface exported on the bus by another client.
|
||||
|
||||
Implementations of this class are not meant to be constructed directly by
|
||||
users. Use :func:`BaseProxyObject.get_interface` to get a proxy interface.
|
||||
Each message bus implementation provides its own proxy interface
|
||||
implementation that will be returned by that method.
|
||||
|
||||
Proxy interfaces can be used to call methods, get properties, and listen to
|
||||
signals on the interface. Proxy interfaces are created dynamically with a
|
||||
family of methods for each of these operations based on what members the
|
||||
interface exposes. Each proxy interface implementation exposes these
|
||||
members in a different way depending on the features of the backend. See
|
||||
the documentation of the proxy interface implementation you use for more
|
||||
details.
|
||||
|
||||
:ivar bus_name: The name of the bus this interface is exported on.
|
||||
:vartype bus_name: str
|
||||
:ivar path: The object path exported on the client that owns the bus name.
|
||||
:vartype path: str
|
||||
:ivar introspection: Parsed introspection data for the proxy interface.
|
||||
:vartype introspection: :class:`Node <dbus_next.introspection.Interface>`
|
||||
:ivar bus: The message bus this proxy interface is connected to.
|
||||
:vartype bus: :class:`BaseMessageBus <dbus_next.message_bus.BaseMessageBus>`
|
||||
"""
|
||||
|
||||
def __init__(self, bus_name, path, introspection, bus):
|
||||
|
||||
self.bus_name = bus_name
|
||||
self.path = path
|
||||
self.introspection = introspection
|
||||
self.bus = bus
|
||||
self._signal_handlers = {}
|
||||
self._signal_match_rule = f"type='signal',sender={bus_name},interface={introspection.name},path={path}"
|
||||
|
||||
_underscorer1 = re.compile(r"(.)([A-Z][a-z]+)")
|
||||
_underscorer2 = re.compile(r"([a-z0-9])([A-Z])")
|
||||
|
||||
@staticmethod
|
||||
def _to_snake_case(member):
|
||||
subbed = BaseProxyInterface._underscorer1.sub(r"\1_\2", member)
|
||||
return BaseProxyInterface._underscorer2.sub(r"\1_\2", subbed).lower()
|
||||
|
||||
@staticmethod
|
||||
def _check_method_return(msg, signature=None):
|
||||
if msg.message_type == MessageType.ERROR:
|
||||
raise DBusError._from_message(msg)
|
||||
elif msg.message_type != MessageType.METHOD_RETURN:
|
||||
raise DBusError(
|
||||
ErrorType.CLIENT_ERROR, "method call didnt return a method return", msg
|
||||
)
|
||||
elif signature is not None and msg.signature != signature:
|
||||
raise DBusError(
|
||||
ErrorType.CLIENT_ERROR,
|
||||
f'method call returned unexpected signature: "{msg.signature}"',
|
||||
msg,
|
||||
)
|
||||
|
||||
def _add_method(self, intr_method):
|
||||
raise NotImplementedError("this must be implemented in the inheriting class")
|
||||
|
||||
def _add_property(self, intr_property):
|
||||
raise NotImplementedError("this must be implemented in the inheriting class")
|
||||
|
||||
def _message_handler(self, msg):
|
||||
if (
|
||||
not msg._matches(
|
||||
message_type=MessageType.SIGNAL,
|
||||
interface=self.introspection.name,
|
||||
path=self.path,
|
||||
)
|
||||
or msg.member not in self._signal_handlers
|
||||
):
|
||||
return
|
||||
|
||||
if (
|
||||
msg.sender != self.bus_name
|
||||
and self.bus._name_owners.get(self.bus_name, "") != msg.sender
|
||||
):
|
||||
# The sender is always a unique name, but the bus name given might
|
||||
# be a well known name. If the sender isn't an exact match, check
|
||||
# to see if it owns the bus_name we were given from the cache kept
|
||||
# on the bus for this purpose.
|
||||
return
|
||||
|
||||
match = [s for s in self.introspection.signals if s.name == msg.member]
|
||||
if not len(match):
|
||||
return
|
||||
intr_signal = match[0]
|
||||
if intr_signal.signature != msg.signature:
|
||||
logging.warning(
|
||||
f'got signal "{self.introspection.name}.{msg.member}" with unexpected signature "{msg.signature}"'
|
||||
)
|
||||
return
|
||||
|
||||
body = replace_idx_with_fds(msg.signature, msg.body, msg.unix_fds)
|
||||
for handler in self._signal_handlers[msg.member]:
|
||||
cb_result = handler(*body)
|
||||
if isinstance(cb_result, Coroutine):
|
||||
asyncio.create_task(cb_result)
|
||||
|
||||
def _add_signal(self, intr_signal, interface):
|
||||
def on_signal_fn(fn):
|
||||
fn_signature = inspect.signature(fn)
|
||||
if not callable(fn) or len(fn_signature.parameters) != len(
|
||||
intr_signal.args
|
||||
):
|
||||
raise TypeError(
|
||||
f"reply_notify must be a function with {len(intr_signal.args)} parameters"
|
||||
)
|
||||
|
||||
if not self._signal_handlers:
|
||||
self.bus._add_match_rule(self._signal_match_rule)
|
||||
self.bus.add_message_handler(self._message_handler)
|
||||
|
||||
if intr_signal.name not in self._signal_handlers:
|
||||
self._signal_handlers[intr_signal.name] = []
|
||||
|
||||
self._signal_handlers[intr_signal.name].append(fn)
|
||||
|
||||
def off_signal_fn(fn):
|
||||
try:
|
||||
i = self._signal_handlers[intr_signal.name].index(fn)
|
||||
del self._signal_handlers[intr_signal.name][i]
|
||||
if not self._signal_handlers[intr_signal.name]:
|
||||
del self._signal_handlers[intr_signal.name]
|
||||
except (KeyError, ValueError):
|
||||
return
|
||||
|
||||
if not self._signal_handlers:
|
||||
self.bus._remove_match_rule(self._signal_match_rule)
|
||||
self.bus.remove_message_handler(self._message_handler)
|
||||
|
||||
snake_case = BaseProxyInterface._to_snake_case(intr_signal.name)
|
||||
setattr(interface, f"on_{snake_case}", on_signal_fn)
|
||||
setattr(interface, f"off_{snake_case}", off_signal_fn)
|
||||
|
||||
|
||||
class BaseProxyObject:
|
||||
"""An abstract class representing a proxy to an object exported on the bus by another client.
|
||||
|
||||
Implementations of this class are not meant to be constructed directly. Use
|
||||
:func:`BaseMessageBus.get_proxy_object()
|
||||
<dbus_next.message_bus.BaseMessageBus.get_proxy_object>` to get a proxy
|
||||
object. Each message bus implementation provides its own proxy object
|
||||
implementation that will be returned by that method.
|
||||
|
||||
The primary use of the proxy object is to select a proxy interface to act
|
||||
on. Information on what interfaces are available is provided by
|
||||
introspection data provided to this class. This introspection data can
|
||||
either be included in your project as an XML file (recommended) or
|
||||
retrieved from the ``org.freedesktop.DBus.Introspectable`` interface at
|
||||
runtime.
|
||||
|
||||
:ivar bus_name: The name of the bus this object is exported on.
|
||||
:vartype bus_name: str
|
||||
:ivar path: The object path exported on the client that owns the bus name.
|
||||
:vartype path: str
|
||||
:ivar introspection: Parsed introspection data for the proxy object.
|
||||
:vartype introspection: :class:`Node <dbus_next.introspection.Node>`
|
||||
:ivar bus: The message bus this proxy object is connected to.
|
||||
:vartype bus: :class:`BaseMessageBus <dbus_next.message_bus.BaseMessageBus>`
|
||||
:ivar ~.ProxyInterface: The proxy interface class this proxy object uses.
|
||||
:vartype ~.ProxyInterface: Type[:class:`BaseProxyInterface <dbus_next.proxy_object.BaseProxyObject>`]
|
||||
:ivar child_paths: A list of absolute object paths of the children of this object.
|
||||
:vartype child_paths: list(str)
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidBusNameError <dbus_next.InvalidBusNameError>` - If the given bus name is not valid.
|
||||
- :class:`InvalidObjectPathError <dbus_next.InvalidObjectPathError>` - If the given object path is not valid.
|
||||
- :class:`InvalidIntrospectionError <dbus_next.InvalidIntrospectionError>` - If the introspection data for the node is not valid.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bus_name: str,
|
||||
path: str,
|
||||
introspection: Union[intr.Node, str, ET.Element],
|
||||
bus: "message_bus.BaseMessageBus",
|
||||
ProxyInterface: Type[BaseProxyInterface],
|
||||
):
|
||||
assert_object_path_valid(path)
|
||||
assert_bus_name_valid(bus_name)
|
||||
|
||||
if not isinstance(bus, message_bus.BaseMessageBus):
|
||||
raise TypeError("bus must be an instance of BaseMessageBus")
|
||||
if not issubclass(ProxyInterface, BaseProxyInterface):
|
||||
raise TypeError("ProxyInterface must be an instance of BaseProxyInterface")
|
||||
|
||||
if type(introspection) is intr.Node:
|
||||
self.introspection = introspection
|
||||
elif type(introspection) is str:
|
||||
self.introspection = intr.Node.parse(introspection)
|
||||
elif type(introspection) is ET.Element:
|
||||
self.introspection = intr.Node.from_xml(introspection)
|
||||
else:
|
||||
raise TypeError(
|
||||
"introspection must be xml node introspection or introspection.Node class"
|
||||
)
|
||||
|
||||
self.bus_name = bus_name
|
||||
self.path = path
|
||||
self.bus = bus
|
||||
self.ProxyInterface = ProxyInterface
|
||||
self.child_paths = [f"{path}/{n.name}" for n in self.introspection.nodes]
|
||||
|
||||
self._interfaces = {}
|
||||
|
||||
# lazy loaded by get_children()
|
||||
self._children = None
|
||||
|
||||
def get_interface(self, name: str) -> BaseProxyInterface:
|
||||
"""Get an interface exported on this proxy object and connect it to the bus.
|
||||
|
||||
:param name: The name of the interface to retrieve.
|
||||
:type name: str
|
||||
|
||||
:raises:
|
||||
- :class:`InterfaceNotFoundError <dbus_next.InterfaceNotFoundError>` - If there is no interface by this name exported on the bus.
|
||||
"""
|
||||
if name in self._interfaces:
|
||||
return self._interfaces[name]
|
||||
|
||||
try:
|
||||
intr_interface = next(
|
||||
i for i in self.introspection.interfaces if i.name == name
|
||||
)
|
||||
except StopIteration:
|
||||
raise InterfaceNotFoundError(f"interface not found on this object: {name}")
|
||||
|
||||
interface = self.ProxyInterface(
|
||||
self.bus_name, self.path, intr_interface, self.bus
|
||||
)
|
||||
|
||||
for intr_method in intr_interface.methods:
|
||||
interface._add_method(intr_method)
|
||||
for intr_property in intr_interface.properties:
|
||||
interface._add_property(intr_property)
|
||||
for intr_signal in intr_interface.signals:
|
||||
interface._add_signal(intr_signal, interface)
|
||||
|
||||
def get_owner_notify(msg, err):
|
||||
if err:
|
||||
logging.error(f'getting name owner for "{name}" failed, {err}')
|
||||
return
|
||||
if msg.message_type == MessageType.ERROR:
|
||||
if msg.error_name != ErrorType.NAME_HAS_NO_OWNER.value:
|
||||
logging.error(
|
||||
f'getting name owner for "{name}" failed, {msg.body[0]}'
|
||||
)
|
||||
return
|
||||
|
||||
self.bus._name_owners[self.bus_name] = msg.body[0]
|
||||
|
||||
if self.bus_name[0] != ":" and not self.bus._name_owners.get(self.bus_name, ""):
|
||||
self.bus._call(
|
||||
Message(
|
||||
destination="org.freedesktop.DBus",
|
||||
interface="org.freedesktop.DBus",
|
||||
path="/org/freedesktop/DBus",
|
||||
member="GetNameOwner",
|
||||
signature="s",
|
||||
body=[self.bus_name],
|
||||
),
|
||||
get_owner_notify,
|
||||
)
|
||||
|
||||
self._interfaces[name] = interface
|
||||
return interface
|
||||
|
||||
def get_children(self) -> List["BaseProxyObject"]:
|
||||
"""Get the child nodes of this proxy object according to the introspection data."""
|
||||
if self._children is None:
|
||||
self._children = [
|
||||
self.__class__(self.bus_name, self.path, child, self.bus)
|
||||
for child in self.introspection.nodes
|
||||
]
|
||||
|
||||
return self._children
|
||||
578
src/dbus_fast/service.py
Normal file
578
src/dbus_fast/service.py
Normal file
@@ -0,0 +1,578 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from typing import Any, Dict, List, no_type_check_decorator
|
||||
|
||||
from . import introspection as intr
|
||||
from ._private.util import (
|
||||
parse_annotation,
|
||||
replace_fds_with_idx,
|
||||
replace_idx_with_fds,
|
||||
signature_contains_type,
|
||||
)
|
||||
from .constants import PropertyAccess
|
||||
from .errors import SignalDisabledError
|
||||
from .signature import SignatureBodyMismatchError, SignatureTree, Variant
|
||||
|
||||
|
||||
class _Method:
|
||||
def __init__(self, fn, name, disabled=False):
|
||||
in_signature = ""
|
||||
out_signature = ""
|
||||
|
||||
inspection = inspect.signature(fn)
|
||||
|
||||
in_args = []
|
||||
for i, param in enumerate(inspection.parameters.values()):
|
||||
if i == 0:
|
||||
# first is self
|
||||
continue
|
||||
annotation = parse_annotation(param.annotation)
|
||||
if not annotation:
|
||||
raise ValueError(
|
||||
"method parameters must specify the dbus type string as an annotation"
|
||||
)
|
||||
in_args.append(intr.Arg(annotation, intr.ArgDirection.IN, param.name))
|
||||
in_signature += annotation
|
||||
|
||||
out_args = []
|
||||
out_signature = parse_annotation(inspection.return_annotation)
|
||||
if out_signature:
|
||||
for type_ in SignatureTree._get(out_signature).types:
|
||||
out_args.append(intr.Arg(type_, intr.ArgDirection.OUT))
|
||||
|
||||
self.name = name
|
||||
self.fn = fn
|
||||
self.disabled = disabled
|
||||
self.introspection = intr.Method(name, in_args, out_args)
|
||||
self.in_signature = in_signature
|
||||
self.out_signature = out_signature
|
||||
self.in_signature_tree = SignatureTree._get(in_signature)
|
||||
self.out_signature_tree = SignatureTree._get(out_signature)
|
||||
|
||||
|
||||
def method(name: str = None, disabled: bool = False):
|
||||
"""A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus service method.
|
||||
|
||||
The parameters and return value must each be annotated with a signature
|
||||
string of a single complete DBus type.
|
||||
|
||||
This class method will be called when a client calls the method on the DBus
|
||||
interface. The parameters given to the function come from the calling
|
||||
client and will conform to the dbus-next type system. The parameters
|
||||
returned will be returned to the calling client and must conform to the
|
||||
dbus-next type system. If multiple parameters are returned, they must be
|
||||
contained within a :class:`list`.
|
||||
|
||||
The decorated method may raise a :class:`DBusError <dbus_next.DBusError>`
|
||||
to return an error to the client.
|
||||
|
||||
:param name: The member name that DBus clients will use to call this method. Defaults to the name of the class method.
|
||||
:type name: str
|
||||
:param disabled: If set to true, the method will not be visible to clients.
|
||||
:type disabled: bool
|
||||
|
||||
:example:
|
||||
|
||||
::
|
||||
|
||||
@method()
|
||||
def echo(self, val: 's') -> 's':
|
||||
return val
|
||||
|
||||
@method()
|
||||
def echo_two(self, val1: 's', val2: 'u') -> 'su':
|
||||
return [val1, val2]
|
||||
"""
|
||||
if name is not None and type(name) is not str:
|
||||
raise TypeError("name must be a string")
|
||||
if type(disabled) is not bool:
|
||||
raise TypeError("disabled must be a bool")
|
||||
|
||||
@no_type_check_decorator
|
||||
def decorator(fn):
|
||||
@wraps(fn)
|
||||
def wrapped(*args, **kwargs):
|
||||
fn(*args, **kwargs)
|
||||
|
||||
fn_name = name if name else fn.__name__
|
||||
wrapped.__dict__["__DBUS_METHOD"] = _Method(fn, fn_name, disabled=disabled)
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class _Signal:
|
||||
def __init__(self, fn, name, disabled=False):
|
||||
inspection = inspect.signature(fn)
|
||||
|
||||
args = []
|
||||
signature = ""
|
||||
signature_tree = None
|
||||
|
||||
return_annotation = parse_annotation(inspection.return_annotation)
|
||||
|
||||
if return_annotation:
|
||||
signature = return_annotation
|
||||
signature_tree = SignatureTree._get(signature)
|
||||
for type_ in signature_tree.types:
|
||||
args.append(intr.Arg(type_, intr.ArgDirection.OUT))
|
||||
else:
|
||||
signature = ""
|
||||
signature_tree = SignatureTree._get("")
|
||||
|
||||
self.signature = signature
|
||||
self.signature_tree = signature_tree
|
||||
self.name = name
|
||||
self.disabled = disabled
|
||||
self.introspection = intr.Signal(self.name, args)
|
||||
|
||||
|
||||
def signal(name: str = None, disabled: bool = False):
|
||||
"""A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus signal.
|
||||
|
||||
The signal is broadcast on the bus when the decorated class method is
|
||||
called by the user.
|
||||
|
||||
If the signal has an out argument, the class method must have a return type
|
||||
annotation with a signature string of a single complete DBus type and the
|
||||
return value of the class method must conform to the dbus-next type system.
|
||||
If the signal has multiple out arguments, they must be returned within a
|
||||
``list``.
|
||||
|
||||
:param name: The member name that will be used for this signal. Defaults to
|
||||
the name of the class method.
|
||||
:type name: str
|
||||
:param disabled: If set to true, the signal will not be visible to clients.
|
||||
:type disabled: bool
|
||||
|
||||
:example:
|
||||
|
||||
::
|
||||
|
||||
@signal()
|
||||
def string_signal(self, val) -> 's':
|
||||
return val
|
||||
|
||||
@signal()
|
||||
def two_strings_signal(self, val1, val2) -> 'ss':
|
||||
return [val1, val2]
|
||||
"""
|
||||
if name is not None and type(name) is not str:
|
||||
raise TypeError("name must be a string")
|
||||
if type(disabled) is not bool:
|
||||
raise TypeError("disabled must be a bool")
|
||||
|
||||
@no_type_check_decorator
|
||||
def decorator(fn):
|
||||
fn_name = name if name else fn.__name__
|
||||
signal = _Signal(fn, fn_name, disabled)
|
||||
|
||||
@wraps(fn)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
if signal.disabled:
|
||||
raise SignalDisabledError("Tried to call a disabled signal")
|
||||
result = fn(self, *args, **kwargs)
|
||||
ServiceInterface._handle_signal(self, signal, result)
|
||||
return result
|
||||
|
||||
wrapped.__dict__["__DBUS_SIGNAL"] = signal
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class _Property(property):
|
||||
def set_options(self, options):
|
||||
self.options = getattr(self, "options", {})
|
||||
for k, v in options.items():
|
||||
self.options[k] = v
|
||||
|
||||
if "name" in options and options["name"] is not None:
|
||||
self.name = options["name"]
|
||||
else:
|
||||
self.name = self.prop_getter.__name__
|
||||
|
||||
if "access" in options:
|
||||
self.access = PropertyAccess(options["access"])
|
||||
else:
|
||||
self.access = PropertyAccess.READWRITE
|
||||
|
||||
if "disabled" in options:
|
||||
self.disabled = options["disabled"]
|
||||
else:
|
||||
self.disabled = False
|
||||
|
||||
self.introspection = intr.Property(self.name, self.signature, self.access)
|
||||
|
||||
self.__dict__["__DBUS_PROPERTY"] = True
|
||||
|
||||
def __init__(self, fn, *args, **kwargs):
|
||||
self.prop_getter = fn
|
||||
self.prop_setter = None
|
||||
|
||||
inspection = inspect.signature(fn)
|
||||
if len(inspection.parameters) != 1:
|
||||
raise ValueError('the property must only have the "self" input parameter')
|
||||
|
||||
return_annotation = parse_annotation(inspection.return_annotation)
|
||||
|
||||
if not return_annotation:
|
||||
raise ValueError(
|
||||
"the property must specify the dbus type string as a return annotation string"
|
||||
)
|
||||
|
||||
self.signature = return_annotation
|
||||
tree = SignatureTree._get(return_annotation)
|
||||
|
||||
if len(tree.types) != 1:
|
||||
raise ValueError("the property signature must be a single complete type")
|
||||
|
||||
self.type = tree.types[0]
|
||||
|
||||
if "options" in kwargs:
|
||||
options = kwargs["options"]
|
||||
self.set_options(options)
|
||||
del kwargs["options"]
|
||||
|
||||
super().__init__(fn, *args, **kwargs)
|
||||
|
||||
def setter(self, fn, **kwargs):
|
||||
# XXX The setter decorator seems to be recreating the class in the list
|
||||
# of class members and clobbering the options so we need to reset them.
|
||||
# Why does it do that?
|
||||
result = super().setter(fn, **kwargs)
|
||||
result.prop_setter = fn
|
||||
result.set_options(self.options)
|
||||
return result
|
||||
|
||||
|
||||
def dbus_property(
|
||||
access: PropertyAccess = PropertyAccess.READWRITE,
|
||||
name: str = None,
|
||||
disabled: bool = False,
|
||||
):
|
||||
"""A decorator to mark a class method of a :class:`ServiceInterface` to be a DBus property.
|
||||
|
||||
The class method must be a Python getter method with a return annotation
|
||||
that is a signature string of a single complete DBus type. When a client
|
||||
gets the property through the ``org.freedesktop.DBus.Properties``
|
||||
interface, the getter will be called and the resulting value will be
|
||||
returned to the client.
|
||||
|
||||
If the property is writable, it must have a setter method that takes a
|
||||
single parameter that is annotated with the same signature. When a client
|
||||
sets the property through the ``org.freedesktop.DBus.Properties``
|
||||
interface, the setter will be called with the value from the calling
|
||||
client.
|
||||
|
||||
The parameters of the getter and the setter must conform to the dbus-next
|
||||
type system. The getter or the setter may raise a :class:`DBusError
|
||||
<dbus_next.DBusError>` to return an error to the client.
|
||||
|
||||
:param name: The name that DBus clients will use to interact with this
|
||||
property on the bus.
|
||||
:type name: str
|
||||
:param disabled: If set to true, the property will not be visible to
|
||||
clients.
|
||||
:type disabled: bool
|
||||
|
||||
:example:
|
||||
|
||||
::
|
||||
|
||||
@dbus_property()
|
||||
def string_prop(self) -> 's':
|
||||
return self._string_prop
|
||||
|
||||
@string_prop.setter
|
||||
def string_prop(self, val: 's'):
|
||||
self._string_prop = val
|
||||
"""
|
||||
if type(access) is not PropertyAccess:
|
||||
raise TypeError("access must be a PropertyAccess class")
|
||||
if name is not None and type(name) is not str:
|
||||
raise TypeError("name must be a string")
|
||||
if type(disabled) is not bool:
|
||||
raise TypeError("disabled must be a bool")
|
||||
|
||||
@no_type_check_decorator
|
||||
def decorator(fn):
|
||||
options = {"name": name, "access": access, "disabled": disabled}
|
||||
return _Property(fn, options=options)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class ServiceInterface:
|
||||
"""An abstract class that can be extended by the user to define DBus services.
|
||||
|
||||
Instances of :class:`ServiceInterface` can be exported on a path of the bus
|
||||
with the :class:`export <dbus_next.message_bus.BaseMessageBus.export>`
|
||||
method of a :class:`MessageBus <dbus_next.message_bus.BaseMessageBus>`.
|
||||
|
||||
Use the :func:`@method <dbus_next.service.method>`, :func:`@dbus_property
|
||||
<dbus_next.service.dbus_property>`, and :func:`@signal
|
||||
<dbus_next.service.signal>` decorators to mark class methods as DBus
|
||||
methods, properties, and signals respectively.
|
||||
|
||||
:ivar name: The name of this interface as it appears to clients. Must be a
|
||||
valid interface name.
|
||||
:vartype name: str
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
# TODO cannot be overridden by a dbus member
|
||||
self.name = name
|
||||
self.__methods = []
|
||||
self.__properties = []
|
||||
self.__signals = []
|
||||
self.__buses = set()
|
||||
|
||||
for name, member in inspect.getmembers(type(self)):
|
||||
member_dict = getattr(member, "__dict__", {})
|
||||
if type(member) is _Property:
|
||||
# XXX The getter and the setter may show up as different
|
||||
# members if they have different names. But if they have the
|
||||
# same name, they will be the same member. So we try to merge
|
||||
# them together here. I wish we could make this cleaner.
|
||||
found = False
|
||||
for prop in self.__properties:
|
||||
if prop.prop_getter is member.prop_getter:
|
||||
found = True
|
||||
if member.prop_setter is not None:
|
||||
prop.prop_setter = member.prop_setter
|
||||
|
||||
if not found:
|
||||
self.__properties.append(member)
|
||||
elif "__DBUS_METHOD" in member_dict:
|
||||
method = member_dict["__DBUS_METHOD"]
|
||||
assert type(method) is _Method
|
||||
self.__methods.append(method)
|
||||
elif "__DBUS_SIGNAL" in member_dict:
|
||||
signal = member_dict["__DBUS_SIGNAL"]
|
||||
assert type(signal) is _Signal
|
||||
self.__signals.append(signal)
|
||||
|
||||
# validate that writable properties have a setter
|
||||
for prop in self.__properties:
|
||||
if prop.access.writable() and prop.prop_setter is None:
|
||||
raise ValueError(
|
||||
f'property "{prop.name}" is writable but does not have a setter'
|
||||
)
|
||||
|
||||
def emit_properties_changed(
|
||||
self, changed_properties: Dict[str, Any], invalidated_properties: List[str] = []
|
||||
):
|
||||
"""Emit the ``org.freedesktop.DBus.Properties.PropertiesChanged`` signal.
|
||||
|
||||
This signal is intended to be used to alert clients when a property of
|
||||
the interface has changed.
|
||||
|
||||
:param changed_properties: The keys must be the names of properties exposed by this bus. The values must be valid for the signature of those properties.
|
||||
:type changed_properties: dict(str, Any)
|
||||
:param invalidated_properties: A list of names of properties that are now invalid (presumably for clients who cache the value).
|
||||
:type invalidated_properties: list(str)
|
||||
"""
|
||||
# TODO cannot be overridden by a dbus member
|
||||
variant_dict = {}
|
||||
|
||||
for prop in ServiceInterface._get_properties(self):
|
||||
if prop.name in changed_properties:
|
||||
variant_dict[prop.name] = Variant(
|
||||
prop.signature, changed_properties[prop.name]
|
||||
)
|
||||
|
||||
body = [self.name, variant_dict, invalidated_properties]
|
||||
for bus in ServiceInterface._get_buses(self):
|
||||
bus._interface_signal_notify(
|
||||
self,
|
||||
"org.freedesktop.DBus.Properties",
|
||||
"PropertiesChanged",
|
||||
"sa{sv}as",
|
||||
body,
|
||||
)
|
||||
|
||||
def introspect(self) -> intr.Interface:
|
||||
"""Get introspection information for this interface.
|
||||
|
||||
This might be useful for creating clients for the interface or examining the introspection output of an interface.
|
||||
|
||||
:returns: The introspection data for the interface.
|
||||
:rtype: :class:`dbus_next.introspection.Interface`
|
||||
"""
|
||||
# TODO cannot be overridden by a dbus member
|
||||
return intr.Interface(
|
||||
self.name,
|
||||
methods=[
|
||||
method.introspection
|
||||
for method in ServiceInterface._get_methods(self)
|
||||
if not method.disabled
|
||||
],
|
||||
signals=[
|
||||
signal.introspection
|
||||
for signal in ServiceInterface._get_signals(self)
|
||||
if not signal.disabled
|
||||
],
|
||||
properties=[
|
||||
prop.introspection
|
||||
for prop in ServiceInterface._get_properties(self)
|
||||
if not prop.disabled
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_properties(interface):
|
||||
return interface.__properties
|
||||
|
||||
@staticmethod
|
||||
def _get_methods(interface):
|
||||
return interface.__methods
|
||||
|
||||
@staticmethod
|
||||
def _get_signals(interface):
|
||||
return interface.__signals
|
||||
|
||||
@staticmethod
|
||||
def _get_buses(interface):
|
||||
return interface.__buses
|
||||
|
||||
@staticmethod
|
||||
def _add_bus(interface, bus):
|
||||
interface.__buses.add(bus)
|
||||
|
||||
@staticmethod
|
||||
def _remove_bus(interface, bus):
|
||||
interface.__buses.remove(bus)
|
||||
|
||||
@staticmethod
|
||||
def _msg_body_to_args(msg):
|
||||
if signature_contains_type(msg.signature_tree, msg.body, "h"):
|
||||
# XXX: This deep copy could be expensive if messages are very
|
||||
# large. We could optimize this by only copying what we change
|
||||
# here.
|
||||
return replace_idx_with_fds(
|
||||
msg.signature_tree, copy.deepcopy(msg.body), msg.unix_fds
|
||||
)
|
||||
else:
|
||||
return msg.body
|
||||
|
||||
@staticmethod
|
||||
def _fn_result_to_body(result, signature_tree):
|
||||
"""The high level interfaces may return single values which may be
|
||||
wrapped in a list to be a message body. Also they may return fds
|
||||
directly for type 'h' which need to be put into an external list."""
|
||||
out_len = len(signature_tree.types)
|
||||
if result is None:
|
||||
result = []
|
||||
else:
|
||||
if out_len == 1:
|
||||
result = [result]
|
||||
else:
|
||||
if type(result) is not list:
|
||||
raise SignatureBodyMismatchError(
|
||||
"Expected signal to return a list of arguments"
|
||||
)
|
||||
|
||||
if out_len != len(result):
|
||||
raise SignatureBodyMismatchError(
|
||||
f"Signature and function return mismatch, expected {len(signature_tree.types)} arguments but got {len(result)}"
|
||||
)
|
||||
|
||||
return replace_fds_with_idx(signature_tree, result)
|
||||
|
||||
@staticmethod
|
||||
def _handle_signal(interface, signal, result):
|
||||
body, fds = ServiceInterface._fn_result_to_body(result, signal.signature_tree)
|
||||
for bus in ServiceInterface._get_buses(interface):
|
||||
bus._interface_signal_notify(
|
||||
interface, interface.name, signal.name, signal.signature, body, fds
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_property_value(interface, prop, callback):
|
||||
# XXX MUST CHECK TYPE RETURNED BY GETTER
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(prop.prop_getter):
|
||||
task = asyncio.ensure_future(prop.prop_getter(interface))
|
||||
|
||||
def get_property_callback(task):
|
||||
try:
|
||||
result = task.result()
|
||||
except Exception as e:
|
||||
callback(interface, prop, None, e)
|
||||
return
|
||||
|
||||
callback(interface, prop, result, None)
|
||||
|
||||
task.add_done_callback(get_property_callback)
|
||||
return
|
||||
|
||||
callback(
|
||||
interface, prop, getattr(interface, prop.prop_getter.__name__), None
|
||||
)
|
||||
except Exception as e:
|
||||
callback(interface, prop, None, e)
|
||||
|
||||
@staticmethod
|
||||
def _set_property_value(interface, prop, value, callback):
|
||||
# XXX MUST CHECK TYPE TO SET
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(prop.prop_setter):
|
||||
task = asyncio.ensure_future(prop.prop_setter(interface, value))
|
||||
|
||||
def set_property_callback(task):
|
||||
try:
|
||||
task.result()
|
||||
except Exception as e:
|
||||
callback(interface, prop, e)
|
||||
return
|
||||
|
||||
callback(interface, prop, None)
|
||||
|
||||
task.add_done_callback(set_property_callback)
|
||||
return
|
||||
|
||||
setattr(interface, prop.prop_setter.__name__, value)
|
||||
callback(interface, prop, None)
|
||||
except Exception as e:
|
||||
callback(interface, prop, e)
|
||||
|
||||
@staticmethod
|
||||
def _get_all_property_values(interface, callback, user_data=None):
|
||||
result = {}
|
||||
result_error = None
|
||||
|
||||
for prop in ServiceInterface._get_properties(interface):
|
||||
if prop.disabled or not prop.access.readable():
|
||||
continue
|
||||
result[prop.name] = None
|
||||
|
||||
if not result:
|
||||
callback(interface, result, user_data, None)
|
||||
return
|
||||
|
||||
def get_property_callback(interface, prop, value, e):
|
||||
nonlocal result_error
|
||||
if e is not None:
|
||||
result_error = e
|
||||
del result[prop.name]
|
||||
else:
|
||||
try:
|
||||
result[prop.name] = Variant(prop.signature, value)
|
||||
except SignatureBodyMismatchError as e:
|
||||
result_error = e
|
||||
del result[prop.name]
|
||||
|
||||
if any(v is None for v in result.values()):
|
||||
return
|
||||
|
||||
callback(interface, result, user_data, result_error)
|
||||
|
||||
for prop in ServiceInterface._get_properties(interface):
|
||||
if prop.disabled or not prop.access.readable():
|
||||
continue
|
||||
ServiceInterface._get_property_value(interface, prop, get_property_callback)
|
||||
454
src/dbus_fast/signature.py
Normal file
454
src/dbus_fast/signature.py
Normal file
@@ -0,0 +1,454 @@
|
||||
from typing import Any, List, Union
|
||||
|
||||
from .errors import InvalidSignatureError, SignatureBodyMismatchError
|
||||
from .validators import is_object_path_valid
|
||||
|
||||
|
||||
class SignatureType:
|
||||
"""A class that represents a single complete type within a signature.
|
||||
|
||||
This class is not meant to be constructed directly. Use the :class:`SignatureTree`
|
||||
class to parse signatures.
|
||||
|
||||
:ivar ~.signature: The signature of this complete type.
|
||||
:vartype ~.signature: str
|
||||
|
||||
:ivar children: A list of child types if this is a container type. Arrays \
|
||||
have one child type, dict entries have two child types (key and value), and \
|
||||
structs have child types equal to the number of struct members.
|
||||
:vartype children: list(:class:`SignatureType`)
|
||||
"""
|
||||
|
||||
_tokens = "ybnqiuxtdsogavh({"
|
||||
|
||||
def __init__(self, token):
|
||||
self.token = token
|
||||
self.children = []
|
||||
self._signature = None
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is SignatureType:
|
||||
return self.signature == other.signature
|
||||
else:
|
||||
return super().__eq__(other)
|
||||
|
||||
def _collapse(self):
|
||||
if self.token not in "a({":
|
||||
return self.token
|
||||
|
||||
signature = [self.token]
|
||||
|
||||
for child in self.children:
|
||||
signature.append(child._collapse())
|
||||
|
||||
if self.token == "(":
|
||||
signature.append(")")
|
||||
elif self.token == "{":
|
||||
signature.append("}")
|
||||
|
||||
return "".join(signature)
|
||||
|
||||
@property
|
||||
def signature(self) -> str:
|
||||
if self._signature is not None:
|
||||
return self._signature
|
||||
self._signature = self._collapse()
|
||||
return self._signature
|
||||
|
||||
@staticmethod
|
||||
def _parse_next(signature):
|
||||
if not signature:
|
||||
return (None, "")
|
||||
|
||||
token = signature[0]
|
||||
|
||||
if token not in SignatureType._tokens:
|
||||
raise InvalidSignatureError(f'got unexpected token: "{token}"')
|
||||
|
||||
# container types
|
||||
if token == "a":
|
||||
self = SignatureType("a")
|
||||
(child, signature) = SignatureType._parse_next(signature[1:])
|
||||
if not child:
|
||||
raise InvalidSignatureError("missing type for array")
|
||||
self.children.append(child)
|
||||
return (self, signature)
|
||||
elif token == "(":
|
||||
self = SignatureType("(")
|
||||
signature = signature[1:]
|
||||
while True:
|
||||
(child, signature) = SignatureType._parse_next(signature)
|
||||
if not signature:
|
||||
raise InvalidSignatureError('missing closing ")" for struct')
|
||||
self.children.append(child)
|
||||
if signature[0] == ")":
|
||||
return (self, signature[1:])
|
||||
elif token == "{":
|
||||
self = SignatureType("{")
|
||||
signature = signature[1:]
|
||||
(key_child, signature) = SignatureType._parse_next(signature)
|
||||
if not key_child or len(key_child.children):
|
||||
raise InvalidSignatureError("expected a simple type for dict entry key")
|
||||
self.children.append(key_child)
|
||||
(value_child, signature) = SignatureType._parse_next(signature)
|
||||
if not value_child:
|
||||
raise InvalidSignatureError("expected a value for dict entry")
|
||||
if not signature or signature[0] != "}":
|
||||
raise InvalidSignatureError('missing closing "}" for dict entry')
|
||||
self.children.append(value_child)
|
||||
return (self, signature[1:])
|
||||
|
||||
# basic type
|
||||
return (SignatureType(token), signature[1:])
|
||||
|
||||
def _verify_byte(self, body):
|
||||
BYTE_MIN = 0x00
|
||||
BYTE_MAX = 0xFF
|
||||
if not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus BYTE type "y" must be Python type "int", got {type(body)}'
|
||||
)
|
||||
if body < BYTE_MIN or body > BYTE_MAX:
|
||||
raise SignatureBodyMismatchError(
|
||||
f"DBus BYTE type must be between {BYTE_MIN} and {BYTE_MAX}"
|
||||
)
|
||||
|
||||
def _verify_boolean(self, body):
|
||||
if not isinstance(body, bool):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus BOOLEAN type "b" must be Python type "bool", got {type(body)}'
|
||||
)
|
||||
|
||||
def _verify_int16(self, body):
|
||||
INT16_MIN = -0x7FFF - 1
|
||||
INT16_MAX = 0x7FFF
|
||||
if not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus INT16 type "n" must be Python type "int", got {type(body)}'
|
||||
)
|
||||
elif body > INT16_MAX or body < INT16_MIN:
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus INT16 type "n" must be between {INT16_MIN} and {INT16_MAX}'
|
||||
)
|
||||
|
||||
def _verify_uint16(self, body):
|
||||
UINT16_MIN = 0
|
||||
UINT16_MAX = 0xFFFF
|
||||
if not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus UINT16 type "q" must be Python type "int", got {type(body)}'
|
||||
)
|
||||
elif body > UINT16_MAX or body < UINT16_MIN:
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus UINT16 type "q" must be between {UINT16_MIN} and {UINT16_MAX}'
|
||||
)
|
||||
|
||||
def _verify_int32(self, body):
|
||||
INT32_MIN = -0x7FFFFFFF - 1
|
||||
INT32_MAX = 0x7FFFFFFF
|
||||
if not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus INT32 type "i" must be Python type "int", got {type(body)}'
|
||||
)
|
||||
elif body > INT32_MAX or body < INT32_MIN:
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus INT32 type "i" must be between {INT32_MIN} and {INT32_MAX}'
|
||||
)
|
||||
|
||||
def _verify_uint32(self, body):
|
||||
UINT32_MIN = 0
|
||||
UINT32_MAX = 0xFFFFFFFF
|
||||
if not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus UINT32 type "u" must be Python type "int", got {type(body)}'
|
||||
)
|
||||
elif body > UINT32_MAX or body < UINT32_MIN:
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus UINT32 type "u" must be between {UINT32_MIN} and {UINT32_MAX}'
|
||||
)
|
||||
|
||||
def _verify_int64(self, body):
|
||||
INT64_MAX = 9223372036854775807
|
||||
INT64_MIN = -INT64_MAX - 1
|
||||
if not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus INT64 type "x" must be Python type "int", got {type(body)}'
|
||||
)
|
||||
elif body > INT64_MAX or body < INT64_MIN:
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus INT64 type "x" must be between {INT64_MIN} and {INT64_MAX}'
|
||||
)
|
||||
|
||||
def _verify_uint64(self, body):
|
||||
UINT64_MIN = 0
|
||||
UINT64_MAX = 18446744073709551615
|
||||
if not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus UINT64 type "t" must be Python type "int", got {type(body)}'
|
||||
)
|
||||
elif body > UINT64_MAX or body < UINT64_MIN:
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus UINT64 type "t" must be between {UINT64_MIN} and {UINT64_MAX}'
|
||||
)
|
||||
|
||||
def _verify_double(self, body):
|
||||
if not isinstance(body, float) and not isinstance(body, int):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus DOUBLE type "d" must be Python type "float" or "int", got {type(body)}'
|
||||
)
|
||||
|
||||
def _verify_unix_fd(self, body):
|
||||
try:
|
||||
self._verify_uint32(body)
|
||||
except SignatureBodyMismatchError:
|
||||
raise SignatureBodyMismatchError(
|
||||
'DBus UNIX_FD type "h" must be a valid UINT32'
|
||||
)
|
||||
|
||||
def _verify_object_path(self, body):
|
||||
if not is_object_path_valid(body):
|
||||
raise SignatureBodyMismatchError(
|
||||
'DBus OBJECT_PATH type "o" must be a valid object path'
|
||||
)
|
||||
|
||||
def _verify_string(self, body):
|
||||
if not isinstance(body, str):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus STRING type "s" must be Python type "str", got {type(body)}'
|
||||
)
|
||||
|
||||
def _verify_signature(self, body):
|
||||
# I guess we could run it through the SignatureTree parser instead
|
||||
if not isinstance(body, str):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus SIGNATURE type "g" must be Python type "str", got {type(body)}'
|
||||
)
|
||||
if len(body.encode()) > 0xFF:
|
||||
raise SignatureBodyMismatchError(
|
||||
'DBus SIGNATURE type "g" must be less than 256 bytes'
|
||||
)
|
||||
|
||||
def _verify_array(self, body):
|
||||
child_type = self.children[0]
|
||||
|
||||
if child_type.token == "{":
|
||||
if not isinstance(body, dict):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus ARRAY type "a" with DICT_ENTRY child must be Python type "dict", got {type(body)}'
|
||||
)
|
||||
for key, value in body.items():
|
||||
child_type.children[0].verify(key)
|
||||
child_type.children[1].verify(value)
|
||||
elif child_type.token == "y":
|
||||
if not isinstance(body, bytes):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus ARRAY type "a" with BYTE child must be Python type "bytes", got {type(body)}'
|
||||
)
|
||||
# no need to verify children
|
||||
else:
|
||||
if not isinstance(body, list):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus ARRAY type "a" must be Python type "list", got {type(body)}'
|
||||
)
|
||||
for member in body:
|
||||
child_type.verify(member)
|
||||
|
||||
def _verify_struct(self, body):
|
||||
# TODO allow tuples
|
||||
if not isinstance(body, list):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus STRUCT type "(" must be Python type "list", got {type(body)}'
|
||||
)
|
||||
|
||||
if len(body) != len(self.children):
|
||||
raise SignatureBodyMismatchError(
|
||||
'DBus STRUCT type "(" must have Python list members equal to the number of struct type members'
|
||||
)
|
||||
|
||||
for i, member in enumerate(body):
|
||||
self.children[i].verify(member)
|
||||
|
||||
def _verify_variant(self, body):
|
||||
# a variant signature and value is valid by construction
|
||||
if not isinstance(body, Variant):
|
||||
raise SignatureBodyMismatchError(
|
||||
f'DBus VARIANT type "v" must be Python type "Variant", got {type(body)}'
|
||||
)
|
||||
|
||||
def verify(self, body: Any) -> bool:
|
||||
"""Verify that the body matches this type.
|
||||
|
||||
:returns: True if the body matches this type.
|
||||
:raises:
|
||||
:class:`SignatureBodyMismatchError` if the body does not match this type.
|
||||
"""
|
||||
if body is None:
|
||||
raise SignatureBodyMismatchError('Cannot serialize Python type "None"')
|
||||
elif self.token == "y":
|
||||
self._verify_byte(body)
|
||||
elif self.token == "b":
|
||||
self._verify_boolean(body)
|
||||
elif self.token == "n":
|
||||
self._verify_int16(body)
|
||||
elif self.token == "q":
|
||||
self._verify_uint16(body)
|
||||
elif self.token == "i":
|
||||
self._verify_int32(body)
|
||||
elif self.token == "u":
|
||||
self._verify_uint32(body)
|
||||
elif self.token == "x":
|
||||
self._verify_int64(body)
|
||||
elif self.token == "t":
|
||||
self._verify_uint64(body)
|
||||
elif self.token == "d":
|
||||
self._verify_double(body)
|
||||
elif self.token == "h":
|
||||
self._verify_unix_fd(body)
|
||||
elif self.token == "o":
|
||||
self._verify_object_path(body)
|
||||
elif self.token == "s":
|
||||
self._verify_string(body)
|
||||
elif self.token == "g":
|
||||
self._verify_signature(body)
|
||||
elif self.token == "a":
|
||||
self._verify_array(body)
|
||||
elif self.token == "(":
|
||||
self._verify_struct(body)
|
||||
elif self.token == "v":
|
||||
self._verify_variant(body)
|
||||
else:
|
||||
raise Exception(f"cannot verify type with token {self.token}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SignatureTree:
|
||||
"""A class that represents a signature as a tree structure for conveniently
|
||||
working with DBus signatures.
|
||||
|
||||
This class will not normally be used directly by the user.
|
||||
|
||||
:ivar types: A list of parsed complete types.
|
||||
:vartype types: list(:class:`SignatureType`)
|
||||
|
||||
:ivar ~.signature: The signature of this signature tree.
|
||||
:vartype ~.signature: str
|
||||
|
||||
:raises:
|
||||
:class:`InvalidSignatureError` if the given signature is not valid.
|
||||
"""
|
||||
|
||||
_cache = {}
|
||||
|
||||
@staticmethod
|
||||
def _get(signature: str = ""):
|
||||
if signature in SignatureTree._cache:
|
||||
return SignatureTree._cache[signature]
|
||||
SignatureTree._cache[signature] = SignatureTree(signature)
|
||||
return SignatureTree._cache[signature]
|
||||
|
||||
def __init__(self, signature: str = ""):
|
||||
self.signature = signature
|
||||
|
||||
self.types = []
|
||||
|
||||
if len(signature) > 0xFF:
|
||||
raise InvalidSignatureError("A signature must be less than 256 characters")
|
||||
|
||||
while signature:
|
||||
(type_, signature) = SignatureType._parse_next(signature)
|
||||
self.types.append(type_)
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is SignatureTree:
|
||||
return self.signature == other.signature
|
||||
else:
|
||||
return super().__eq__(other)
|
||||
|
||||
def verify(self, body: List[Any]):
|
||||
"""Verifies that the give body matches this signature tree
|
||||
|
||||
:param body: the body to verify for this tree
|
||||
:type body: list(Any)
|
||||
|
||||
:returns: True if the signature matches the body or an exception if not.
|
||||
|
||||
:raises:
|
||||
:class:`SignatureBodyMismatchError` if the signature does not match the body.
|
||||
"""
|
||||
if not isinstance(body, list):
|
||||
raise SignatureBodyMismatchError(
|
||||
f"The body must be a list (got {type(body)})"
|
||||
)
|
||||
if len(body) != len(self.types):
|
||||
raise SignatureBodyMismatchError(
|
||||
f"The body has the wrong number of types (got {len(body)}, expected {len(self.types)})"
|
||||
)
|
||||
for i, type_ in enumerate(self.types):
|
||||
type_.verify(body[i])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Variant:
|
||||
"""A class to represent a DBus variant (type "v").
|
||||
|
||||
This class is used in message bodies to represent variants. The user can
|
||||
expect a value in the body with type "v" to use this class and can
|
||||
construct this class directly for use in message bodies sent over the bus.
|
||||
|
||||
:ivar signature: The signature for this variant. Must be a single complete type.
|
||||
:vartype signature: str
|
||||
|
||||
:ivar signature_type: The parsed signature of this variant.
|
||||
:vartype signature_type: :class:`SignatureType`
|
||||
|
||||
:ivar value: The value of this variant. Must correspond to the signature.
|
||||
:vartype value: Any
|
||||
|
||||
:raises:
|
||||
:class:`InvalidSignatureError` if the signature is not valid.
|
||||
:class:`SignatureBodyMismatchError` if the signature does not match the body.
|
||||
"""
|
||||
|
||||
def __init__(self, signature: Union[str, SignatureTree, SignatureType], value: Any):
|
||||
signature_str = ""
|
||||
signature_tree = None
|
||||
signature_type = None
|
||||
|
||||
if type(signature) is SignatureTree:
|
||||
signature_tree = signature
|
||||
elif type(signature) is SignatureType:
|
||||
signature_type = signature
|
||||
signature_str = signature.signature
|
||||
elif type(signature) is str:
|
||||
signature_tree = SignatureTree._get(signature)
|
||||
else:
|
||||
raise TypeError(
|
||||
"signature must be a SignatureTree, SignatureType, or a string"
|
||||
)
|
||||
|
||||
if signature_tree:
|
||||
if len(signature_tree.types) != 1:
|
||||
raise ValueError(
|
||||
"variants must have a signature for a single complete type"
|
||||
)
|
||||
signature_str = signature_tree.signature
|
||||
signature_type = signature_tree.types[0]
|
||||
|
||||
signature_type.verify(value)
|
||||
|
||||
self.type = signature_type
|
||||
self.signature = signature_str
|
||||
self.value = value
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) is Variant:
|
||||
return self.signature == other.signature and self.value == other.value
|
||||
else:
|
||||
return super().__eq__(other)
|
||||
|
||||
def __repr__(self):
|
||||
return "<dbus_next.signature.Variant ('{}', {})>".format(
|
||||
self.type.signature, self.value
|
||||
)
|
||||
190
src/dbus_fast/validators.py
Normal file
190
src/dbus_fast/validators.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import re
|
||||
|
||||
from .errors import (
|
||||
InvalidBusNameError,
|
||||
InvalidInterfaceNameError,
|
||||
InvalidMemberNameError,
|
||||
InvalidObjectPathError,
|
||||
)
|
||||
|
||||
_bus_name_re = re.compile(r"^[A-Za-z_-][A-Za-z0-9_-]*$")
|
||||
_path_re = re.compile(r"^[A-Za-z0-9_]+$")
|
||||
_element_re = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
_member_re = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
|
||||
|
||||
|
||||
def is_bus_name_valid(name: str) -> bool:
|
||||
"""Whether this is a valid bus name.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus
|
||||
|
||||
:param name: The bus name to validate.
|
||||
:type name: str
|
||||
|
||||
:returns: Whether the name is a valid bus name.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
return False
|
||||
|
||||
if not name or len(name) > 255:
|
||||
return False
|
||||
|
||||
if name.startswith(":"):
|
||||
# a unique bus name
|
||||
return True
|
||||
|
||||
if name.startswith("."):
|
||||
return False
|
||||
|
||||
if name.find(".") == -1:
|
||||
return False
|
||||
|
||||
for element in name.split("."):
|
||||
if _bus_name_re.search(element) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_object_path_valid(path: str) -> bool:
|
||||
"""Whether this is a valid object path.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
|
||||
|
||||
:param path: The object path to validate.
|
||||
:type path: str
|
||||
|
||||
:returns: Whether the object path is valid.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not isinstance(path, str):
|
||||
return False
|
||||
|
||||
if not path:
|
||||
return False
|
||||
|
||||
if not path.startswith("/"):
|
||||
return False
|
||||
|
||||
if len(path) == 1:
|
||||
return True
|
||||
|
||||
for element in path[1:].split("/"):
|
||||
if _path_re.search(element) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_interface_name_valid(name: str) -> bool:
|
||||
"""Whether this is a valid interface name.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface
|
||||
|
||||
:param name: The interface name to validate.
|
||||
:type name: str
|
||||
|
||||
:returns: Whether the name is a valid interface name.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not isinstance(name, str):
|
||||
return False
|
||||
|
||||
if not name or len(name) > 255:
|
||||
return False
|
||||
|
||||
if name.startswith("."):
|
||||
return False
|
||||
|
||||
if name.find(".") == -1:
|
||||
return False
|
||||
|
||||
for element in name.split("."):
|
||||
if _element_re.search(element) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_member_name_valid(member: str) -> bool:
|
||||
"""Whether this is a valid member name.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-member
|
||||
|
||||
:param member: The member name to validate.
|
||||
:type member: str
|
||||
|
||||
:returns: Whether the name is a valid member name.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not isinstance(member, str):
|
||||
return False
|
||||
|
||||
if not member or len(member) > 255:
|
||||
return False
|
||||
|
||||
if _member_re.search(member) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def assert_bus_name_valid(name: str):
|
||||
"""Raise an error if this is not a valid bus name.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus
|
||||
|
||||
:param name: The bus name to validate.
|
||||
:type name: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidBusNameError` - If this is not a valid bus name.
|
||||
"""
|
||||
if not is_bus_name_valid(name):
|
||||
raise InvalidBusNameError(name)
|
||||
|
||||
|
||||
def assert_object_path_valid(path: str):
|
||||
"""Raise an error if this is not a valid object path.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
|
||||
|
||||
:param path: The object path to validate.
|
||||
:type path: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidObjectPathError` - If this is not a valid object path.
|
||||
"""
|
||||
if not is_object_path_valid(path):
|
||||
raise InvalidObjectPathError(path)
|
||||
|
||||
|
||||
def assert_interface_name_valid(name: str):
|
||||
"""Raise an error if this is not a valid interface name.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface
|
||||
|
||||
:param name: The interface name to validate.
|
||||
:type name: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidInterfaceNameError` - If this is not a valid object path.
|
||||
"""
|
||||
if not is_interface_name_valid(name):
|
||||
raise InvalidInterfaceNameError(name)
|
||||
|
||||
|
||||
def assert_member_name_valid(member):
|
||||
"""Raise an error if this is not a valid member name.
|
||||
|
||||
.. seealso:: https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-member
|
||||
|
||||
:param member: The member name to validate.
|
||||
:type member: str
|
||||
|
||||
:raises:
|
||||
- :class:`InvalidMemberNameError` - If this is not a valid object path.
|
||||
"""
|
||||
if not is_member_name_valid(member):
|
||||
raise InvalidMemberNameError(member)
|
||||
Reference in New Issue
Block a user