mirror of
https://github.com/EvolutionAPI/evolution-client-python.git
synced 2026-02-04 05:46:23 -06:00
initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""Low-level, pure Python DBus protocol wrapper.
|
||||
"""
|
||||
from .auth import AuthenticationError, FDNegotiationError
|
||||
from .low_level import (
|
||||
Endianness, Header, HeaderFields, Message, MessageFlag, MessageType,
|
||||
Parser, SizeLimitError,
|
||||
)
|
||||
from .bus import find_session_bus, find_system_bus
|
||||
from .bus_messages import *
|
||||
from .fds import FileDescriptor, NoFDError
|
||||
from .wrappers import *
|
||||
|
||||
__version__ = '0.8.0'
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
+136
@@ -0,0 +1,136 @@
|
||||
from binascii import hexlify
|
||||
from enum import Enum
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
def make_auth_external() -> bytes:
|
||||
"""Prepare an AUTH command line with the current effective user ID.
|
||||
|
||||
This is the preferred authentication method for typical D-Bus connections
|
||||
over a Unix domain socket.
|
||||
"""
|
||||
hex_uid = hexlify(str(os.geteuid()).encode('ascii'))
|
||||
return b'AUTH EXTERNAL %b\r\n' % hex_uid
|
||||
|
||||
def make_auth_anonymous() -> bytes:
|
||||
"""Format an AUTH command line for the ANONYMOUS mechanism
|
||||
|
||||
Jeepney's higher-level wrappers don't currently use this mechanism,
|
||||
but third-party code may choose to.
|
||||
|
||||
See <https://tools.ietf.org/html/rfc4505> for details.
|
||||
"""
|
||||
from . import __version__
|
||||
trace = hexlify(('Jeepney %s' % __version__).encode('ascii'))
|
||||
return b'AUTH ANONYMOUS %s\r\n' % trace
|
||||
|
||||
BEGIN = b'BEGIN\r\n'
|
||||
NEGOTIATE_UNIX_FD = b'NEGOTIATE_UNIX_FD\r\n'
|
||||
|
||||
class ClientState(Enum):
|
||||
# States from the D-Bus spec (plus 'Success'). Not all used in Jeepney.
|
||||
WaitingForData = 1
|
||||
WaitingForOk = 2
|
||||
WaitingForReject = 3
|
||||
WaitingForAgreeUnixFD = 4
|
||||
Success = 5
|
||||
|
||||
class AuthenticationError(ValueError):
|
||||
"""Raised when DBus authentication fails"""
|
||||
def __init__(self, data, msg="Authentication failed"):
|
||||
self.msg = msg
|
||||
self.data = data
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.msg}. Bus sent: {self.data!r}"
|
||||
|
||||
class FDNegotiationError(AuthenticationError):
|
||||
"""Raised when file descriptor support is requested but not available"""
|
||||
def __init__(self, data):
|
||||
super().__init__(data, msg="File descriptor support not available")
|
||||
|
||||
|
||||
class Authenticator:
|
||||
"""Process data for the SASL authentication conversation
|
||||
|
||||
If enable_fds is True, this includes negotiating support for passing
|
||||
file descriptors.
|
||||
"""
|
||||
def __init__(self, enable_fds=False):
|
||||
self.enable_fds = enable_fds
|
||||
self.buffer = bytearray()
|
||||
self._to_send = b'\0' + make_auth_external()
|
||||
self.state = ClientState.WaitingForOk
|
||||
self.error = None
|
||||
|
||||
@property
|
||||
def authenticated(self):
|
||||
return self.state is ClientState.Success
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.data_to_send, None)
|
||||
|
||||
def data_to_send(self) -> Optional[bytes]:
|
||||
"""Get a line of data to send to the server
|
||||
|
||||
The data returned should be sent before waiting to receive data.
|
||||
Returns empty bytes if waiting for more data from the server, and None
|
||||
if authentication is finished (success or error).
|
||||
|
||||
Iterating over the Authenticator object will also yield these lines;
|
||||
:meth:`feed` should be called with received data inside the loop.
|
||||
"""
|
||||
if self.authenticated or self.error:
|
||||
return None
|
||||
self._to_send, to_send = b'', self._to_send
|
||||
return to_send
|
||||
|
||||
def process_line(self, line):
|
||||
if self.state is ClientState.WaitingForOk:
|
||||
if line.startswith(b'OK '):
|
||||
if self.enable_fds:
|
||||
return NEGOTIATE_UNIX_FD, ClientState.WaitingForAgreeUnixFD
|
||||
else:
|
||||
return BEGIN, ClientState.Success
|
||||
# We only support EXTERNAL authentication, but if we allow others,
|
||||
# 'REJECTED <mechs>' would tell us to try another one.
|
||||
|
||||
elif self.state is ClientState.WaitingForAgreeUnixFD:
|
||||
if line.startswith(b'AGREE_UNIX_FD'):
|
||||
return BEGIN, ClientState.Success
|
||||
# The protocol allows us to continue if FD passing is rejected,
|
||||
# but Jeepney assumes that if you enable FD support you need it,
|
||||
# so we fail rather
|
||||
self.error = line
|
||||
raise FDNegotiationError(line)
|
||||
|
||||
self.error = line
|
||||
raise AuthenticationError(line)
|
||||
|
||||
def feed(self, data: bytes):
|
||||
"""Process received data
|
||||
|
||||
Raises AuthenticationError if the incoming data is not as expected for
|
||||
successful authentication. The connection should then be abandoned.
|
||||
"""
|
||||
self.buffer += data
|
||||
if b'\r\n' in self.buffer:
|
||||
line, self.buffer = self.buffer.split(b'\r\n', 1)
|
||||
if self.buffer:
|
||||
# We only expect one line before we reply
|
||||
raise AuthenticationError(self.buffer, "Unexpected data received")
|
||||
|
||||
self._to_send, self.state = self.process_line(line)
|
||||
|
||||
# Avoid consuming lots of memory if the server is not sending what we
|
||||
# expect. There doesn't appear to be a specified maximum line length,
|
||||
# but 8192 bytes leaves a sizeable margin over all the examples in the
|
||||
# spec (all < 100 bytes per line).
|
||||
elif len(self.buffer) > 8192:
|
||||
raise AuthenticationError(
|
||||
self.buffer, "Too much data received without line ending"
|
||||
)
|
||||
|
||||
|
||||
# Old name (behaviour on errors has changed, but should work for standard case)
|
||||
SASLParser = Authenticator
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
"""Generate a wrapper class from DBus introspection data"""
|
||||
import argparse
|
||||
from textwrap import indent
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from jeepney.wrappers import Introspectable
|
||||
from jeepney.io.blocking import open_dbus_connection, Proxy
|
||||
from jeepney import __version__
|
||||
|
||||
class Method:
|
||||
def __init__(self, xml_node):
|
||||
self.name = xml_node.attrib['name']
|
||||
self.in_args = []
|
||||
self.signature = []
|
||||
for arg in xml_node.findall("arg[@direction='in']"):
|
||||
try:
|
||||
name = arg.attrib['name']
|
||||
except KeyError:
|
||||
name = 'arg{}'.format(len(self.in_args))
|
||||
self.in_args.append(name)
|
||||
self.signature.append(arg.attrib['type'])
|
||||
|
||||
def _make_code_noargs(self):
|
||||
return ("def {name}(self):\n"
|
||||
" return new_method_call(self, '{name}')\n").format(
|
||||
name=self.name)
|
||||
|
||||
def make_code(self):
|
||||
if not self.in_args:
|
||||
return self._make_code_noargs()
|
||||
|
||||
args = ', '.join(self.in_args)
|
||||
signature = ''.join(self.signature)
|
||||
tuple = ('({},)' if len(self.in_args) == 1 else '({})').format(args)
|
||||
return ("def {name}(self, {args}):\n"
|
||||
" return new_method_call(self, '{name}', '{signature}',\n"
|
||||
" {tuple})\n").format(
|
||||
name=self.name, args=args, signature=signature, tuple=tuple
|
||||
)
|
||||
|
||||
INTERFACE_CLASS_TEMPLATE = """
|
||||
class {cls_name}(MessageGenerator):
|
||||
interface = {interface!r}
|
||||
|
||||
def __init__(self, object_path={path!r},
|
||||
bus_name={bus_name!r}):
|
||||
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||
"""
|
||||
|
||||
class Interface:
|
||||
def __init__(self, xml_node, path, bus_name):
|
||||
self.name = xml_node.attrib['name']
|
||||
self.path = path
|
||||
self.bus_name = bus_name
|
||||
self.methods = [Method(node) for node in xml_node.findall('method')]
|
||||
|
||||
def make_code(self):
|
||||
cls_name = self.name.split('.')[-1]
|
||||
chunks = [INTERFACE_CLASS_TEMPLATE.format(cls_name=cls_name,
|
||||
interface=self.name, path=self.path, bus_name=self.bus_name)]
|
||||
for method in self.methods:
|
||||
chunks.append(indent(method.make_code(), ' ' * 4))
|
||||
return '\n'.join(chunks)
|
||||
|
||||
MODULE_TEMPLATE = '''\
|
||||
"""Auto-generated DBus bindings
|
||||
|
||||
Generated by jeepney version {version}
|
||||
|
||||
Object path: {path}
|
||||
Bus name : {bus_name}
|
||||
"""
|
||||
|
||||
from jeepney.wrappers import MessageGenerator, new_method_call
|
||||
|
||||
'''
|
||||
|
||||
# Jeepney already includes bindings for these common interfaces
|
||||
IGNORE_INTERFACES = {
|
||||
'org.freedesktop.DBus.Introspectable',
|
||||
'org.freedesktop.DBus.Properties',
|
||||
'org.freedesktop.DBus.Peer',
|
||||
}
|
||||
|
||||
def code_from_xml(xml, path, bus_name, fh):
|
||||
if isinstance(fh, (bytes, str)):
|
||||
with open(fh, 'w') as f:
|
||||
return code_from_xml(xml, path, bus_name, f)
|
||||
|
||||
root = ET.fromstring(xml)
|
||||
fh.write(MODULE_TEMPLATE.format(version=__version__, path=path,
|
||||
bus_name=bus_name))
|
||||
|
||||
i = 0
|
||||
for interface_node in root.findall('interface'):
|
||||
if interface_node.attrib['name'] in IGNORE_INTERFACES:
|
||||
continue
|
||||
fh.write(Interface(interface_node, path, bus_name).make_code())
|
||||
i += 1
|
||||
|
||||
return i
|
||||
|
||||
def generate(path, name, output_file, bus='SESSION'):
|
||||
conn = open_dbus_connection(bus)
|
||||
introspectable = Proxy(Introspectable(path, name), conn)
|
||||
xml, = introspectable.Introspect()
|
||||
# print(xml)
|
||||
|
||||
n_interfaces = code_from_xml(xml, path, name, output_file)
|
||||
print("Written {} interface wrappers to {}".format(n_interfaces, output_file))
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('-n', '--name', required=True)
|
||||
ap.add_argument('-p', '--path', required=True)
|
||||
ap.add_argument('--bus', default='SESSION')
|
||||
ap.add_argument('-o', '--output')
|
||||
args = ap.parse_args()
|
||||
|
||||
output = args.output or (args.path[1:].replace('/', '_') + '.py')
|
||||
|
||||
generate(args.path, args.name, output, args.bus)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
_escape_pat = re.compile(r'%([0-9A-Fa-f]{2})')
|
||||
def unescape(v):
|
||||
def repl(match):
|
||||
n = int(match.group(1), base=16)
|
||||
return chr(n)
|
||||
return _escape_pat.sub(repl, v)
|
||||
|
||||
def parse_addresses(s):
|
||||
for addr in s.split(';'):
|
||||
transport, info = addr.split(':', 1)
|
||||
kv = {}
|
||||
for x in info.split(','):
|
||||
k, v = x.split('=', 1)
|
||||
kv[k] = unescape(v)
|
||||
yield (transport, kv)
|
||||
|
||||
SUPPORTED_TRANSPORTS = ('unix',)
|
||||
|
||||
def get_connectable_addresses(addr):
|
||||
unsupported_transports = set()
|
||||
found = False
|
||||
for transport, kv in parse_addresses(addr):
|
||||
if transport not in SUPPORTED_TRANSPORTS:
|
||||
unsupported_transports.add(transport)
|
||||
|
||||
elif transport == 'unix':
|
||||
if 'abstract' in kv:
|
||||
yield '\0' + kv['abstract']
|
||||
found = True
|
||||
elif 'path' in kv:
|
||||
yield kv['path']
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
raise RuntimeError("DBus transports ({}) not supported. Supported: {}"
|
||||
.format(unsupported_transports, SUPPORTED_TRANSPORTS))
|
||||
|
||||
def find_session_bus():
|
||||
addr = os.environ['DBUS_SESSION_BUS_ADDRESS']
|
||||
return next(get_connectable_addresses(addr))
|
||||
# TODO: fallbacks to X, filesystem
|
||||
|
||||
def find_system_bus():
|
||||
addr = os.environ.get('DBUS_SYSTEM_BUS_ADDRESS', '') \
|
||||
or 'unix:path=/var/run/dbus/system_bus_socket'
|
||||
return next(get_connectable_addresses(addr))
|
||||
|
||||
def get_bus(addr):
|
||||
if addr == 'SESSION':
|
||||
return find_session_bus()
|
||||
elif addr == 'SYSTEM':
|
||||
return find_system_bus()
|
||||
else:
|
||||
return next(get_connectable_addresses(addr))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('System bus at:', find_system_bus())
|
||||
print('Session bus at:', find_session_bus())
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Messages for talking to the DBus daemon itself
|
||||
|
||||
Generated by jeepney.bindgen and modified by hand.
|
||||
"""
|
||||
from .low_level import Message, MessageType, HeaderFields
|
||||
from .wrappers import MessageGenerator, new_method_call
|
||||
|
||||
__all__ = [
|
||||
'DBusNameFlags',
|
||||
'DBus',
|
||||
'message_bus',
|
||||
'Monitoring',
|
||||
'Stats',
|
||||
'MatchRule',
|
||||
]
|
||||
|
||||
class DBusNameFlags:
|
||||
allow_replacement = 1
|
||||
replace_existing = 2
|
||||
do_not_queue = 4
|
||||
|
||||
class DBus(MessageGenerator):
|
||||
"""Messages to talk to the message bus
|
||||
"""
|
||||
interface = 'org.freedesktop.DBus'
|
||||
|
||||
def __init__(self, object_path='/org/freedesktop/DBus',
|
||||
bus_name='org.freedesktop.DBus'):
|
||||
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||
|
||||
def Hello(self):
|
||||
return new_method_call(self, 'Hello')
|
||||
|
||||
def RequestName(self, name, flags=0):
|
||||
return new_method_call(self, 'RequestName', 'su', (name, flags))
|
||||
|
||||
def ReleaseName(self, name):
|
||||
return new_method_call(self, 'ReleaseName', 's', (name,))
|
||||
|
||||
def StartServiceByName(self, name):
|
||||
return new_method_call(self, 'StartServiceByName', 'su',
|
||||
(name, 0))
|
||||
|
||||
def UpdateActivationEnvironment(self, env):
|
||||
return new_method_call(self, 'UpdateActivationEnvironment', 'a{ss}',
|
||||
(env,))
|
||||
|
||||
def NameHasOwner(self, name):
|
||||
return new_method_call(self, 'NameHasOwner', 's', (name,))
|
||||
|
||||
def ListNames(self):
|
||||
return new_method_call(self, 'ListNames')
|
||||
|
||||
def ListActivatableNames(self):
|
||||
return new_method_call(self, 'ListActivatableNames')
|
||||
|
||||
def AddMatch(self, rule):
|
||||
"""*rule* can be a str or a :class:`MatchRule` instance"""
|
||||
if isinstance(rule, MatchRule):
|
||||
rule = rule.serialise()
|
||||
return new_method_call(self, 'AddMatch', 's', (rule,))
|
||||
|
||||
def RemoveMatch(self, rule):
|
||||
if isinstance(rule, MatchRule):
|
||||
rule = rule.serialise()
|
||||
return new_method_call(self, 'RemoveMatch', 's', (rule,))
|
||||
|
||||
def GetNameOwner(self, name):
|
||||
return new_method_call(self, 'GetNameOwner', 's', (name,))
|
||||
|
||||
def ListQueuedOwners(self, name):
|
||||
return new_method_call(self, 'ListQueuedOwners', 's', (name,))
|
||||
|
||||
def GetConnectionUnixUser(self, name):
|
||||
return new_method_call(self, 'GetConnectionUnixUser', 's', (name,))
|
||||
|
||||
def GetConnectionUnixProcessID(self, name):
|
||||
return new_method_call(self, 'GetConnectionUnixProcessID', 's', (name,))
|
||||
|
||||
def GetAdtAuditSessionData(self, name):
|
||||
return new_method_call(self, 'GetAdtAuditSessionData', 's', (name,))
|
||||
|
||||
def GetConnectionSELinuxSecurityContext(self, name):
|
||||
return new_method_call(self, 'GetConnectionSELinuxSecurityContext', 's',
|
||||
(name,))
|
||||
|
||||
def ReloadConfig(self):
|
||||
return new_method_call(self, 'ReloadConfig')
|
||||
|
||||
def GetId(self):
|
||||
return new_method_call(self, 'GetId')
|
||||
|
||||
def GetConnectionCredentials(self, name):
|
||||
return new_method_call(self, 'GetConnectionCredentials', 's', (name,))
|
||||
|
||||
message_bus = DBus()
|
||||
|
||||
class Monitoring(MessageGenerator):
|
||||
interface = 'org.freedesktop.DBus.Monitoring'
|
||||
|
||||
def __init__(self, object_path='/org/freedesktop/DBus',
|
||||
bus_name='org.freedesktop.DBus'):
|
||||
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||
|
||||
def BecomeMonitor(self, rules):
|
||||
"""Convert this connection to a monitor connection (advanced)"""
|
||||
return new_method_call(self, 'BecomeMonitor', 'asu', (rules, 0))
|
||||
|
||||
class Stats(MessageGenerator):
|
||||
interface = 'org.freedesktop.DBus.Debug.Stats'
|
||||
|
||||
def __init__(self, object_path='/org/freedesktop/DBus',
|
||||
bus_name='org.freedesktop.DBus'):
|
||||
super().__init__(object_path=object_path, bus_name=bus_name)
|
||||
|
||||
def GetStats(self):
|
||||
return new_method_call(self, 'GetStats')
|
||||
|
||||
def GetConnectionStats(self, arg0):
|
||||
return new_method_call(self, 'GetConnectionStats', 's',
|
||||
(arg0,))
|
||||
|
||||
def GetAllMatchRules(self):
|
||||
return new_method_call(self, 'GetAllMatchRules')
|
||||
|
||||
|
||||
class MatchRule:
|
||||
"""Construct a match rule to subscribe to DBus messages.
|
||||
|
||||
e.g.::
|
||||
|
||||
mr = MatchRule(
|
||||
interface='org.freedesktop.DBus',
|
||||
member='NameOwnerChanged',
|
||||
type='signal'
|
||||
)
|
||||
msg = message_bus.AddMatch(mr)
|
||||
# Send this message to subscribe to the signal
|
||||
"""
|
||||
def __init__(self, *, type=None, sender=None, interface=None, member=None,
|
||||
path=None, path_namespace=None, destination=None,
|
||||
eavesdrop=False):
|
||||
if isinstance(type, str):
|
||||
type = MessageType[type]
|
||||
self.message_type = type
|
||||
fields = {
|
||||
'sender': sender,
|
||||
'interface': interface,
|
||||
'member': member,
|
||||
'path': path,
|
||||
'destination': destination,
|
||||
}
|
||||
self.header_fields = {
|
||||
k: v for (k, v) in fields.items() if (v is not None)
|
||||
}
|
||||
self.path_namespace = path_namespace
|
||||
self.eavesdrop = eavesdrop
|
||||
self.arg_conditions = {}
|
||||
|
||||
def add_arg_condition(self, argno: int, value: str, kind='string'):
|
||||
"""Add a condition for a particular argument
|
||||
|
||||
argno: int, 0-63
|
||||
kind: 'string', 'path', 'namespace'
|
||||
"""
|
||||
if kind not in {'string', 'path', 'namespace'}:
|
||||
raise ValueError("kind={!r}".format(kind))
|
||||
if kind == 'namespace' and argno != 0:
|
||||
raise ValueError("argno must be 0 for kind='namespace'")
|
||||
self.arg_conditions[argno] = (value, kind)
|
||||
|
||||
def serialise(self) -> str:
|
||||
"""Convert to a string to use in an AddMatch call to the message bus"""
|
||||
pairs = list(self.header_fields.items())
|
||||
|
||||
if self.message_type:
|
||||
pairs.append(('type', self.message_type.name))
|
||||
|
||||
if self.eavesdrop:
|
||||
pairs.append(('eavesdrop', 'true'))
|
||||
|
||||
for argno, (val, kind) in self.arg_conditions.items():
|
||||
if kind == 'string':
|
||||
kind = ''
|
||||
pairs.append((f'arg{argno}{kind}', val))
|
||||
|
||||
# Quoting rules: single quotes ('') needed if the value contains a comma.
|
||||
# A literal ' can only be represented outside single quotes, by
|
||||
# backslash-escaping it. No escaping inside the quotes.
|
||||
# The simplest way to handle this is to use '' around every value, and
|
||||
# use '\'' (end quote, escaped ', restart quote) for literal ' .
|
||||
return ','.join(
|
||||
"{}='{}'".format(k, v.replace("'", r"'\''")) for (k, v) in pairs
|
||||
)
|
||||
|
||||
def matches(self, msg: Message) -> bool:
|
||||
"""Returns True if msg matches this rule"""
|
||||
h = msg.header
|
||||
if (self.message_type is not None) and h.message_type != self.message_type:
|
||||
return False
|
||||
|
||||
for field, expected in self.header_fields.items():
|
||||
if h.fields.get(HeaderFields[field], None) != expected:
|
||||
return False
|
||||
|
||||
if self.path_namespace is not None:
|
||||
path = h.fields.get(HeaderFields.path, '\0')
|
||||
path_ns = self.path_namespace.rstrip('/')
|
||||
if not ((path == path_ns) or path.startswith(path_ns + '/')):
|
||||
return False
|
||||
|
||||
for argno, (expected, kind) in self.arg_conditions.items():
|
||||
if argno >= len(msg.body):
|
||||
return False
|
||||
arg = msg.body[argno]
|
||||
if not isinstance(arg, str):
|
||||
return False
|
||||
if kind == 'string':
|
||||
if arg != expected:
|
||||
return False
|
||||
elif kind == 'path':
|
||||
if not (
|
||||
(arg == expected)
|
||||
or (expected.endswith('/') and arg.startswith(expected))
|
||||
or (arg.endswith('/') and expected.startswith(arg))
|
||||
):
|
||||
return False
|
||||
elif kind == 'namespace':
|
||||
if not (
|
||||
(arg == expected)
|
||||
or arg.startswith(expected + '.')
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import array
|
||||
import os
|
||||
import socket
|
||||
from warnings import warn
|
||||
|
||||
|
||||
class NoFDError(RuntimeError):
|
||||
"""Raised by :class:`FileDescriptor` methods if it was already closed/converted
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FileDescriptor:
|
||||
"""A file descriptor received in a D-Bus message
|
||||
|
||||
This wrapper helps ensure that the file descriptor is closed exactly once.
|
||||
If you don't explicitly convert or close the FileDescriptor object, it will
|
||||
close its file descriptor when it goes out of scope, and emit a
|
||||
ResourceWarning.
|
||||
"""
|
||||
__slots__ = ('_fd',)
|
||||
_CLOSED = -1
|
||||
_CONVERTED = -2
|
||||
|
||||
def __init__(self, fd):
|
||||
self._fd = fd
|
||||
|
||||
def __repr__(self):
|
||||
detail = self._fd
|
||||
if self._fd == self._CLOSED:
|
||||
detail = 'closed'
|
||||
elif self._fd == self._CONVERTED:
|
||||
detail = 'converted'
|
||||
return f"<FileDescriptor ({detail})>"
|
||||
|
||||
def close(self):
|
||||
"""Close the file descriptor
|
||||
|
||||
This can safely be called multiple times, but will raise RuntimeError
|
||||
if called after converting it with one of the ``to_*`` methods.
|
||||
|
||||
This object can also be used in a ``with`` block, to close it on
|
||||
leaving the block.
|
||||
"""
|
||||
if self._fd == self._CLOSED:
|
||||
pass
|
||||
elif self._fd == self._CONVERTED:
|
||||
raise NoFDError("Can't close FileDescriptor after converting it")
|
||||
else:
|
||||
self._fd, fd = self._CLOSED, self._fd
|
||||
os.close(fd)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def __del__(self):
|
||||
if self._fd >= 0:
|
||||
warn(
|
||||
f'FileDescriptor ({self._fd}) was neither closed nor converted',
|
||||
ResourceWarning, stacklevel=2, source=self
|
||||
)
|
||||
self.close()
|
||||
|
||||
def _check(self):
|
||||
if self._fd < 0:
|
||||
detail = 'closed' if self._fd == self._CLOSED else 'converted'
|
||||
raise NoFDError(f'FileDescriptor object was already {detail}')
|
||||
|
||||
def fileno(self):
|
||||
"""Get the integer file descriptor
|
||||
|
||||
This does not change the state of the :class:`FileDescriptor` object,
|
||||
unlike the ``to_*`` methods.
|
||||
"""
|
||||
self._check()
|
||||
return self._fd
|
||||
|
||||
def to_raw_fd(self):
|
||||
"""Convert to the low-level integer file descriptor::
|
||||
|
||||
raw_fd = fd.to_raw_fd()
|
||||
os.write(raw_fd, b'xyz')
|
||||
os.close(raw_fd)
|
||||
|
||||
The :class:`FileDescriptor` can't be used after calling this. The caller
|
||||
is responsible for closing the file descriptor.
|
||||
"""
|
||||
self._check()
|
||||
self._fd, fd = self._CONVERTED, self._fd
|
||||
return fd
|
||||
|
||||
def to_file(self, mode, buffering=-1, encoding=None, errors=None, newline=None):
|
||||
"""Convert to a Python file object::
|
||||
|
||||
with fd.to_file('w') as f:
|
||||
f.write('xyz')
|
||||
|
||||
The arguments are the same as for the builtin :func:`open` function.
|
||||
|
||||
The :class:`FileDescriptor` can't be used after calling this. Closing
|
||||
the file object will also close the file descriptor.
|
||||
"""
|
||||
self._check()
|
||||
f = open(
|
||||
self._fd, mode, buffering=buffering,
|
||||
encoding=encoding, errors=errors, newline=newline
|
||||
)
|
||||
self._fd = self._CONVERTED
|
||||
return f
|
||||
|
||||
def to_socket(self):
|
||||
"""Convert to a socket object
|
||||
|
||||
This returns a standard library :func:`socket.socket` object::
|
||||
|
||||
with fd.to_socket() as sock:
|
||||
b = sock.sendall(b'xyz')
|
||||
|
||||
The wrapper object can't be used after calling this. Closing the socket
|
||||
object will also close the file descriptor.
|
||||
"""
|
||||
from socket import socket
|
||||
|
||||
self._check()
|
||||
s = socket(fileno=self._fd)
|
||||
self._fd = self._CONVERTED
|
||||
return s
|
||||
|
||||
@classmethod
|
||||
def from_ancdata(cls, ancdata) -> ['FileDescriptor']:
|
||||
"""Make a list of FileDescriptor from received file descriptors
|
||||
|
||||
ancdata is a list of ancillary data tuples as returned by socket.recvmsg()
|
||||
"""
|
||||
fds = array.array("i") # Array of ints
|
||||
for cmsg_level, cmsg_type, data in ancdata:
|
||||
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
|
||||
# Append data, ignoring any truncated integers at the end.
|
||||
fds.frombytes(data[:len(data) - (len(data) % fds.itemsize)])
|
||||
return [cls(i) for i in fds]
|
||||
|
||||
|
||||
_fds_buf_size_cache = None
|
||||
|
||||
def fds_buf_size():
|
||||
# If there may be file descriptors, we try to read 1 message at a time.
|
||||
# The reference implementation of D-Bus defaults to allowing 16 FDs per
|
||||
# message, and the Linux kernel currently allows 253 FDs per sendmsg()
|
||||
# call. So hopefully allowing 256 FDs per recvmsg() will always suffice.
|
||||
global _fds_buf_size_cache
|
||||
if _fds_buf_size_cache is None:
|
||||
maxfds = 256
|
||||
fd_size = array.array('i').itemsize
|
||||
_fds_buf_size_cache = socket.CMSG_SPACE(maxfds * fd_size)
|
||||
return _fds_buf_size_cache
|
||||
@@ -0,0 +1 @@
|
||||
from .common import RouterClosed
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,233 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
from itertools import count
|
||||
from typing import Optional
|
||||
|
||||
from jeepney.auth import Authenticator, BEGIN
|
||||
from jeepney.bus import get_bus
|
||||
from jeepney import Message, MessageType, Parser
|
||||
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||
from jeepney.bus_messages import message_bus
|
||||
from .common import (
|
||||
MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable,
|
||||
)
|
||||
|
||||
|
||||
class DBusConnection:
|
||||
"""A plain D-Bus connection with no matching of replies.
|
||||
|
||||
This doesn't run any separate tasks: sending and receiving are done in
|
||||
the task that calls those methods. It's suitable for implementing servers:
|
||||
several worker tasks can receive requests and send replies.
|
||||
For a typical client pattern, see :class:`DBusRouter`.
|
||||
"""
|
||||
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
self.reader = reader
|
||||
self.writer = writer
|
||||
self.parser = Parser()
|
||||
self.outgoing_serial = count(start=1)
|
||||
self.unique_name = None
|
||||
self.send_lock = asyncio.Lock()
|
||||
|
||||
async def send(self, message: Message, *, serial=None):
|
||||
"""Serialise and send a :class:`~.Message` object"""
|
||||
async with self.send_lock:
|
||||
if serial is None:
|
||||
serial = next(self.outgoing_serial)
|
||||
self.writer.write(message.serialise(serial))
|
||||
await self.writer.drain()
|
||||
|
||||
async def receive(self) -> Message:
|
||||
"""Return the next available message from the connection"""
|
||||
while True:
|
||||
msg = self.parser.get_next_message()
|
||||
if msg is not None:
|
||||
return msg
|
||||
|
||||
b = await self.reader.read(4096)
|
||||
if not b:
|
||||
raise EOFError
|
||||
self.parser.add_data(b)
|
||||
|
||||
async def close(self):
|
||||
"""Close the D-Bus connection"""
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
|
||||
|
||||
async def open_dbus_connection(bus='SESSION'):
|
||||
"""Open a plain D-Bus connection
|
||||
|
||||
:return: :class:`DBusConnection`
|
||||
"""
|
||||
bus_addr = get_bus(bus)
|
||||
reader, writer = await asyncio.open_unix_connection(bus_addr)
|
||||
|
||||
# Authentication flow
|
||||
authr = Authenticator()
|
||||
for req_data in authr:
|
||||
writer.write(req_data)
|
||||
await writer.drain()
|
||||
b = await reader.read(1024)
|
||||
if not b:
|
||||
raise EOFError("Socket closed before authentication")
|
||||
authr.feed(b)
|
||||
|
||||
writer.write(BEGIN)
|
||||
await writer.drain()
|
||||
# Authentication finished
|
||||
|
||||
conn = DBusConnection(reader, writer)
|
||||
|
||||
# Say *Hello* to the message bus - this must be the first message, and the
|
||||
# reply gives us our unique name.
|
||||
async with DBusRouter(conn) as router:
|
||||
reply_body = await asyncio.wait_for(Proxy(message_bus, router).Hello(), 10)
|
||||
conn.unique_name = reply_body[0]
|
||||
|
||||
return conn
|
||||
|
||||
class DBusRouter:
|
||||
"""A 'client' D-Bus connection which can wait for a specific reply.
|
||||
|
||||
This runs a background receiver task, and makes it possible to send a
|
||||
request and wait for the relevant reply.
|
||||
"""
|
||||
_nursery_mgr = None
|
||||
_send_cancel_scope = None
|
||||
_rcv_cancel_scope = None
|
||||
|
||||
def __init__(self, conn: DBusConnection):
|
||||
self._conn = conn
|
||||
self._replies = ReplyMatcher()
|
||||
self._filters = MessageFilters()
|
||||
self._rcv_task = asyncio.create_task(self._receiver())
|
||||
|
||||
@property
|
||||
def unique_name(self):
|
||||
return self._conn.unique_name
|
||||
|
||||
async def send(self, message, *, serial=None):
|
||||
"""Send a message, don't wait for a reply"""
|
||||
await self._conn.send(message, serial=serial)
|
||||
|
||||
async def send_and_get_reply(self, message) -> Message:
|
||||
"""Send a method call message and wait for the reply
|
||||
|
||||
Returns the reply message (method return or error message type).
|
||||
"""
|
||||
check_replyable(message)
|
||||
if self._rcv_task.done():
|
||||
raise RouterClosed("This DBusRouter has stopped")
|
||||
|
||||
serial = next(self._conn.outgoing_serial)
|
||||
|
||||
with self._replies.catch(serial, asyncio.Future()) as reply_fut:
|
||||
await self.send(message, serial=serial)
|
||||
return (await reply_fut)
|
||||
|
||||
def filter(self, rule, *, queue: Optional[asyncio.Queue] =None, bufsize=1):
|
||||
"""Create a filter for incoming messages
|
||||
|
||||
Usage::
|
||||
|
||||
with router.filter(rule) as queue:
|
||||
matching_msg = await queue.get()
|
||||
|
||||
:param MatchRule rule: Catch messages matching this rule
|
||||
:param asyncio.Queue queue: Send matching messages here
|
||||
:param int bufsize: If no queue is passed in, create one with this size
|
||||
"""
|
||||
return FilterHandle(self._filters, rule, queue or asyncio.Queue(bufsize))
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self._rcv_task.done():
|
||||
self._rcv_task.result() # Throw exception if receive task failed
|
||||
else:
|
||||
self._rcv_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._rcv_task
|
||||
return False
|
||||
|
||||
# Code to run in receiver task ------------------------------------
|
||||
|
||||
def _dispatch(self, msg: Message):
|
||||
"""Handle one received message"""
|
||||
if self._replies.dispatch(msg):
|
||||
return
|
||||
|
||||
for filter in list(self._filters.matches(msg)):
|
||||
try:
|
||||
filter.queue.put_nowait(msg)
|
||||
except asyncio.QueueFull:
|
||||
pass
|
||||
|
||||
async def _receiver(self):
|
||||
"""Receiver loop - runs in a separate task"""
|
||||
try:
|
||||
while True:
|
||||
msg = await self._conn.receive()
|
||||
self._dispatch(msg)
|
||||
finally:
|
||||
# Send errors to any tasks still waiting for a message.
|
||||
self._replies.drop_all()
|
||||
|
||||
class open_dbus_router:
|
||||
"""Open a D-Bus 'router' to send and receive messages
|
||||
|
||||
Use as an async context manager::
|
||||
|
||||
async with open_dbus_router() as router:
|
||||
...
|
||||
"""
|
||||
conn = None
|
||||
req_ctx = None
|
||||
|
||||
def __init__(self, bus='SESSION'):
|
||||
self.bus = bus
|
||||
|
||||
async def __aenter__(self):
|
||||
self.conn = await open_dbus_connection(self.bus)
|
||||
self.req_ctx = DBusRouter(self.conn)
|
||||
return await self.req_ctx.__aenter__()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.req_ctx.__aexit__(exc_type, exc_val, exc_tb)
|
||||
await self.conn.close()
|
||||
|
||||
|
||||
class Proxy(ProxyBase):
|
||||
"""An asyncio proxy for calling D-Bus methods
|
||||
|
||||
You can call methods on the proxy object, such as ``await bus_proxy.Hello()``
|
||||
to make a method call over D-Bus and wait for a reply. It will either
|
||||
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||
The methods available are defined by the message generator you wrap.
|
||||
|
||||
:param msggen: A message generator object.
|
||||
:param ~asyncio.DBusRouter router: Router to send and receive messages.
|
||||
"""
|
||||
def __init__(self, msggen, router):
|
||||
super().__init__(msggen)
|
||||
self._router = router
|
||||
|
||||
def __repr__(self):
|
||||
return 'Proxy({}, {})'.format(self._msggen, self._router)
|
||||
|
||||
def _method_call(self, make_msg):
|
||||
async def inner(*args, **kwargs):
|
||||
msg = make_msg(*args, **kwargs)
|
||||
assert msg.header.message_type is MessageType.method_call
|
||||
reply = await self._router.send_and_get_reply(msg)
|
||||
return unwrap_msg(reply)
|
||||
|
||||
return inner
|
||||
@@ -0,0 +1,350 @@
|
||||
"""Synchronous IO wrappers around jeepney
|
||||
"""
|
||||
import array
|
||||
from collections import deque
|
||||
from errno import ECONNRESET
|
||||
import functools
|
||||
from itertools import count
|
||||
import os
|
||||
from selectors import DefaultSelector, EVENT_READ
|
||||
import socket
|
||||
import time
|
||||
from typing import Optional
|
||||
from warnings import warn
|
||||
|
||||
from jeepney import Parser, Message, MessageType, HeaderFields
|
||||
from jeepney.auth import Authenticator, BEGIN
|
||||
from jeepney.bus import get_bus
|
||||
from jeepney.fds import FileDescriptor, fds_buf_size
|
||||
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||
from jeepney.routing import Router
|
||||
from jeepney.bus_messages import message_bus
|
||||
from .common import MessageFilters, FilterHandle, check_replyable
|
||||
|
||||
__all__ = [
|
||||
'open_dbus_connection',
|
||||
'DBusConnection',
|
||||
'Proxy',
|
||||
]
|
||||
|
||||
|
||||
class _Future:
|
||||
def __init__(self):
|
||||
self._result = None
|
||||
|
||||
def done(self):
|
||||
return bool(self._result)
|
||||
|
||||
def set_exception(self, exception):
|
||||
self._result = (False, exception)
|
||||
|
||||
def set_result(self, result):
|
||||
self._result = (True, result)
|
||||
|
||||
def result(self):
|
||||
success, value = self._result
|
||||
if success:
|
||||
return value
|
||||
raise value
|
||||
|
||||
|
||||
def timeout_to_deadline(timeout):
|
||||
if timeout is not None:
|
||||
return time.monotonic() + timeout
|
||||
return None
|
||||
|
||||
def deadline_to_timeout(deadline):
|
||||
if deadline is not None:
|
||||
return max(deadline - time.monotonic(), 0.)
|
||||
return None
|
||||
|
||||
|
||||
class DBusConnectionBase:
|
||||
"""Connection machinery shared by this module and threading"""
|
||||
def __init__(self, sock: socket.socket, enable_fds=False):
|
||||
self.sock = sock
|
||||
self.enable_fds = enable_fds
|
||||
self.parser = Parser()
|
||||
self.outgoing_serial = count(start=1)
|
||||
self.selector = DefaultSelector()
|
||||
self.select_key = self.selector.register(sock, EVENT_READ)
|
||||
self.unique_name = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
def _serialise(self, message: Message, serial) -> (bytes, Optional[array.array]):
|
||||
if serial is None:
|
||||
serial = next(self.outgoing_serial)
|
||||
fds = array.array('i') if self.enable_fds else None
|
||||
data = message.serialise(serial=serial, fds=fds)
|
||||
return data, fds
|
||||
|
||||
def _send_with_fds(self, data, fds):
|
||||
bytes_sent = self.sock.sendmsg(
|
||||
[data], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)]
|
||||
)
|
||||
# If sendmsg succeeds, I think ancillary data has been sent atomically?
|
||||
# So now we just need to send any leftover normal data.
|
||||
if bytes_sent < len(data):
|
||||
self.sock.sendall(data[bytes_sent:])
|
||||
|
||||
def _receive(self, deadline):
|
||||
while True:
|
||||
msg = self.parser.get_next_message()
|
||||
if msg is not None:
|
||||
return msg
|
||||
|
||||
b, fds = self._read_some_data(timeout=deadline_to_timeout(deadline))
|
||||
self.parser.add_data(b, fds=fds)
|
||||
|
||||
def _read_some_data(self, timeout=None):
|
||||
for key, ev in self.selector.select(timeout):
|
||||
if key == self.select_key:
|
||||
if self.enable_fds:
|
||||
return self._read_with_fds()
|
||||
else:
|
||||
return unwrap_read(self.sock.recv(4096)), []
|
||||
|
||||
raise TimeoutError
|
||||
|
||||
def _read_with_fds(self):
|
||||
nbytes = self.parser.bytes_desired()
|
||||
data, ancdata, flags, _ = self.sock.recvmsg(nbytes, fds_buf_size())
|
||||
if flags & getattr(socket, 'MSG_CTRUNC', 0):
|
||||
self.close()
|
||||
raise RuntimeError("Unable to receive all file descriptors")
|
||||
return unwrap_read(data), FileDescriptor.from_ancdata(ancdata)
|
||||
|
||||
def close(self):
|
||||
"""Close the connection"""
|
||||
self.selector.close()
|
||||
self.sock.close()
|
||||
|
||||
|
||||
class DBusConnection(DBusConnectionBase):
|
||||
def __init__(self, sock: socket.socket, enable_fds=False):
|
||||
super().__init__(sock, enable_fds)
|
||||
|
||||
# Message routing machinery
|
||||
self._router = Router(_Future) # Old interface, for backwards compat
|
||||
self._filters = MessageFilters()
|
||||
|
||||
# Say Hello, get our unique name
|
||||
self.bus_proxy = Proxy(message_bus, self)
|
||||
hello_reply = self.bus_proxy.Hello()
|
||||
self.unique_name = hello_reply[0]
|
||||
|
||||
@property
|
||||
def router(self):
|
||||
warn("conn.router is deprecated, see the docs for APIs to use instead.",
|
||||
stacklevel=2)
|
||||
return self._router
|
||||
|
||||
def send(self, message: Message, serial=None):
|
||||
"""Serialise and send a :class:`~.Message` object"""
|
||||
data, fds = self._serialise(message, serial)
|
||||
if fds:
|
||||
self._send_with_fds(data, fds)
|
||||
else:
|
||||
self.sock.sendall(data)
|
||||
|
||||
send_message = send # Backwards compatibility
|
||||
|
||||
def receive(self, *, timeout=None) -> Message:
|
||||
"""Return the next available message from the connection
|
||||
|
||||
If the data is ready, this will return immediately, even if timeout<=0.
|
||||
Otherwise, it will wait for up to timeout seconds, or indefinitely if
|
||||
timeout is None. If no message comes in time, it raises TimeoutError.
|
||||
"""
|
||||
return self._receive(timeout_to_deadline(timeout))
|
||||
|
||||
def recv_messages(self, *, timeout=None):
|
||||
"""Receive one message and apply filters
|
||||
|
||||
See :meth:`filter`. Returns nothing.
|
||||
"""
|
||||
msg = self.receive(timeout=timeout)
|
||||
self._router.incoming(msg)
|
||||
for filter in self._filters.matches(msg):
|
||||
filter.queue.append(msg)
|
||||
|
||||
def send_and_get_reply(self, message, *, timeout=None, unwrap=None):
|
||||
"""Send a message, wait for the reply and return it
|
||||
|
||||
Filters are applied to other messages received before the reply -
|
||||
see :meth:`add_filter`.
|
||||
"""
|
||||
check_replyable(message)
|
||||
deadline = timeout_to_deadline(timeout)
|
||||
|
||||
if unwrap is None:
|
||||
unwrap = False
|
||||
else:
|
||||
warn("Passing unwrap= to .send_and_get_reply() is deprecated and "
|
||||
"will break in a future version of Jeepney.", stacklevel=2)
|
||||
|
||||
serial = next(self.outgoing_serial)
|
||||
self.send_message(message, serial=serial)
|
||||
while True:
|
||||
msg_in = self.receive(timeout=deadline_to_timeout(deadline))
|
||||
reply_to = msg_in.header.fields.get(HeaderFields.reply_serial, -1)
|
||||
if reply_to == serial:
|
||||
if unwrap:
|
||||
return unwrap_msg(msg_in)
|
||||
return msg_in
|
||||
|
||||
# Not the reply
|
||||
self._router.incoming(msg_in)
|
||||
for filter in self._filters.matches(msg_in):
|
||||
filter.queue.append(msg_in)
|
||||
|
||||
def filter(self, rule, *, queue: Optional[deque] =None, bufsize=1):
|
||||
"""Create a filter for incoming messages
|
||||
|
||||
Usage::
|
||||
|
||||
with conn.filter(rule) as matches:
|
||||
# matches is a deque containing matched messages
|
||||
matching_msg = conn.recv_until_filtered(matches)
|
||||
|
||||
:param jeepney.MatchRule rule: Catch messages matching this rule
|
||||
:param collections.deque queue: Matched messages will be added to this
|
||||
:param int bufsize: If no deque is passed in, create one with this size
|
||||
"""
|
||||
if queue is None:
|
||||
queue = deque(maxlen=bufsize)
|
||||
return FilterHandle(self._filters, rule, queue)
|
||||
|
||||
def recv_until_filtered(self, queue, *, timeout=None) -> Message:
|
||||
"""Process incoming messages until one is filtered into queue
|
||||
|
||||
Pops the message from queue and returns it, or raises TimeoutError if
|
||||
the optional timeout expires. Without a timeout, this is equivalent to::
|
||||
|
||||
while len(queue) == 0:
|
||||
conn.recv_messages()
|
||||
return queue.popleft()
|
||||
|
||||
In the other I/O modules, there is no need for this, because messages
|
||||
are placed in queues by a separate task.
|
||||
|
||||
:param collections.deque queue: A deque connected by :meth:`filter`
|
||||
:param float timeout: Maximum time to wait in seconds
|
||||
"""
|
||||
deadline = timeout_to_deadline(timeout)
|
||||
while len(queue) == 0:
|
||||
self.recv_messages(timeout=deadline_to_timeout(deadline))
|
||||
return queue.popleft()
|
||||
|
||||
|
||||
class Proxy(ProxyBase):
|
||||
"""A blocking proxy for calling D-Bus methods
|
||||
|
||||
You can call methods on the proxy object, such as ``bus_proxy.Hello()``
|
||||
to make a method call over D-Bus and wait for a reply. It will either
|
||||
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||
The methods available are defined by the message generator you wrap.
|
||||
|
||||
You can set a time limit on a call by passing ``_timeout=`` in the method
|
||||
call, or set a default when creating the proxy. The ``_timeout`` argument
|
||||
is not passed to the message generator.
|
||||
All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it
|
||||
expires before a reply arrives.
|
||||
|
||||
:param msggen: A message generator object
|
||||
:param ~blocking.DBusConnection connection: Connection to send and receive messages
|
||||
:param float timeout: Default seconds to wait for a reply, or None for no limit
|
||||
"""
|
||||
def __init__(self, msggen, connection, *, timeout=None):
|
||||
super().__init__(msggen)
|
||||
self._connection = connection
|
||||
self._timeout = timeout
|
||||
|
||||
def __repr__(self):
|
||||
extra = '' if (self._timeout is None) else f', timeout={self._timeout}'
|
||||
return f"Proxy({self._msggen}, {self._connection}{extra})"
|
||||
|
||||
def _method_call(self, make_msg):
|
||||
@functools.wraps(make_msg)
|
||||
def inner(*args, **kwargs):
|
||||
timeout = kwargs.pop('_timeout', self._timeout)
|
||||
msg = make_msg(*args, **kwargs)
|
||||
assert msg.header.message_type is MessageType.method_call
|
||||
return unwrap_msg(self._connection.send_and_get_reply(
|
||||
msg, timeout=timeout
|
||||
))
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def unwrap_read(b):
|
||||
"""Raise ConnectionResetError from an empty read.
|
||||
|
||||
Sometimes the socket raises an error itself, sometimes it gives no data.
|
||||
I haven't worked out when it behaves each way.
|
||||
"""
|
||||
if not b:
|
||||
raise ConnectionResetError(ECONNRESET, os.strerror(ECONNRESET))
|
||||
return b
|
||||
|
||||
|
||||
def prep_socket(addr, enable_fds=False, timeout=2.0) -> socket.socket:
|
||||
"""Create a socket and authenticate ready to send D-Bus messages"""
|
||||
sock = socket.socket(family=socket.AF_UNIX)
|
||||
|
||||
# To impose the overall auth timeout, we'll update the timeout on the socket
|
||||
# before each send/receive. This is ugly, but we can't use the socket for
|
||||
# anything else until this has succeeded, so this should be safe.
|
||||
deadline = timeout_to_deadline(timeout)
|
||||
def with_sock_deadline(meth, *args):
|
||||
sock.settimeout(deadline_to_timeout(deadline))
|
||||
return meth(*args)
|
||||
|
||||
try:
|
||||
with_sock_deadline(sock.connect, addr)
|
||||
authr = Authenticator(enable_fds=enable_fds)
|
||||
for req_data in authr:
|
||||
with_sock_deadline(sock.sendall, req_data)
|
||||
authr.feed(unwrap_read(with_sock_deadline(sock.recv, 1024)))
|
||||
with_sock_deadline(sock.sendall, BEGIN)
|
||||
except socket.timeout as e:
|
||||
sock.close()
|
||||
raise TimeoutError(f"Did not authenticate in {timeout} seconds") from e
|
||||
except:
|
||||
sock.close()
|
||||
raise
|
||||
|
||||
sock.settimeout(None) # Put the socket back in blocking mode
|
||||
return sock
|
||||
|
||||
|
||||
def open_dbus_connection(
|
||||
bus='SESSION', enable_fds=False, auth_timeout=1.,
|
||||
) -> DBusConnection:
|
||||
"""Connect to a D-Bus message bus
|
||||
|
||||
Pass ``enable_fds=True`` to allow sending & receiving file descriptors.
|
||||
An error will be raised if the bus does not allow this. For simplicity,
|
||||
it's advisable to leave this disabled unless you need it.
|
||||
|
||||
D-Bus has an authentication step before sending or receiving messages.
|
||||
This takes < 1 ms in normal operation, but there is a timeout so that client
|
||||
code won't get stuck if the server doesn't reply. *auth_timeout* configures
|
||||
this timeout in seconds.
|
||||
"""
|
||||
bus_addr = get_bus(bus)
|
||||
sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout)
|
||||
|
||||
conn = DBusConnection(sock, enable_fds)
|
||||
return conn
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
conn = open_dbus_connection()
|
||||
print("Unique name:", conn.unique_name)
|
||||
@@ -0,0 +1,88 @@
|
||||
from contextlib import contextmanager
|
||||
from itertools import count
|
||||
|
||||
from jeepney import HeaderFields, Message, MessageFlag, MessageType
|
||||
|
||||
class MessageFilters:
|
||||
def __init__(self):
|
||||
self.filters = {}
|
||||
self.filter_ids = count()
|
||||
|
||||
def matches(self, message):
|
||||
for handle in self.filters.values():
|
||||
if handle.rule.matches(message):
|
||||
yield handle
|
||||
|
||||
|
||||
class FilterHandle:
|
||||
def __init__(self, filters: MessageFilters, rule, queue):
|
||||
self._filters = filters
|
||||
self._filter_id = next(filters.filter_ids)
|
||||
self.rule = rule
|
||||
self.queue = queue
|
||||
|
||||
self._filters.filters[self._filter_id] = self
|
||||
|
||||
def close(self):
|
||||
del self._filters.filters[self._filter_id]
|
||||
|
||||
def __enter__(self):
|
||||
return self.queue
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
|
||||
class ReplyMatcher:
|
||||
def __init__(self):
|
||||
self._futures = {}
|
||||
|
||||
@contextmanager
|
||||
def catch(self, serial, future):
|
||||
"""Context manager to capture a reply for the given serial number"""
|
||||
self._futures[serial] = future
|
||||
|
||||
try:
|
||||
yield future
|
||||
finally:
|
||||
del self._futures[serial]
|
||||
|
||||
def dispatch(self, msg):
|
||||
"""Dispatch an incoming message which may be a reply
|
||||
|
||||
Returns True if a task was waiting for it, otherwise False.
|
||||
"""
|
||||
rep_serial = msg.header.fields.get(HeaderFields.reply_serial, -1)
|
||||
if rep_serial in self._futures:
|
||||
self._futures[rep_serial].set_result(msg)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def drop_all(self, exc: Exception = None):
|
||||
"""Throw an error in any task still waiting for a reply"""
|
||||
if exc is None:
|
||||
exc = RouterClosed("D-Bus router closed before reply arrived")
|
||||
futures, self._futures = self._futures, {}
|
||||
for fut in futures.values():
|
||||
fut.set_exception(exc)
|
||||
|
||||
|
||||
class RouterClosed(Exception):
|
||||
"""Raised in tasks waiting for a reply when the router is closed
|
||||
|
||||
This will also be raised if the receiver task crashes, so tasks are not
|
||||
stuck waiting for a reply that can never come. The router object will not
|
||||
be usable after this is raised.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def check_replyable(msg: Message):
|
||||
"""Raise an error if we wouldn't expect a reply for msg"""
|
||||
if msg.header.message_type != MessageType.method_call:
|
||||
raise TypeError("Only method call messages have replies "
|
||||
f"(not {msg.header.message_type})")
|
||||
if MessageFlag.no_reply_expected & msg.header.flags:
|
||||
raise ValueError("This message has the no_reply_expected flag set")
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,81 @@
|
||||
from tempfile import TemporaryFile
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from jeepney import (
|
||||
DBusAddress, HeaderFields, message_bus, MessageType, new_error,
|
||||
new_method_return,
|
||||
)
|
||||
from jeepney.io.threading import open_dbus_connection, DBusRouter, Proxy
|
||||
|
||||
@pytest.fixture()
|
||||
def respond_with_fd():
|
||||
name = "io.gitlab.takluyver.jeepney.tests.respond_with_fd"
|
||||
addr = DBusAddress(bus_name=name, object_path='/')
|
||||
|
||||
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||
with DBusRouter(conn) as router:
|
||||
status, = Proxy(message_bus, router).RequestName(name)
|
||||
assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
|
||||
|
||||
def _reply_once():
|
||||
while True:
|
||||
msg = conn.receive()
|
||||
if msg.header.message_type is MessageType.method_call:
|
||||
if msg.header.fields[HeaderFields.member] == 'GetFD':
|
||||
with TemporaryFile('w+') as tf:
|
||||
tf.write('readme')
|
||||
tf.seek(0)
|
||||
rep = new_method_return(msg, 'h', (tf,))
|
||||
conn.send(rep)
|
||||
return
|
||||
else:
|
||||
conn.send(new_error(msg, 'NoMethod'))
|
||||
|
||||
reply_thread = threading.Thread(target=_reply_once, daemon=True)
|
||||
reply_thread.start()
|
||||
yield addr
|
||||
|
||||
reply_thread.join()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def read_from_fd():
|
||||
name = "io.gitlab.takluyver.jeepney.tests.read_from_fd"
|
||||
addr = DBusAddress(bus_name=name, object_path='/')
|
||||
|
||||
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||
with DBusRouter(conn) as router:
|
||||
status, = Proxy(message_bus, router).RequestName(name)
|
||||
assert status == 1 # DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
|
||||
|
||||
def _reply_once():
|
||||
while True:
|
||||
msg = conn.receive()
|
||||
if msg.header.message_type is MessageType.method_call:
|
||||
if msg.header.fields[HeaderFields.member] == 'ReadFD':
|
||||
with msg.body[0].to_file('rb') as f:
|
||||
f.seek(0)
|
||||
b = f.read()
|
||||
conn.send(new_method_return(msg, 'ay', (b,)))
|
||||
return
|
||||
else:
|
||||
conn.send(new_error(msg, 'NoMethod'))
|
||||
|
||||
reply_thread = threading.Thread(target=_reply_once, daemon=True)
|
||||
reply_thread.start()
|
||||
yield addr
|
||||
|
||||
reply_thread.join()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def temp_file_and_contents():
|
||||
data = b'abc123'
|
||||
with TemporaryFile('w+b') as tf:
|
||||
tf.write(data)
|
||||
tf.flush()
|
||||
tf.seek(0)
|
||||
yield tf, data
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import asyncio
|
||||
|
||||
import async_timeout
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from jeepney import DBusAddress, new_method_call
|
||||
from jeepney.bus_messages import message_bus, MatchRule
|
||||
from jeepney.io.asyncio import (
|
||||
open_dbus_connection, open_dbus_router, Proxy
|
||||
)
|
||||
from .utils import have_session_bus
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.asyncio,
|
||||
pytest.mark.skipif(
|
||||
not have_session_bus, reason="Tests require DBus session bus"
|
||||
),
|
||||
]
|
||||
|
||||
bus_peer = DBusAddress(
|
||||
bus_name='org.freedesktop.DBus',
|
||||
object_path='/org/freedesktop/DBus',
|
||||
interface='org.freedesktop.DBus.Peer'
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def connection():
|
||||
async with (await open_dbus_connection(bus='SESSION')) as conn:
|
||||
yield conn
|
||||
|
||||
async def test_connect(connection):
|
||||
assert connection.unique_name.startswith(':')
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def router():
|
||||
async with open_dbus_router(bus='SESSION') as router:
|
||||
yield router
|
||||
|
||||
async def test_send_and_get_reply(router):
|
||||
ping_call = new_method_call(bus_peer, 'Ping')
|
||||
reply = await asyncio.wait_for(
|
||||
router.send_and_get_reply(ping_call), timeout=5
|
||||
)
|
||||
assert reply.body == ()
|
||||
|
||||
async def test_proxy(router):
|
||||
proxy = Proxy(message_bus, router)
|
||||
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||
res = await proxy.RequestName(name)
|
||||
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||
|
||||
has_owner, = await proxy.NameHasOwner(name)
|
||||
assert has_owner is True
|
||||
|
||||
async def test_filter(router):
|
||||
bus = Proxy(message_bus, router)
|
||||
name = "io.gitlab.takluyver.jeepney.tests.asyncio_test_filter"
|
||||
|
||||
match_rule = MatchRule(
|
||||
type="signal",
|
||||
sender=message_bus.bus_name,
|
||||
interface=message_bus.interface,
|
||||
member="NameOwnerChanged",
|
||||
path=message_bus.object_path,
|
||||
)
|
||||
match_rule.add_arg_condition(0, name)
|
||||
|
||||
# Ask the message bus to subscribe us to this signal
|
||||
await bus.AddMatch(match_rule)
|
||||
|
||||
with router.filter(match_rule) as queue:
|
||||
res, = await bus.RequestName(name)
|
||||
assert res == 1 # 1: got the name
|
||||
|
||||
signal_msg = await asyncio.wait_for(queue.get(), timeout=2.0)
|
||||
assert signal_msg.body == (name, '', router.unique_name)
|
||||
|
||||
async def test_recv_after_connect():
|
||||
# Can't use here:
|
||||
# 1. 'connection' fixture
|
||||
# 2. asyncio.wait_for()
|
||||
# If (1) and/or (2) is used, the error won't be triggered.
|
||||
conn = await open_dbus_connection(bus='SESSION')
|
||||
try:
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
async with async_timeout.timeout(0):
|
||||
await conn.receive()
|
||||
finally:
|
||||
await conn.close()
|
||||
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
|
||||
from jeepney import new_method_call, MessageType, DBusAddress
|
||||
from jeepney.bus_messages import message_bus, MatchRule
|
||||
from jeepney.io.blocking import open_dbus_connection, Proxy
|
||||
from .utils import have_session_bus
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not have_session_bus, reason="Tests require DBus session bus"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def session_conn():
|
||||
with open_dbus_connection(bus='SESSION') as conn:
|
||||
yield conn
|
||||
|
||||
|
||||
def test_connect(session_conn):
|
||||
assert session_conn.unique_name.startswith(':')
|
||||
|
||||
bus_peer = DBusAddress(
|
||||
bus_name='org.freedesktop.DBus',
|
||||
object_path='/org/freedesktop/DBus',
|
||||
interface='org.freedesktop.DBus.Peer'
|
||||
)
|
||||
|
||||
def test_send_and_get_reply(session_conn):
|
||||
ping_call = new_method_call(bus_peer, 'Ping')
|
||||
reply = session_conn.send_and_get_reply(ping_call, timeout=5)
|
||||
assert reply.header.message_type == MessageType.method_return
|
||||
assert reply.body == ()
|
||||
|
||||
ping_call = new_method_call(bus_peer, 'Ping')
|
||||
reply_body = session_conn.send_and_get_reply(ping_call, timeout=5, unwrap=True)
|
||||
assert reply_body == ()
|
||||
|
||||
def test_proxy(session_conn):
|
||||
proxy = Proxy(message_bus, session_conn, timeout=5)
|
||||
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||
res = proxy.RequestName(name)
|
||||
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||
|
||||
has_owner, = proxy.NameHasOwner(name, _timeout=3)
|
||||
assert has_owner is True
|
||||
|
||||
def test_filter(session_conn):
|
||||
bus = Proxy(message_bus, session_conn)
|
||||
name = "io.gitlab.takluyver.jeepney.tests.blocking_test_filter"
|
||||
|
||||
match_rule = MatchRule(
|
||||
type="signal",
|
||||
sender=message_bus.bus_name,
|
||||
interface=message_bus.interface,
|
||||
member="NameOwnerChanged",
|
||||
path=message_bus.object_path,
|
||||
)
|
||||
match_rule.add_arg_condition(0, name)
|
||||
|
||||
# Ask the message bus to subscribe us to this signal
|
||||
bus.AddMatch(match_rule)
|
||||
|
||||
with session_conn.filter(match_rule) as matches:
|
||||
res, = bus.RequestName(name)
|
||||
assert res == 1 # 1: got the name
|
||||
|
||||
signal_msg = session_conn.recv_until_filtered(matches, timeout=2)
|
||||
|
||||
assert signal_msg.body == (name, '', session_conn.unique_name)
|
||||
|
||||
|
||||
def test_recv_fd(respond_with_fd):
|
||||
getfd_call = new_method_call(respond_with_fd, 'GetFD')
|
||||
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||
reply = conn.send_and_get_reply(getfd_call, timeout=5)
|
||||
|
||||
assert reply.header.message_type is MessageType.method_return
|
||||
with reply.body[0].to_file('w+') as f:
|
||||
assert f.read() == 'readme'
|
||||
|
||||
|
||||
def test_send_fd(temp_file_and_contents, read_from_fd):
|
||||
temp_file, data = temp_file_and_contents
|
||||
readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,))
|
||||
with open_dbus_connection(bus='SESSION', enable_fds=True) as conn:
|
||||
reply = conn.send_and_get_reply(readfd_call, timeout=5)
|
||||
|
||||
assert reply.header.message_type is MessageType.method_return
|
||||
assert reply.body[0] == data
|
||||
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
|
||||
from jeepney import new_method_call, MessageType, DBusAddress
|
||||
from jeepney.bus_messages import message_bus, MatchRule
|
||||
from jeepney.io.threading import open_dbus_router, Proxy
|
||||
from .utils import have_session_bus
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not have_session_bus, reason="Tests require DBus session bus"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def router():
|
||||
with open_dbus_router(bus='SESSION') as conn:
|
||||
yield conn
|
||||
|
||||
|
||||
def test_connect(router):
|
||||
assert router.unique_name.startswith(':')
|
||||
|
||||
bus_peer = DBusAddress(
|
||||
bus_name='org.freedesktop.DBus',
|
||||
object_path='/org/freedesktop/DBus',
|
||||
interface='org.freedesktop.DBus.Peer'
|
||||
)
|
||||
|
||||
def test_send_and_get_reply(router):
|
||||
ping_call = new_method_call(bus_peer, 'Ping')
|
||||
reply = router.send_and_get_reply(ping_call, timeout=5)
|
||||
assert reply.header.message_type == MessageType.method_return
|
||||
assert reply.body == ()
|
||||
|
||||
def test_proxy(router):
|
||||
proxy = Proxy(message_bus, router, timeout=5)
|
||||
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||
res = proxy.RequestName(name)
|
||||
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||
|
||||
has_owner, = proxy.NameHasOwner(name, _timeout=3)
|
||||
assert has_owner is True
|
||||
|
||||
def test_filter(router):
|
||||
bus = Proxy(message_bus, router)
|
||||
name = "io.gitlab.takluyver.jeepney.tests.threading_test_filter"
|
||||
|
||||
match_rule = MatchRule(
|
||||
type="signal",
|
||||
sender=message_bus.bus_name,
|
||||
interface=message_bus.interface,
|
||||
member="NameOwnerChanged",
|
||||
path=message_bus.object_path,
|
||||
)
|
||||
match_rule.add_arg_condition(0, name)
|
||||
|
||||
# Ask the message bus to subscribe us to this signal
|
||||
bus.AddMatch(match_rule)
|
||||
|
||||
with router.filter(match_rule) as queue:
|
||||
res, = bus.RequestName(name)
|
||||
assert res == 1 # 1: got the name
|
||||
|
||||
signal_msg = queue.get(timeout=2.0)
|
||||
assert signal_msg.body == (name, '', router.unique_name)
|
||||
|
||||
|
||||
def test_recv_fd(respond_with_fd):
|
||||
getfd_call = new_method_call(respond_with_fd, 'GetFD')
|
||||
with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||
reply = router.send_and_get_reply(getfd_call, timeout=5)
|
||||
|
||||
assert reply.header.message_type is MessageType.method_return
|
||||
with reply.body[0].to_file('w+') as f:
|
||||
assert f.read() == 'readme'
|
||||
|
||||
|
||||
def test_send_fd(temp_file_and_contents, read_from_fd):
|
||||
temp_file, data = temp_file_and_contents
|
||||
readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,))
|
||||
with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||
reply = router.send_and_get_reply(readfd_call, timeout=5)
|
||||
|
||||
assert reply.header.message_type is MessageType.method_return
|
||||
assert reply.body[0] == data
|
||||
@@ -0,0 +1,114 @@
|
||||
import trio
|
||||
import pytest
|
||||
|
||||
from jeepney import DBusAddress, DBusErrorResponse, MessageType, new_method_call
|
||||
from jeepney.bus_messages import message_bus, MatchRule
|
||||
from jeepney.io.trio import (
|
||||
open_dbus_connection, open_dbus_router, Proxy,
|
||||
)
|
||||
from .utils import have_session_bus
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.trio,
|
||||
pytest.mark.skipif(
|
||||
not have_session_bus, reason="Tests require DBus session bus"
|
||||
),
|
||||
]
|
||||
|
||||
# Can't use any async fixtures here, because pytest-asyncio tries to handle
|
||||
# all of them: https://github.com/pytest-dev/pytest-asyncio/issues/124
|
||||
|
||||
async def test_connect():
|
||||
conn = await open_dbus_connection(bus='SESSION')
|
||||
async with conn:
|
||||
assert conn.unique_name.startswith(':')
|
||||
|
||||
bus_peer = DBusAddress(
|
||||
bus_name='org.freedesktop.DBus',
|
||||
object_path='/org/freedesktop/DBus',
|
||||
interface='org.freedesktop.DBus.Peer'
|
||||
)
|
||||
|
||||
async def test_send_and_get_reply():
|
||||
ping_call = new_method_call(bus_peer, 'Ping')
|
||||
async with open_dbus_router(bus='SESSION') as req:
|
||||
with trio.fail_after(5):
|
||||
reply = await req.send_and_get_reply(ping_call)
|
||||
|
||||
assert reply.header.message_type == MessageType.method_return
|
||||
assert reply.body == ()
|
||||
|
||||
|
||||
async def test_send_and_get_reply_error():
|
||||
ping_call = new_method_call(bus_peer, 'Snart') # No such method
|
||||
async with open_dbus_router(bus='SESSION') as req:
|
||||
with trio.fail_after(5):
|
||||
reply = await req.send_and_get_reply(ping_call)
|
||||
|
||||
assert reply.header.message_type == MessageType.error
|
||||
|
||||
|
||||
async def test_proxy():
|
||||
async with open_dbus_router(bus='SESSION') as req:
|
||||
proxy = Proxy(message_bus, req)
|
||||
name = "io.gitlab.takluyver.jeepney.examples.Server"
|
||||
res = await proxy.RequestName(name)
|
||||
assert res in {(1,), (2,)} # 1: got the name, 2: queued
|
||||
|
||||
has_owner, = await proxy.NameHasOwner(name)
|
||||
assert has_owner is True
|
||||
|
||||
|
||||
async def test_proxy_error():
|
||||
async with open_dbus_router(bus='SESSION') as req:
|
||||
proxy = Proxy(message_bus, req)
|
||||
with pytest.raises(DBusErrorResponse):
|
||||
await proxy.RequestName(":123") # Invalid name
|
||||
|
||||
|
||||
async def test_filter():
|
||||
name = "io.gitlab.takluyver.jeepney.tests.trio_test_filter"
|
||||
async with open_dbus_router(bus='SESSION') as router:
|
||||
bus = Proxy(message_bus, router)
|
||||
|
||||
match_rule = MatchRule(
|
||||
type="signal",
|
||||
sender=message_bus.bus_name,
|
||||
interface=message_bus.interface,
|
||||
member="NameOwnerChanged",
|
||||
path=message_bus.object_path,
|
||||
)
|
||||
match_rule.add_arg_condition(0, name)
|
||||
|
||||
# Ask the message bus to subscribe us to this signal
|
||||
await bus.AddMatch(match_rule)
|
||||
|
||||
async with router.filter(match_rule) as chan:
|
||||
res, = await bus.RequestName(name)
|
||||
assert res == 1 # 1: got the name
|
||||
|
||||
with trio.fail_after(2.0):
|
||||
signal_msg = await chan.receive()
|
||||
assert signal_msg.body == (name, '', router.unique_name)
|
||||
|
||||
|
||||
async def test_recv_fd(respond_with_fd):
|
||||
getfd_call = new_method_call(respond_with_fd, 'GetFD')
|
||||
with trio.fail_after(5):
|
||||
async with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||
reply = await router.send_and_get_reply(getfd_call)
|
||||
|
||||
assert reply.header.message_type is MessageType.method_return
|
||||
with reply.body[0].to_file('w+') as f:
|
||||
assert f.read() == 'readme'
|
||||
|
||||
|
||||
async def test_send_fd(temp_file_and_contents, read_from_fd):
|
||||
temp_file, data = temp_file_and_contents
|
||||
readfd_call = new_method_call(read_from_fd, 'ReadFD', 'h', (temp_file,))
|
||||
with trio.fail_after(5):
|
||||
async with open_dbus_router(bus='SESSION', enable_fds=True) as router:
|
||||
reply = await router.send_and_get_reply(readfd_call)
|
||||
|
||||
assert reply.header.message_type is MessageType.method_return
|
||||
assert reply.body[0] == data
|
||||
@@ -0,0 +1,3 @@
|
||||
import os
|
||||
|
||||
have_session_bus = bool(os.environ.get('DBUS_SESSION_BUS_ADDRESS'))
|
||||
@@ -0,0 +1,273 @@
|
||||
"""Synchronous IO wrappers with thread safety
|
||||
"""
|
||||
from concurrent.futures import Future
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
import os
|
||||
from selectors import EVENT_READ
|
||||
import socket
|
||||
from queue import Queue, Full as QueueFull
|
||||
from threading import Lock, Thread
|
||||
from typing import Optional
|
||||
|
||||
from jeepney import Message, MessageType
|
||||
from jeepney.bus import get_bus
|
||||
from jeepney.bus_messages import message_bus
|
||||
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||
from .blocking import (
|
||||
unwrap_read, prep_socket, DBusConnectionBase, timeout_to_deadline,
|
||||
)
|
||||
from .common import (
|
||||
MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'open_dbus_connection',
|
||||
'open_dbus_router',
|
||||
'DBusConnection',
|
||||
'DBusRouter',
|
||||
'Proxy',
|
||||
'ReceiveStopped',
|
||||
]
|
||||
|
||||
|
||||
class ReceiveStopped(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DBusConnection(DBusConnectionBase):
|
||||
def __init__(self, sock: socket.socket, enable_fds=False):
|
||||
super().__init__(sock, enable_fds=enable_fds)
|
||||
self._stop_r, self._stop_w = os.pipe()
|
||||
self.stop_key = self.selector.register(self._stop_r, EVENT_READ)
|
||||
self.send_lock = Lock()
|
||||
self.rcv_lock = Lock()
|
||||
|
||||
def send(self, message: Message, serial=None):
|
||||
"""Serialise and send a :class:`~.Message` object"""
|
||||
data, fds = self._serialise(message, serial)
|
||||
with self.send_lock:
|
||||
if fds:
|
||||
self._send_with_fds(data, fds)
|
||||
else:
|
||||
self.sock.sendall(data)
|
||||
|
||||
def receive(self, *, timeout=None) -> Message:
|
||||
"""Return the next available message from the connection
|
||||
|
||||
If the data is ready, this will return immediately, even if timeout<=0.
|
||||
Otherwise, it will wait for up to timeout seconds, or indefinitely if
|
||||
timeout is None. If no message comes in time, it raises TimeoutError.
|
||||
|
||||
If the connection is closed from another thread, this will raise
|
||||
ReceiveStopped.
|
||||
"""
|
||||
deadline = timeout_to_deadline(timeout)
|
||||
|
||||
if not self.rcv_lock.acquire(timeout=(timeout or -1)):
|
||||
raise TimeoutError(f"Did not get receive lock in {timeout} seconds")
|
||||
try:
|
||||
return self._receive(deadline)
|
||||
finally:
|
||||
self.rcv_lock.release()
|
||||
|
||||
def _read_some_data(self, timeout=None):
|
||||
# Wait for data or a signal on the stop pipe
|
||||
for key, ev in self.selector.select(timeout):
|
||||
if key == self.select_key:
|
||||
if self.enable_fds:
|
||||
return self._read_with_fds()
|
||||
else:
|
||||
return unwrap_read(self.sock.recv(4096)), []
|
||||
elif key == self.stop_key:
|
||||
raise ReceiveStopped("DBus receive stopped from another thread")
|
||||
|
||||
raise TimeoutError
|
||||
|
||||
def interrupt(self):
|
||||
"""Make any threads waiting for a message raise ReceiveStopped"""
|
||||
os.write(self._stop_w, b'a')
|
||||
|
||||
def reset_interrupt(self):
|
||||
"""Allow calls to .receive() again after .interrupt()
|
||||
|
||||
To avoid race conditions, you should typically wait for threads to
|
||||
respond (e.g. by joining them) between interrupting and resetting.
|
||||
"""
|
||||
# Clear any data on the stop pipe
|
||||
while (self.stop_key, EVENT_READ) in self.selector.select(timeout=0):
|
||||
os.read(self._stop_r, 1024)
|
||||
|
||||
def close(self):
|
||||
"""Close the connection"""
|
||||
self.interrupt()
|
||||
super().close()
|
||||
|
||||
|
||||
def open_dbus_connection(bus='SESSION', enable_fds=False, auth_timeout=1.):
|
||||
"""Open a plain D-Bus connection
|
||||
|
||||
D-Bus has an authentication step before sending or receiving messages.
|
||||
This takes < 1 ms in normal operation, but there is a timeout so that client
|
||||
code won't get stuck if the server doesn't reply. *auth_timeout* configures
|
||||
this timeout in seconds.
|
||||
|
||||
:return: :class:`DBusConnection`
|
||||
"""
|
||||
bus_addr = get_bus(bus)
|
||||
sock = prep_socket(bus_addr, enable_fds, timeout=auth_timeout)
|
||||
|
||||
conn = DBusConnection(sock, enable_fds)
|
||||
|
||||
with DBusRouter(conn) as router:
|
||||
reply_body = Proxy(message_bus, router, timeout=10).Hello()
|
||||
conn.unique_name = reply_body[0]
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
class DBusRouter:
|
||||
"""A client D-Bus connection which can wait for replies.
|
||||
|
||||
This runs a separate receiver thread and dispatches received messages.
|
||||
|
||||
It's possible to wrap a :class:`DBusConnection` in a router temporarily.
|
||||
Using the connection directly while it is wrapped is not supported,
|
||||
but you can use it again after the router is closed.
|
||||
"""
|
||||
def __init__(self, conn: DBusConnection):
|
||||
self.conn = conn
|
||||
self._replies = ReplyMatcher()
|
||||
self._filters = MessageFilters()
|
||||
self._rcv_thread = Thread(target=self._receiver, daemon=True)
|
||||
self._rcv_thread.start()
|
||||
|
||||
@property
|
||||
def unique_name(self):
|
||||
return self.conn.unique_name
|
||||
|
||||
def send(self, message, *, serial=None):
|
||||
"""Serialise and send a :class:`~.Message` object"""
|
||||
self.conn.send(message, serial=serial)
|
||||
|
||||
def send_and_get_reply(self, msg: Message, *, timeout=None) -> Message:
|
||||
"""Send a method call message, wait for and return a reply"""
|
||||
check_replyable(msg)
|
||||
if not self._rcv_thread.is_alive():
|
||||
raise RouterClosed("This D-Bus router has stopped")
|
||||
|
||||
serial = next(self.conn.outgoing_serial)
|
||||
|
||||
with self._replies.catch(serial, Future()) as reply_fut:
|
||||
self.conn.send(msg, serial=serial)
|
||||
return reply_fut.result(timeout=timeout)
|
||||
|
||||
def close(self):
|
||||
"""Close this router
|
||||
|
||||
This does not close the underlying connection.
|
||||
"""
|
||||
self.conn.interrupt()
|
||||
self._rcv_thread.join(timeout=10)
|
||||
self.conn.reset_interrupt()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
def filter(self, rule, *, queue: Optional[Queue] =None, bufsize=1):
|
||||
"""Create a filter for incoming messages
|
||||
|
||||
Usage::
|
||||
|
||||
with router.filter(rule) as queue:
|
||||
matching_msg = queue.get()
|
||||
|
||||
:param jeepney.MatchRule rule: Catch messages matching this rule
|
||||
:param queue.Queue queue: Matched messages will be added to this
|
||||
:param int bufsize: If no queue is passed in, create one with this size
|
||||
"""
|
||||
return FilterHandle(self._filters, rule, queue or Queue(maxsize=bufsize))
|
||||
|
||||
# Code to run in receiver thread ------------------------------------
|
||||
|
||||
def _dispatch(self, msg: Message):
|
||||
if self._replies.dispatch(msg):
|
||||
return
|
||||
|
||||
for filter in self._filters.matches(msg):
|
||||
try:
|
||||
filter.queue.put_nowait(msg)
|
||||
except QueueFull:
|
||||
pass
|
||||
|
||||
def _receiver(self):
|
||||
try:
|
||||
while True:
|
||||
msg = self.conn.receive()
|
||||
self._dispatch(msg)
|
||||
except ReceiveStopped:
|
||||
pass
|
||||
finally:
|
||||
# Send errors to any tasks still waiting for a message.
|
||||
self._replies.drop_all()
|
||||
|
||||
class Proxy(ProxyBase):
|
||||
"""A blocking proxy for calling D-Bus methods via a :class:`DBusRouter`.
|
||||
|
||||
You can call methods on the proxy object, such as ``bus_proxy.Hello()``
|
||||
to make a method call over D-Bus and wait for a reply. It will either
|
||||
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||
The methods available are defined by the message generator you wrap.
|
||||
|
||||
You can set a time limit on a call by passing ``_timeout=`` in the method
|
||||
call, or set a default when creating the proxy. The ``_timeout`` argument
|
||||
is not passed to the message generator.
|
||||
All timeouts are in seconds, and :exc:`TimeoutErrror` is raised if it
|
||||
expires before a reply arrives.
|
||||
|
||||
:param msggen: A message generator object
|
||||
:param ~threading.DBusRouter router: Router to send and receive messages
|
||||
:param float timeout: Default seconds to wait for a reply, or None for no limit
|
||||
"""
|
||||
def __init__(self, msggen, router, *, timeout=None):
|
||||
super().__init__(msggen)
|
||||
self._router = router
|
||||
self._timeout = timeout
|
||||
|
||||
def __repr__(self):
|
||||
extra = '' if (self._timeout is None) else f', timeout={self._timeout}'
|
||||
return f"Proxy({self._msggen}, {self._router}{extra})"
|
||||
|
||||
def _method_call(self, make_msg):
|
||||
@functools.wraps(make_msg)
|
||||
def inner(*args, **kwargs):
|
||||
timeout = kwargs.pop('_timeout', self._timeout)
|
||||
msg = make_msg(*args, **kwargs)
|
||||
assert msg.header.message_type is MessageType.method_call
|
||||
reply = self._router.send_and_get_reply(msg, timeout=timeout)
|
||||
return unwrap_msg(reply)
|
||||
|
||||
return inner
|
||||
|
||||
@contextmanager
|
||||
def open_dbus_router(bus='SESSION', enable_fds=False):
|
||||
"""Open a D-Bus 'router' to send and receive messages.
|
||||
|
||||
Use as a context manager::
|
||||
|
||||
with open_dbus_router() as router:
|
||||
...
|
||||
|
||||
On leaving the ``with`` block, the connection will be closed.
|
||||
|
||||
:param str bus: 'SESSION' or 'SYSTEM' or a supported address.
|
||||
:param bool enable_fds: Whether to enable passing file descriptors.
|
||||
:return: :class:`DBusRouter`
|
||||
"""
|
||||
with open_dbus_connection(bus=bus, enable_fds=enable_fds) as conn:
|
||||
with DBusRouter(conn) as router:
|
||||
yield router
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
import array
|
||||
from contextlib import contextmanager
|
||||
import errno
|
||||
from itertools import count
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
from contextlib import asynccontextmanager # Python 3.7
|
||||
except ImportError:
|
||||
from async_generator import asynccontextmanager # Backport for Python 3.6
|
||||
|
||||
from outcome import Value, Error
|
||||
import trio
|
||||
from trio.abc import Channel
|
||||
|
||||
from jeepney.auth import Authenticator, BEGIN
|
||||
from jeepney.bus import get_bus
|
||||
from jeepney.fds import FileDescriptor, fds_buf_size
|
||||
from jeepney.low_level import Parser, MessageType, Message
|
||||
from jeepney.wrappers import ProxyBase, unwrap_msg
|
||||
from jeepney.bus_messages import message_bus
|
||||
from .common import (
|
||||
MessageFilters, FilterHandle, ReplyMatcher, RouterClosed, check_replyable,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
'open_dbus_connection',
|
||||
'open_dbus_router',
|
||||
'Proxy',
|
||||
]
|
||||
|
||||
|
||||
# The function below is copied from trio, which is under the MIT license:
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
@contextmanager
|
||||
def _translate_socket_errors_to_stream_errors():
|
||||
try:
|
||||
yield
|
||||
except OSError as exc:
|
||||
if exc.errno in {errno.EBADF, errno.ENOTSOCK}:
|
||||
# EBADF on Unix, ENOTSOCK on Windows
|
||||
raise trio.ClosedResourceError("this socket was already closed") from None
|
||||
else:
|
||||
raise trio.BrokenResourceError(
|
||||
"socket connection broken: {}".format(exc)
|
||||
) from exc
|
||||
|
||||
|
||||
|
||||
class DBusConnection(Channel):
|
||||
"""A plain D-Bus connection with no matching of replies.
|
||||
|
||||
This doesn't run any separate tasks: sending and receiving are done in
|
||||
the task that calls those methods. It's suitable for implementing servers:
|
||||
several worker tasks can receive requests and send replies.
|
||||
For a typical client pattern, see :class:`DBusRouter`.
|
||||
|
||||
Implements trio's channel interface for Message objects.
|
||||
"""
|
||||
def __init__(self, socket, enable_fds=False):
|
||||
self.socket = socket
|
||||
self.enable_fds = enable_fds
|
||||
self.parser = Parser()
|
||||
self.outgoing_serial = count(start=1)
|
||||
self.unique_name = None
|
||||
self.send_lock = trio.Lock()
|
||||
self.recv_lock = trio.Lock()
|
||||
self._leftover_to_send = None # type: Optional[memoryview]
|
||||
|
||||
async def send(self, message: Message, *, serial=None):
|
||||
"""Serialise and send a :class:`~.Message` object"""
|
||||
async with self.send_lock:
|
||||
if serial is None:
|
||||
serial = next(self.outgoing_serial)
|
||||
fds = array.array('i') if self.enable_fds else None
|
||||
data = message.serialise(serial, fds=fds)
|
||||
await self._send_data(data, fds)
|
||||
|
||||
# _send_data is copied & modified from trio's SocketStream.send_all() .
|
||||
# See above for the MIT license.
|
||||
async def _send_data(self, data: bytes, fds):
|
||||
if self.socket.did_shutdown_SHUT_WR:
|
||||
raise trio.ClosedResourceError("can't send data after sending EOF")
|
||||
|
||||
with _translate_socket_errors_to_stream_errors():
|
||||
if self._leftover_to_send:
|
||||
# A previous message was partly sent - finish sending it now.
|
||||
await self._send_remainder(self._leftover_to_send)
|
||||
|
||||
with memoryview(data) as data:
|
||||
if fds:
|
||||
sent = await self.socket.sendmsg([data], [(
|
||||
trio.socket.SOL_SOCKET, trio.socket.SCM_RIGHTS, fds
|
||||
)])
|
||||
else:
|
||||
sent = await self.socket.send(data)
|
||||
|
||||
await self._send_remainder(data, sent)
|
||||
|
||||
async def _send_remainder(self, data: memoryview, already_sent=0):
|
||||
try:
|
||||
while already_sent < len(data):
|
||||
with data[already_sent:] as remaining:
|
||||
sent = await self.socket.send(remaining)
|
||||
already_sent += sent
|
||||
self._leftover_to_send = None
|
||||
except trio.Cancelled:
|
||||
# Sending cancelled mid-message. Keep track of the remaining data
|
||||
# so it can be sent before the next message, otherwise the next
|
||||
# message won't be recognised.
|
||||
self._leftover_to_send = data[already_sent:]
|
||||
raise
|
||||
|
||||
async def receive(self) -> Message:
|
||||
"""Return the next available message from the connection"""
|
||||
async with self.recv_lock:
|
||||
while True:
|
||||
msg = self.parser.get_next_message()
|
||||
if msg is not None:
|
||||
return msg
|
||||
|
||||
# Once data is read, it must be given to the parser with no
|
||||
# checkpoints (where the task could be cancelled).
|
||||
b, fds = await self._read_data()
|
||||
if not b:
|
||||
raise trio.EndOfChannel("Socket closed at the other end")
|
||||
self.parser.add_data(b, fds)
|
||||
|
||||
async def _read_data(self):
|
||||
if self.enable_fds:
|
||||
nbytes = self.parser.bytes_desired()
|
||||
with _translate_socket_errors_to_stream_errors():
|
||||
data, ancdata, flags, _ = await self.socket.recvmsg(
|
||||
nbytes, fds_buf_size()
|
||||
)
|
||||
if flags & getattr(trio.socket, 'MSG_CTRUNC', 0):
|
||||
self._close()
|
||||
raise RuntimeError("Unable to receive all file descriptors")
|
||||
return data, FileDescriptor.from_ancdata(ancdata)
|
||||
|
||||
else: # not self.enable_fds
|
||||
with _translate_socket_errors_to_stream_errors():
|
||||
data = await self.socket.recv(4096)
|
||||
return data, []
|
||||
|
||||
def _close(self):
|
||||
self.socket.close()
|
||||
self._leftover_to_send = None
|
||||
|
||||
# Our closing is currently sync, but AsyncResource objects must have aclose
|
||||
async def aclose(self):
|
||||
"""Close the D-Bus connection"""
|
||||
self._close()
|
||||
|
||||
@asynccontextmanager
|
||||
async def router(self):
|
||||
"""Temporarily wrap this connection as a :class:`DBusRouter`
|
||||
|
||||
To be used like::
|
||||
|
||||
async with conn.router() as req:
|
||||
reply = await req.send_and_get_reply(msg)
|
||||
|
||||
While the router is running, you shouldn't use :meth:`receive`.
|
||||
Once the router is closed, you can use the plain connection again.
|
||||
"""
|
||||
async with trio.open_nursery() as nursery:
|
||||
router = DBusRouter(self)
|
||||
await router.start(nursery)
|
||||
try:
|
||||
yield router
|
||||
finally:
|
||||
await router.aclose()
|
||||
|
||||
|
||||
async def open_dbus_connection(bus='SESSION', *, enable_fds=False) -> DBusConnection:
|
||||
"""Open a plain D-Bus connection
|
||||
|
||||
:return: :class:`DBusConnection`
|
||||
"""
|
||||
bus_addr = get_bus(bus)
|
||||
sock : trio.SocketStream = await trio.open_unix_socket(bus_addr)
|
||||
|
||||
# Authentication
|
||||
authr = Authenticator(enable_fds=enable_fds)
|
||||
for req_data in authr:
|
||||
await sock.send_all(req_data)
|
||||
authr.feed(await sock.receive_some())
|
||||
await sock.send_all(BEGIN)
|
||||
|
||||
conn = DBusConnection(sock.socket, enable_fds=enable_fds)
|
||||
|
||||
# Say *Hello* to the message bus - this must be the first message, and the
|
||||
# reply gives us our unique name.
|
||||
async with conn.router() as router:
|
||||
reply = await router.send_and_get_reply(message_bus.Hello())
|
||||
conn.unique_name = reply.body[0]
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
class TrioFilterHandle(FilterHandle):
|
||||
def __init__(self, filters: MessageFilters, rule, send_chn, recv_chn):
|
||||
super().__init__(filters, rule, recv_chn)
|
||||
self.send_channel = send_chn
|
||||
|
||||
@property
|
||||
def receive_channel(self):
|
||||
return self.queue
|
||||
|
||||
async def aclose(self):
|
||||
self.close()
|
||||
await self.send_channel.aclose()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self.queue
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.aclose()
|
||||
|
||||
|
||||
class Future:
|
||||
"""A very simple Future for trio based on `trio.Event`."""
|
||||
def __init__(self):
|
||||
self._outcome = None
|
||||
self._event = trio.Event()
|
||||
|
||||
def set_result(self, result):
|
||||
self._outcome = Value(result)
|
||||
self._event.set()
|
||||
|
||||
def set_exception(self, exc):
|
||||
self._outcome = Error(exc)
|
||||
self._event.set()
|
||||
|
||||
async def get(self):
|
||||
await self._event.wait()
|
||||
return self._outcome.unwrap()
|
||||
|
||||
|
||||
class DBusRouter:
|
||||
"""A client D-Bus connection which can wait for replies.
|
||||
|
||||
This runs a separate receiver task and dispatches received messages.
|
||||
"""
|
||||
_nursery_mgr = None
|
||||
_rcv_cancel_scope = None
|
||||
|
||||
def __init__(self, conn: DBusConnection):
|
||||
self._conn = conn
|
||||
self._replies = ReplyMatcher()
|
||||
self._filters = MessageFilters()
|
||||
|
||||
@property
|
||||
def unique_name(self):
|
||||
return self._conn.unique_name
|
||||
|
||||
async def send(self, message, *, serial=None):
|
||||
"""Send a message, don't wait for a reply
|
||||
"""
|
||||
await self._conn.send(message, serial=serial)
|
||||
|
||||
async def send_and_get_reply(self, message) -> Message:
|
||||
"""Send a method call message and wait for the reply
|
||||
|
||||
Returns the reply message (method return or error message type).
|
||||
"""
|
||||
check_replyable(message)
|
||||
if self._rcv_cancel_scope is None:
|
||||
raise RouterClosed("This DBusRouter has stopped")
|
||||
|
||||
serial = next(self._conn.outgoing_serial)
|
||||
|
||||
with self._replies.catch(serial, Future()) as reply_fut:
|
||||
await self.send(message, serial=serial)
|
||||
return (await reply_fut.get())
|
||||
|
||||
def filter(self, rule, *, channel: Optional[trio.MemorySendChannel]=None, bufsize=1):
|
||||
"""Create a filter for incoming messages
|
||||
|
||||
Usage::
|
||||
|
||||
async with router.filter(rule) as receive_channel:
|
||||
matching_msg = await receive_channel.receive()
|
||||
|
||||
# OR:
|
||||
send_chan, recv_chan = trio.open_memory_channel(1)
|
||||
async with router.filter(rule, channel=send_chan):
|
||||
matching_msg = await recv_chan.receive()
|
||||
|
||||
If the channel fills up,
|
||||
The sending end of the channel is closed when leaving the ``async with``
|
||||
block, whether or not it was passed in.
|
||||
|
||||
:param jeepney.MatchRule rule: Catch messages matching this rule
|
||||
:param trio.MemorySendChannel channel: Send matching messages here
|
||||
:param int bufsize: If no channel is passed in, create one with this size
|
||||
"""
|
||||
if channel is None:
|
||||
channel, recv_channel = trio.open_memory_channel(bufsize)
|
||||
else:
|
||||
recv_channel = None
|
||||
return TrioFilterHandle(self._filters, rule, channel, recv_channel)
|
||||
|
||||
# Task management -------------------------------------------
|
||||
|
||||
async def start(self, nursery: trio.Nursery):
|
||||
if self._rcv_cancel_scope is not None:
|
||||
raise RuntimeError("DBusRouter receiver task is already running")
|
||||
self._rcv_cancel_scope = await nursery.start(self._receiver)
|
||||
|
||||
async def aclose(self):
|
||||
"""Stop the sender & receiver tasks"""
|
||||
# It doesn't matter if we receive a partial message - the connection
|
||||
# should ensure that whatever is received is fed to the parser.
|
||||
if self._rcv_cancel_scope is not None:
|
||||
self._rcv_cancel_scope.cancel()
|
||||
self._rcv_cancel_scope = None
|
||||
|
||||
# Ensure trio checkpoint
|
||||
await trio.sleep(0)
|
||||
|
||||
# Code to run in receiver task ------------------------------------
|
||||
|
||||
def _dispatch(self, msg: Message):
|
||||
"""Handle one received message"""
|
||||
if self._replies.dispatch(msg):
|
||||
return
|
||||
|
||||
for filter in self._filters.matches(msg):
|
||||
try:
|
||||
filter.send_channel.send_nowait(msg)
|
||||
except trio.WouldBlock:
|
||||
pass
|
||||
|
||||
async def _receiver(self, task_status=trio.TASK_STATUS_IGNORED):
|
||||
"""Receiver loop - runs in a separate task"""
|
||||
with trio.CancelScope() as cscope:
|
||||
self.is_running = True
|
||||
task_status.started(cscope)
|
||||
try:
|
||||
while True:
|
||||
msg = await self._conn.receive()
|
||||
self._dispatch(msg)
|
||||
finally:
|
||||
self.is_running = False
|
||||
# Send errors to any tasks still waiting for a message.
|
||||
self._replies.drop_all()
|
||||
|
||||
# Closing a memory channel can't block, but it only has an
|
||||
# async close method, so we need to shield it from cancellation.
|
||||
with trio.move_on_after(3) as cleanup_scope:
|
||||
for filter in self._filters.filters.values():
|
||||
cleanup_scope.shield = True
|
||||
await filter.send_channel.aclose()
|
||||
|
||||
|
||||
class Proxy(ProxyBase):
|
||||
"""A trio proxy for calling D-Bus methods
|
||||
|
||||
You can call methods on the proxy object, such as ``await bus_proxy.Hello()``
|
||||
to make a method call over D-Bus and wait for a reply. It will either
|
||||
return a tuple of returned data, or raise :exc:`.DBusErrorResponse`.
|
||||
The methods available are defined by the message generator you wrap.
|
||||
|
||||
:param msggen: A message generator object.
|
||||
:param ~trio.DBusRouter router: Router to send and receive messages.
|
||||
"""
|
||||
def __init__(self, msggen, router):
|
||||
super().__init__(msggen)
|
||||
if not isinstance(router, DBusRouter):
|
||||
raise TypeError("Proxy can only be used with DBusRequester")
|
||||
self._router = router
|
||||
|
||||
def _method_call(self, make_msg):
|
||||
async def inner(*args, **kwargs):
|
||||
msg = make_msg(*args, **kwargs)
|
||||
assert msg.header.message_type is MessageType.method_call
|
||||
reply = await self._router.send_and_get_reply(msg)
|
||||
return unwrap_msg(reply)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def open_dbus_router(bus='SESSION', *, enable_fds=False):
|
||||
"""Open a D-Bus 'router' to send and receive messages.
|
||||
|
||||
Use as an async context manager::
|
||||
|
||||
async with open_dbus_router() as req:
|
||||
...
|
||||
|
||||
:param str bus: 'SESSION' or 'SYSTEM' or a supported address.
|
||||
:return: :class:`DBusRouter`
|
||||
|
||||
This is a shortcut for::
|
||||
|
||||
conn = await open_dbus_connection()
|
||||
async with conn:
|
||||
async with conn.router() as req:
|
||||
...
|
||||
"""
|
||||
conn = await open_dbus_connection(bus, enable_fds=enable_fds)
|
||||
async with conn:
|
||||
async with conn.router() as rtr:
|
||||
yield rtr
|
||||
@@ -0,0 +1,585 @@
|
||||
from collections import deque
|
||||
from enum import Enum, IntEnum, IntFlag
|
||||
import struct
|
||||
from typing import Optional
|
||||
|
||||
class SizeLimitError(ValueError):
|
||||
"""Raised when trying to (de-)serialise data exceeding D-Bus' size limit.
|
||||
|
||||
This is currently only implemented for arrays, where the maximum size is
|
||||
64 MiB.
|
||||
"""
|
||||
pass
|
||||
|
||||
class Endianness(Enum):
|
||||
little = 1
|
||||
big = 2
|
||||
|
||||
def struct_code(self):
|
||||
return '<' if (self is Endianness.little) else '>'
|
||||
|
||||
def dbus_code(self):
|
||||
return b'l' if (self is Endianness.little) else b'B'
|
||||
|
||||
|
||||
endian_map = {b'l': Endianness.little, b'B': Endianness.big}
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
method_call = 1
|
||||
method_return = 2
|
||||
error = 3
|
||||
signal = 4
|
||||
|
||||
|
||||
class MessageFlag(IntFlag):
|
||||
no_reply_expected = 1
|
||||
no_auto_start = 2
|
||||
allow_interactive_authorization = 4
|
||||
|
||||
|
||||
class HeaderFields(IntEnum):
|
||||
path = 1
|
||||
interface = 2
|
||||
member = 3
|
||||
error_name = 4
|
||||
reply_serial = 5
|
||||
destination = 6
|
||||
sender = 7
|
||||
signature = 8
|
||||
unix_fds = 9
|
||||
|
||||
|
||||
def padding(pos, step):
|
||||
pad = step - (pos % step)
|
||||
if pad == step:
|
||||
return 0
|
||||
return pad
|
||||
|
||||
|
||||
class FixedType:
|
||||
def __init__(self, size, struct_code):
|
||||
self.size = self.alignment = size
|
||||
self.struct_code = struct_code
|
||||
|
||||
def parse_data(self, buf, pos, endianness, fds=()):
|
||||
pos += padding(pos, self.alignment)
|
||||
code = endianness.struct_code() + self.struct_code
|
||||
val = struct.unpack(code, buf[pos:pos + self.size])[0]
|
||||
return val, pos + self.size
|
||||
|
||||
def serialise(self, data, pos, endianness, fds=None):
|
||||
pad = b'\0' * padding(pos, self.alignment)
|
||||
code = endianness.struct_code() + self.struct_code
|
||||
return pad + struct.pack(code, data)
|
||||
|
||||
def __repr__(self):
|
||||
return 'FixedType({!r}, {!r})'.format(self.size, self.struct_code)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (type(other) is FixedType) and (self.size == other.size) \
|
||||
and (self.struct_code == other.struct_code)
|
||||
|
||||
|
||||
class Boolean(FixedType):
|
||||
def __init__(self):
|
||||
super().__init__(4, 'I') # D-Bus booleans take 4 bytes
|
||||
|
||||
def parse_data(self, buf, pos, endianness, fds=()):
|
||||
val, new_pos = super().parse_data(buf, pos, endianness)
|
||||
return bool(val), new_pos
|
||||
|
||||
def __repr__(self):
|
||||
return 'Boolean()'
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(other) is Boolean
|
||||
|
||||
|
||||
class FileDescriptor(FixedType):
|
||||
def __init__(self):
|
||||
super().__init__(4, 'I')
|
||||
|
||||
def parse_data(self, buf, pos, endianness, fds=()):
|
||||
idx, new_pos = super().parse_data(buf, pos, endianness)
|
||||
return fds[idx], new_pos
|
||||
|
||||
def serialise(self, data, pos, endianness, fds=None):
|
||||
if fds is None:
|
||||
raise RuntimeError("Sending FDs is not supported or not enabled")
|
||||
|
||||
if hasattr(data, 'fileno'):
|
||||
data = data.fileno()
|
||||
if isinstance(data, bool) or not isinstance(data, int):
|
||||
raise TypeError("Cannot use {data!r} as file descriptor. Expected "
|
||||
"an int or an object with fileno() method")
|
||||
|
||||
if data < 0:
|
||||
raise ValueError(f"File descriptor can't be negative ({data})")
|
||||
|
||||
fds.append(data)
|
||||
return super().serialise(len(fds) - 1, pos, endianness)
|
||||
|
||||
def __repr__(self):
|
||||
return 'FileDescriptor()'
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(other) is FileDescriptor
|
||||
|
||||
|
||||
simple_types = {
|
||||
'y': FixedType(1, 'B'), # unsigned 8 bit
|
||||
'n': FixedType(2, 'h'), # signed 16 bit
|
||||
'q': FixedType(2, 'H'), # unsigned 16 bit
|
||||
'b': Boolean(), # bool (32-bit)
|
||||
'i': FixedType(4, 'i'), # signed 32-bit
|
||||
'u': FixedType(4, 'I'), # unsigned 32-bit
|
||||
'x': FixedType(8, 'q'), # signed 64-bit
|
||||
't': FixedType(8, 'Q'), # unsigned 64-bit
|
||||
'd': FixedType(8, 'd'), # double
|
||||
'h': FileDescriptor(), # file descriptor (uint32 index in a separate list)
|
||||
}
|
||||
|
||||
|
||||
class StringType:
|
||||
def __init__(self, length_type):
|
||||
self.length_type = length_type
|
||||
|
||||
@property
|
||||
def alignment(self):
|
||||
return self.length_type.size
|
||||
|
||||
def parse_data(self, buf, pos, endianness, fds=()):
|
||||
length, pos = self.length_type.parse_data(buf, pos, endianness)
|
||||
end = pos + length
|
||||
val = buf[pos:end].decode('utf-8')
|
||||
assert buf[end:end + 1] == b'\0'
|
||||
return val, end + 1
|
||||
|
||||
def serialise(self, data, pos, endianness, fds=None):
|
||||
if not isinstance(data, str):
|
||||
raise TypeError("Expected str, not {!r}".format(data))
|
||||
encoded = data.encode('utf-8')
|
||||
len_data = self.length_type.serialise(len(encoded), pos, endianness)
|
||||
return len_data + encoded + b'\0'
|
||||
|
||||
def __repr__(self):
|
||||
return 'StringType({!r})'.format(self.length_type)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (type(other) is StringType) \
|
||||
and (self.length_type == other.length_type)
|
||||
|
||||
|
||||
simple_types.update({
|
||||
's': StringType(simple_types['u']), # String
|
||||
'o': StringType(simple_types['u']), # Object path
|
||||
'g': StringType(simple_types['y']), # Signature
|
||||
})
|
||||
|
||||
|
||||
class Struct:
|
||||
alignment = 8
|
||||
|
||||
def __init__(self, fields):
|
||||
if any(isinstance(f, DictEntry) for f in fields):
|
||||
raise TypeError("Found dict entry outside array")
|
||||
self.fields = fields
|
||||
|
||||
def parse_data(self, buf, pos, endianness, fds=()):
|
||||
pos += padding(pos, 8)
|
||||
res = []
|
||||
for field in self.fields:
|
||||
v, pos = field.parse_data(buf, pos, endianness, fds=fds)
|
||||
res.append(v)
|
||||
return tuple(res), pos
|
||||
|
||||
def serialise(self, data, pos, endianness, fds=None):
|
||||
if not isinstance(data, tuple):
|
||||
raise TypeError("Expected tuple, not {!r}".format(data))
|
||||
if len(data) != len(self.fields):
|
||||
raise ValueError("{} entries for {} fields".format(
|
||||
len(data), len(self.fields)
|
||||
))
|
||||
pad = b'\0' * padding(pos, self.alignment)
|
||||
pos += len(pad)
|
||||
res_pieces = []
|
||||
for item, field in zip(data, self.fields):
|
||||
res_pieces.append(field.serialise(item, pos, endianness, fds=fds))
|
||||
pos += len(res_pieces[-1])
|
||||
return pad + b''.join(res_pieces)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({!r})".format(type(self).__name__, self.fields)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (type(other) is type(self)) and (self.fields == other.fields)
|
||||
|
||||
|
||||
class DictEntry(Struct):
|
||||
def __init__(self, fields):
|
||||
if len(fields) != 2:
|
||||
raise TypeError(
|
||||
"Dict entry must have 2 fields, not %d" % len(fields))
|
||||
if not isinstance(fields[0], (FixedType, StringType)):
|
||||
raise TypeError(
|
||||
"First field in dict entry must be simple type, not {}"
|
||||
.format(type(fields[0])))
|
||||
super().__init__(fields)
|
||||
|
||||
class Array:
|
||||
alignment = 4
|
||||
length_type = FixedType(4, 'I')
|
||||
|
||||
def __init__(self, elt_type):
|
||||
self.elt_type = elt_type
|
||||
|
||||
def parse_data(self, buf, pos, endianness, fds=()):
|
||||
# print('Array start', pos)
|
||||
length, pos = self.length_type.parse_data(buf, pos, endianness)
|
||||
pos += padding(pos, self.elt_type.alignment)
|
||||
end = pos + length
|
||||
if self.elt_type == simple_types['y']: # Array of bytes
|
||||
return buf[pos:end], end
|
||||
|
||||
res = []
|
||||
while pos < end:
|
||||
# print('Array elem', pos)
|
||||
v, pos = self.elt_type.parse_data(buf, pos, endianness, fds=fds)
|
||||
res.append(v)
|
||||
if isinstance(self.elt_type, DictEntry):
|
||||
# Convert list of 2-tuples to dict
|
||||
res = dict(res)
|
||||
return res, pos
|
||||
|
||||
def serialise(self, data, pos, endianness, fds=None):
|
||||
data_is_bytes = False
|
||||
if isinstance(self.elt_type, DictEntry) and isinstance(data, dict):
|
||||
data = data.items()
|
||||
elif (self.elt_type == simple_types['y']) and isinstance(data, bytes):
|
||||
data_is_bytes = True
|
||||
elif not isinstance(data, list):
|
||||
raise TypeError("Not suitable for array: {!r}".format(data))
|
||||
|
||||
# Fail fast if we know in advance that the data is too big:
|
||||
if isinstance(self.elt_type, FixedType):
|
||||
if (self.elt_type.size * len(data)) > 2**26:
|
||||
raise SizeLimitError("Array size exceeds 64 MiB limit")
|
||||
|
||||
pad1 = padding(pos, self.alignment)
|
||||
pos_after_length = pos + pad1 + 4
|
||||
pad2 = padding(pos_after_length, self.elt_type.alignment)
|
||||
|
||||
if data_is_bytes:
|
||||
buf = data
|
||||
else:
|
||||
data_pos = pos_after_length + pad2
|
||||
limit_pos = data_pos + 2 ** 26
|
||||
chunks = []
|
||||
for item in data:
|
||||
chunks.append(self.elt_type.serialise(
|
||||
item, data_pos, endianness, fds=fds
|
||||
))
|
||||
data_pos += len(chunks[-1])
|
||||
if data_pos > limit_pos:
|
||||
raise SizeLimitError("Array size exceeds 64 MiB limit")
|
||||
buf = b''.join(chunks)
|
||||
|
||||
len_data = self.length_type.serialise(len(buf), pos+pad1, endianness)
|
||||
# print('Array ser: pad1={!r}, len_data={!r}, pad2={!r}, buf={!r}'.format(
|
||||
# pad1, len_data, pad2, buf))
|
||||
return (b'\0' * pad1) + len_data + (b'\0' * pad2) + buf
|
||||
|
||||
def __repr__(self):
|
||||
return 'Array({!r})'.format(self.elt_type)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (type(other) is Array) and (self.elt_type == other.elt_type)
|
||||
|
||||
|
||||
class Variant:
|
||||
alignment = 1
|
||||
|
||||
def parse_data(self, buf, pos, endianness, fds=()):
|
||||
# print('variant', pos)
|
||||
sig, pos = simple_types['g'].parse_data(buf, pos, endianness)
|
||||
# print('variant sig:', repr(sig), pos)
|
||||
valtype = parse_signature(list(sig))
|
||||
val, pos = valtype.parse_data(buf, pos, endianness, fds=fds)
|
||||
# print('variant done', (sig, val), pos)
|
||||
return (sig, val), pos
|
||||
|
||||
def serialise(self, data, pos, endianness, fds=None):
|
||||
sig, data = data
|
||||
valtype = parse_signature(list(sig))
|
||||
sig_buf = simple_types['g'].serialise(sig, pos, endianness)
|
||||
return sig_buf + valtype.serialise(
|
||||
data, pos + len(sig_buf), endianness, fds=fds
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Variant()'
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(other) is Variant
|
||||
|
||||
def parse_signature(sig):
|
||||
"""Parse a symbolic signature into objects.
|
||||
"""
|
||||
# Based on http://norvig.com/lispy.html
|
||||
token = sig.pop(0)
|
||||
if token == 'a':
|
||||
return Array(parse_signature(sig))
|
||||
if token == 'v':
|
||||
return Variant()
|
||||
elif token == '(':
|
||||
fields = []
|
||||
while sig[0] != ')':
|
||||
fields.append(parse_signature(sig))
|
||||
sig.pop(0) # )
|
||||
return Struct(fields)
|
||||
elif token == '{':
|
||||
de = []
|
||||
while sig[0] != '}':
|
||||
de.append(parse_signature(sig))
|
||||
sig.pop(0) # }
|
||||
return DictEntry(de)
|
||||
elif token in ')}':
|
||||
raise ValueError('Unexpected end of struct')
|
||||
else:
|
||||
return simple_types[token]
|
||||
|
||||
|
||||
def calc_msg_size(buf):
|
||||
endian, = struct.unpack('c', buf[:1])
|
||||
endian = endian_map[endian]
|
||||
body_length, = struct.unpack(endian.struct_code() + 'I', buf[4:8])
|
||||
fields_array_len, = struct.unpack(endian.struct_code() + 'I', buf[12:16])
|
||||
header_len = 16 + fields_array_len
|
||||
return header_len + padding(header_len, 8) + body_length
|
||||
|
||||
|
||||
_header_fields_type = Array(Struct([simple_types['y'], Variant()]))
|
||||
|
||||
|
||||
def parse_header_fields(buf, endianness):
|
||||
l, pos = _header_fields_type.parse_data(buf, 12, endianness)
|
||||
return {HeaderFields(k): v[1] for (k, v) in l}, pos
|
||||
|
||||
|
||||
header_field_codes = {
|
||||
1: 'o',
|
||||
2: 's',
|
||||
3: 's',
|
||||
4: 's',
|
||||
5: 'u',
|
||||
6: 's',
|
||||
7: 's',
|
||||
8: 'g',
|
||||
9: 'u',
|
||||
}
|
||||
|
||||
|
||||
def serialise_header_fields(d, endianness):
|
||||
l = [(i.value, (header_field_codes[i], v)) for (i, v) in sorted(d.items())]
|
||||
return _header_fields_type.serialise(l, 12, endianness)
|
||||
|
||||
|
||||
class Header:
|
||||
def __init__(self, endianness, message_type, flags, protocol_version,
|
||||
body_length, serial, fields):
|
||||
"""A D-Bus message header
|
||||
|
||||
It's not normally necessary to construct this directly: use higher level
|
||||
functions and methods instead.
|
||||
"""
|
||||
self.endianness = endianness
|
||||
self.message_type = MessageType(message_type)
|
||||
self.flags = MessageFlag(flags)
|
||||
self.protocol_version = protocol_version
|
||||
self.body_length = body_length
|
||||
self.serial = serial
|
||||
self.fields = fields
|
||||
|
||||
def __repr__(self):
|
||||
return 'Header({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, fields={!r})'.format(
|
||||
self.endianness, self.message_type, self.flags,
|
||||
self.protocol_version, self.body_length, self.serial, self.fields)
|
||||
|
||||
def serialise(self, serial=None):
|
||||
s = self.endianness.struct_code() + 'cBBBII'
|
||||
if serial is None:
|
||||
serial = self.serial
|
||||
return struct.pack(s, self.endianness.dbus_code(),
|
||||
self.message_type.value, self.flags,
|
||||
self.protocol_version,
|
||||
self.body_length, serial) \
|
||||
+ serialise_header_fields(self.fields, self.endianness)
|
||||
|
||||
@classmethod
|
||||
def from_buffer(cls, buf):
|
||||
endian, msgtype, flags, pv = struct.unpack('cBBB', buf[:4])
|
||||
endian = endian_map[endian]
|
||||
bodylen, serial = struct.unpack(endian.struct_code() + 'II', buf[4:12])
|
||||
fields, pos = parse_header_fields(buf, endian)
|
||||
return cls(endian, msgtype, flags, pv, bodylen, serial, fields), pos
|
||||
|
||||
|
||||
class Message:
|
||||
"""Object representing a DBus message.
|
||||
|
||||
It's not normally necessary to construct this directly: use higher level
|
||||
functions and methods instead.
|
||||
"""
|
||||
def __init__(self, header, body):
|
||||
self.header = header
|
||||
self.body = body
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({!r}, {!r})".format(type(self).__name__, self.header, self.body)
|
||||
|
||||
@classmethod
|
||||
def from_buffer(cls, buf: bytes, fds=()) -> 'Message':
|
||||
header, pos = Header.from_buffer(buf)
|
||||
n_fds = header.fields.get(HeaderFields.unix_fds, 0)
|
||||
if n_fds > len(fds):
|
||||
raise ValueError(
|
||||
f"Message expects {n_fds} FDs, but only {len(fds)} were received"
|
||||
)
|
||||
fds = fds[:n_fds]
|
||||
body = ()
|
||||
if HeaderFields.signature in header.fields:
|
||||
sig = header.fields[HeaderFields.signature]
|
||||
body_type = parse_signature(list('(%s)' % sig))
|
||||
body = body_type.parse_data(buf, pos, header.endianness, fds=fds)[0]
|
||||
return Message(header, body)
|
||||
|
||||
def serialise(self, serial=None, fds=None) -> bytes:
|
||||
"""Convert this message to bytes.
|
||||
|
||||
Specifying *serial* overrides the ``msg.header.serial`` field, so a
|
||||
connection can use its own serial number without modifying the message.
|
||||
|
||||
If file-descriptor support is in use, *fds* should be a
|
||||
:class:`array.array` object with type ``'i'``. Any file descriptors in
|
||||
the message will be added to the array. If the message contains FDs,
|
||||
it can't be serialised without this array.
|
||||
"""
|
||||
endian = self.header.endianness
|
||||
|
||||
if HeaderFields.signature in self.header.fields:
|
||||
sig = self.header.fields[HeaderFields.signature]
|
||||
body_type = parse_signature(list('(%s)' % sig))
|
||||
body_buf = body_type.serialise(self.body, 0, endian, fds=fds)
|
||||
else:
|
||||
body_buf = b''
|
||||
|
||||
self.header.body_length = len(body_buf)
|
||||
if fds:
|
||||
self.header.fields[HeaderFields.unix_fds] = len(fds)
|
||||
|
||||
header_buf = self.header.serialise(serial=serial)
|
||||
pad = b'\0' * padding(len(header_buf), 8)
|
||||
return header_buf + pad + body_buf
|
||||
|
||||
|
||||
class Parser:
|
||||
"""Parse DBus messages from a stream of incoming data.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.buf = BufferPipe()
|
||||
self.fds = []
|
||||
self.next_msg_size = None
|
||||
|
||||
def add_data(self, data: bytes, fds=()):
|
||||
"""Provide newly received data to the parser"""
|
||||
self.buf.write(data)
|
||||
self.fds.extend(fds)
|
||||
|
||||
def feed(self, data):
|
||||
"""Feed the parser newly read data.
|
||||
|
||||
Returns a list of messages completed by the new data.
|
||||
"""
|
||||
self.add_data(data)
|
||||
return list(iter(self.get_next_message, None))
|
||||
|
||||
def bytes_desired(self):
|
||||
"""How many bytes can be received without going beyond the next message?
|
||||
|
||||
This is only used with file-descriptor passing, so we don't get too many
|
||||
FDs in a single recvmsg call.
|
||||
"""
|
||||
got = self.buf.bytes_buffered
|
||||
if got < 16: # The first 16 bytes tell us the message size
|
||||
return 16 - got
|
||||
|
||||
if self.next_msg_size is None:
|
||||
self.next_msg_size = calc_msg_size(self.buf.peek(16))
|
||||
return self.next_msg_size - got
|
||||
|
||||
def get_next_message(self) -> Optional[Message]:
|
||||
"""Parse one message, if there is enough data.
|
||||
|
||||
Returns None if it doesn't have a complete message.
|
||||
"""
|
||||
if self.next_msg_size is None:
|
||||
if self.buf.bytes_buffered >= 16:
|
||||
self.next_msg_size = calc_msg_size(self.buf.peek(16))
|
||||
nms = self.next_msg_size
|
||||
if (nms is not None) and self.buf.bytes_buffered >= nms:
|
||||
raw_msg = self.buf.read(nms)
|
||||
msg = Message.from_buffer(raw_msg, fds=self.fds)
|
||||
self.next_msg_size = None
|
||||
fds_consumed = msg.header.fields.get(HeaderFields.unix_fds, 0)
|
||||
self.fds = self.fds[fds_consumed:]
|
||||
return msg
|
||||
|
||||
|
||||
class BufferPipe:
|
||||
"""A place to store received data until we can parse a complete message
|
||||
|
||||
The main difference from io.BytesIO is that read & write operate at
|
||||
opposite ends, like a pipe.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.chunks = deque()
|
||||
self.bytes_buffered = 0
|
||||
|
||||
def write(self, b: bytes):
|
||||
self.chunks.append(b)
|
||||
self.bytes_buffered += len(b)
|
||||
|
||||
def _peek_iter(self, nbytes: int):
|
||||
assert nbytes <= self.bytes_buffered
|
||||
for chunk in self.chunks:
|
||||
chunk = chunk[:nbytes]
|
||||
nbytes -= len(chunk)
|
||||
yield chunk
|
||||
if nbytes <= 0:
|
||||
break
|
||||
|
||||
def peek(self, nbytes: int) -> bytes:
|
||||
"""Get exactly nbytes bytes from the front without removing them"""
|
||||
return b''.join(self._peek_iter(nbytes))
|
||||
|
||||
def _read_iter(self, nbytes: int):
|
||||
assert nbytes <= self.bytes_buffered
|
||||
while True:
|
||||
chunk = self.chunks.popleft()
|
||||
self.bytes_buffered -= len(chunk)
|
||||
if nbytes <= len(chunk):
|
||||
break
|
||||
nbytes -= len(chunk)
|
||||
yield chunk
|
||||
|
||||
# Final chunk
|
||||
chunk, rem = chunk[:nbytes], chunk[nbytes:]
|
||||
if rem:
|
||||
self.chunks.appendleft(rem)
|
||||
self.bytes_buffered += len(rem)
|
||||
yield chunk
|
||||
|
||||
def read(self, nbytes: int) -> bytes:
|
||||
"""Take & return exactly nbytes bytes from the front"""
|
||||
return b''.join(self._read_iter(nbytes))
|
||||
@@ -0,0 +1,76 @@
|
||||
from warnings import warn
|
||||
|
||||
from .low_level import MessageType, HeaderFields
|
||||
from .wrappers import DBusErrorResponse
|
||||
|
||||
class Router:
|
||||
"""Routing for messages coming back to a client application.
|
||||
|
||||
:param handle_factory: Constructor for an object like asyncio.Future,
|
||||
with methods *set_result* and *set_exception*. Outgoing method call
|
||||
messages will get a handle associated with them.
|
||||
:param on_unhandled: Callback for messages not otherwise dispatched.
|
||||
"""
|
||||
def __init__(self, handle_factory, on_unhandled=None):
|
||||
self.handle_factory = handle_factory
|
||||
self._on_unhandled = on_unhandled
|
||||
self.outgoing_serial = 0
|
||||
self.awaiting_reply = {}
|
||||
self.signal_callbacks = {}
|
||||
|
||||
@property
|
||||
def on_unhandled(self):
|
||||
return self._on_unhandled
|
||||
|
||||
@on_unhandled.setter
|
||||
def on_unhandled(self, value):
|
||||
warn("Setting on_unhandled is deprecated. Please use the filter() "
|
||||
"method or simple receive() calls instead.", stacklevel=2)
|
||||
self._on_unhandled = value
|
||||
|
||||
def outgoing(self, msg):
|
||||
"""Set the serial number in the message & make a handle if a method call
|
||||
"""
|
||||
self.outgoing_serial += 1
|
||||
msg.header.serial = self.outgoing_serial
|
||||
|
||||
if msg.header.message_type is MessageType.method_call:
|
||||
self.awaiting_reply[msg.header.serial] = handle = self.handle_factory()
|
||||
return handle
|
||||
|
||||
def subscribe_signal(self, callback, path, interface, member):
|
||||
"""Add a callback for a signal.
|
||||
"""
|
||||
warn("The subscribe_signal() method is deprecated. "
|
||||
"Please use the filter() API instead.", stacklevel=2)
|
||||
self.signal_callbacks[(path, interface, member)] = callback
|
||||
|
||||
def incoming(self, msg):
|
||||
"""Route an incoming message.
|
||||
"""
|
||||
hdr = msg.header
|
||||
|
||||
# Signals:
|
||||
if hdr.message_type is MessageType.signal:
|
||||
key = (hdr.fields.get(HeaderFields.path, None),
|
||||
hdr.fields.get(HeaderFields.interface, None),
|
||||
hdr.fields.get(HeaderFields.member, None)
|
||||
)
|
||||
cb = self.signal_callbacks.get(key, None)
|
||||
if cb is not None:
|
||||
cb(msg.body)
|
||||
return
|
||||
|
||||
# Method returns & errors
|
||||
reply_serial = hdr.fields.get(HeaderFields.reply_serial, -1)
|
||||
reply_handle = self.awaiting_reply.pop(reply_serial, None)
|
||||
if reply_handle is not None:
|
||||
if hdr.message_type is MessageType.method_return:
|
||||
reply_handle.set_result(msg.body)
|
||||
return
|
||||
elif hdr.message_type is MessageType.error:
|
||||
reply_handle.set_exception(DBusErrorResponse(msg))
|
||||
return
|
||||
|
||||
if self.on_unhandled:
|
||||
self.on_unhandled(msg)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<!-- GDBus 2.50.3 -->
|
||||
<node>
|
||||
<interface name="org.freedesktop.DBus.Properties">
|
||||
<method name="Get">
|
||||
<arg type="s" name="interface_name" direction="in"/>
|
||||
<arg type="s" name="property_name" direction="in"/>
|
||||
<arg type="v" name="value" direction="out"/>
|
||||
</method>
|
||||
<method name="GetAll">
|
||||
<arg type="s" name="interface_name" direction="in"/>
|
||||
<arg type="a{sv}" name="properties" direction="out"/>
|
||||
</method>
|
||||
<method name="Set">
|
||||
<arg type="s" name="interface_name" direction="in"/>
|
||||
<arg type="s" name="property_name" direction="in"/>
|
||||
<arg type="v" name="value" direction="in"/>
|
||||
</method>
|
||||
<signal name="PropertiesChanged">
|
||||
<arg type="s" name="interface_name"/>
|
||||
<arg type="a{sv}" name="changed_properties"/>
|
||||
<arg type="as" name="invalidated_properties"/>
|
||||
</signal>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Introspectable">
|
||||
<method name="Introspect">
|
||||
<arg type="s" name="xml_data" direction="out"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Peer">
|
||||
<method name="Ping"/>
|
||||
<method name="GetMachineId">
|
||||
<arg type="s" name="machine_uuid" direction="out"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.Secret.Service">
|
||||
<method name="OpenSession">
|
||||
<arg type="s" name="algorithm" direction="in"/>
|
||||
<arg type="v" name="input" direction="in"/>
|
||||
<arg type="v" name="output" direction="out"/>
|
||||
<arg type="o" name="result" direction="out"/>
|
||||
</method>
|
||||
<method name="CreateCollection">
|
||||
<arg type="a{sv}" name="properties" direction="in"/>
|
||||
<arg type="s" name="alias" direction="in"/>
|
||||
<arg type="o" name="collection" direction="out"/>
|
||||
<arg type="o" name="prompt" direction="out"/>
|
||||
</method>
|
||||
<method name="SearchItems">
|
||||
<arg type="a{ss}" name="attributes" direction="in"/>
|
||||
<arg type="ao" name="unlocked" direction="out"/>
|
||||
<arg type="ao" name="locked" direction="out"/>
|
||||
</method>
|
||||
<method name="Unlock">
|
||||
<arg type="ao" name="objects" direction="in"/>
|
||||
<arg type="ao" name="unlocked" direction="out"/>
|
||||
<arg type="o" name="prompt" direction="out"/>
|
||||
</method>
|
||||
<method name="Lock">
|
||||
<arg type="ao" name="objects" direction="in"/>
|
||||
<arg type="ao" name="locked" direction="out"/>
|
||||
<arg type="o" name="Prompt" direction="out"/>
|
||||
</method>
|
||||
<method name="LockService"/>
|
||||
<method name="ChangeLock">
|
||||
<arg type="o" name="collection" direction="in"/>
|
||||
<arg type="o" name="prompt" direction="out"/>
|
||||
</method>
|
||||
<method name="GetSecrets">
|
||||
<arg type="ao" name="items" direction="in"/>
|
||||
<arg type="o" name="session" direction="in"/>
|
||||
<arg type="a{o(oayays)}" name="secrets" direction="out"/>
|
||||
</method>
|
||||
<method name="ReadAlias">
|
||||
<arg type="s" name="name" direction="in"/>
|
||||
<arg type="o" name="collection" direction="out"/>
|
||||
</method>
|
||||
<method name="SetAlias">
|
||||
<arg type="s" name="name" direction="in"/>
|
||||
<arg type="o" name="collection" direction="in"/>
|
||||
</method>
|
||||
<signal name="CollectionCreated">
|
||||
<arg type="o" name="collection"/>
|
||||
</signal>
|
||||
<signal name="CollectionDeleted">
|
||||
<arg type="o" name="collection"/>
|
||||
</signal>
|
||||
<signal name="CollectionChanged">
|
||||
<arg type="o" name="collection"/>
|
||||
</signal>
|
||||
<property type="ao" name="Collections" access="read"/>
|
||||
</interface>
|
||||
<interface name="org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface">
|
||||
<method name="ChangeWithMasterPassword">
|
||||
<arg type="o" name="collection" direction="in"/>
|
||||
<arg type="(oayays)" name="original" direction="in"/>
|
||||
<arg type="(oayays)" name="master" direction="in"/>
|
||||
</method>
|
||||
<method name="ChangeWithPrompt">
|
||||
<arg type="o" name="collection" direction="in"/>
|
||||
<arg type="o" name="prompt" direction="out"/>
|
||||
</method>
|
||||
<method name="CreateWithMasterPassword">
|
||||
<arg type="a{sv}" name="attributes" direction="in"/>
|
||||
<arg type="(oayays)" name="master" direction="in"/>
|
||||
<arg type="o" name="collection" direction="out"/>
|
||||
</method>
|
||||
<method name="UnlockWithMasterPassword">
|
||||
<arg type="o" name="collection" direction="in"/>
|
||||
<arg type="(oayays)" name="master" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
<node name="session"/>
|
||||
<node name="collection"/>
|
||||
</node>
|
||||
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from jeepney import auth
|
||||
|
||||
def test_make_auth_external():
|
||||
b = auth.make_auth_external()
|
||||
assert b.startswith(b'AUTH EXTERNAL')
|
||||
|
||||
def test_make_auth_anonymous():
|
||||
b = auth.make_auth_anonymous()
|
||||
assert b.startswith(b'AUTH ANONYMOUS')
|
||||
|
||||
def test_parser():
|
||||
p = auth.SASLParser()
|
||||
p.feed(b'OK 728d62bc2eb394')
|
||||
assert not p.authenticated
|
||||
p.feed(b'1ebbb0b42958b1e0d6\r\n')
|
||||
assert p.authenticated
|
||||
|
||||
def test_parser_rejected():
|
||||
p = auth.SASLParser()
|
||||
with pytest.raises(auth.AuthenticationError):
|
||||
p.feed(b'REJECTED EXTERNAL\r\n')
|
||||
assert not p.authenticated
|
||||
@@ -0,0 +1,28 @@
|
||||
from io import StringIO
|
||||
import os.path
|
||||
|
||||
from jeepney.low_level import MessageType, HeaderFields
|
||||
from jeepney.bindgen import code_from_xml
|
||||
|
||||
sample_file = os.path.join(os.path.dirname(__file__), 'secrets_introspect.xml')
|
||||
|
||||
def test_bindgen():
|
||||
with open(sample_file) as f:
|
||||
xml = f.read()
|
||||
sio = StringIO()
|
||||
n_interfaces = code_from_xml(xml, path='/org/freedesktop/secrets',
|
||||
bus_name='org.freedesktop.secrets',
|
||||
fh=sio)
|
||||
# 5 interfaces defined, but we ignore Properties, Introspectable, Peer
|
||||
assert n_interfaces == 2
|
||||
|
||||
# Run the generated code, defining the message generator classes.
|
||||
binding_ns = {}
|
||||
exec(sio.getvalue(), binding_ns)
|
||||
Service = binding_ns['Service']
|
||||
|
||||
# Check basic functionality of the Service class
|
||||
assert Service.interface == 'org.freedesktop.Secret.Service'
|
||||
msg = Service().SearchItems({"service": "foo", "user": "bar"})
|
||||
assert msg.header.message_type is MessageType.method_call
|
||||
assert msg.header.fields[HeaderFields.destination] == 'org.freedesktop.secrets'
|
||||
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
from testpath import modified_env
|
||||
|
||||
from jeepney import bus
|
||||
|
||||
def test_get_connectable_addresses():
|
||||
a = list(bus.get_connectable_addresses('unix:path=/run/user/1000/bus'))
|
||||
assert a == ['/run/user/1000/bus']
|
||||
|
||||
a = list(bus.get_connectable_addresses('unix:abstract=/tmp/foo'))
|
||||
assert a == ['\0/tmp/foo']
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
list(bus.get_connectable_addresses('unix:tmpdir=/tmp'))
|
||||
|
||||
def test_get_bus():
|
||||
with modified_env({
|
||||
'DBUS_SESSION_BUS_ADDRESS':'unix:path=/run/user/1000/bus',
|
||||
'DBUS_SYSTEM_BUS_ADDRESS': 'unix:path=/var/run/dbus/system_bus_socket'
|
||||
}):
|
||||
assert bus.get_bus('SESSION') == '/run/user/1000/bus'
|
||||
assert bus.get_bus('SYSTEM') == '/var/run/dbus/system_bus_socket'
|
||||
|
||||
assert bus.get_bus('unix:path=/run/user/1002/bus') == '/run/user/1002/bus'
|
||||
@@ -0,0 +1,109 @@
|
||||
from jeepney import DBusAddress, new_signal, new_method_call
|
||||
from jeepney.bus_messages import MatchRule, message_bus
|
||||
|
||||
portal = DBusAddress(
|
||||
object_path='/org/freedesktop/portal/desktop',
|
||||
bus_name='org.freedesktop.portal.Desktop',
|
||||
)
|
||||
portal_req_iface = portal.with_interface('org.freedesktop.portal.Request')
|
||||
|
||||
|
||||
def test_match_rule_simple():
|
||||
rule = MatchRule(
|
||||
type='signal', interface='org.freedesktop.portal.Request',
|
||||
)
|
||||
assert rule.matches(new_signal(portal_req_iface, 'Response'))
|
||||
|
||||
# Wrong message type
|
||||
assert not rule.matches(new_method_call(portal_req_iface, 'Boo'))
|
||||
|
||||
# Wrong interface
|
||||
assert not rule.matches(new_signal(
|
||||
portal.with_interface('org.freedesktop.portal.FileChooser'), 'Response'
|
||||
))
|
||||
|
||||
|
||||
def test_match_rule_path_namespace():
|
||||
assert MatchRule(path_namespace='/org/freedesktop/portal').matches(
|
||||
new_signal(portal_req_iface, 'Response')
|
||||
)
|
||||
|
||||
# Prefix but not a parent in the path hierarchy
|
||||
assert not MatchRule(path_namespace='/org/freedesktop/por').matches(
|
||||
new_signal(portal_req_iface, 'Response')
|
||||
)
|
||||
|
||||
|
||||
def test_match_rule_arg():
|
||||
rule = MatchRule(type='method_call')
|
||||
rule.add_arg_condition(0, 'foo')
|
||||
|
||||
assert rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='s', body=('foo',)
|
||||
))
|
||||
|
||||
assert not rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='s', body=('foobar',)
|
||||
))
|
||||
|
||||
# No such argument
|
||||
assert not rule.matches(new_method_call(portal_req_iface, 'Boo'))
|
||||
|
||||
|
||||
def test_match_rule_arg_path():
|
||||
rule = MatchRule(type='method_call')
|
||||
rule.add_arg_condition(0, '/aa/bb/', kind='path')
|
||||
|
||||
# Exact match
|
||||
assert rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='s', body=('/aa/bb/',)
|
||||
))
|
||||
|
||||
# Match a prefix
|
||||
assert rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='s', body=('/aa/bb/cc',)
|
||||
))
|
||||
|
||||
# Argument is a prefix, ending with /
|
||||
assert rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='s', body=('/aa/',)
|
||||
))
|
||||
|
||||
# Argument is a prefix, but NOT ending with /
|
||||
assert not rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='s', body=('/aa',)
|
||||
))
|
||||
|
||||
assert not rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='s', body=('/aa/bb',)
|
||||
))
|
||||
|
||||
# Not a string
|
||||
assert not rule.matches(new_method_call(
|
||||
portal_req_iface, 'Boo', signature='u', body=(12,)
|
||||
))
|
||||
|
||||
|
||||
def test_match_rule_arg_namespace():
|
||||
rule = MatchRule(member='NameOwnerChanged')
|
||||
rule.add_arg_condition(0, 'com.example.backend1', kind='namespace')
|
||||
|
||||
# Exact match
|
||||
assert rule.matches(new_signal(
|
||||
message_bus, 'NameOwnerChanged', 's', ('com.example.backend1',)
|
||||
))
|
||||
|
||||
# Parent of the name
|
||||
assert rule.matches(new_signal(
|
||||
message_bus, 'NameOwnerChanged', 's', ('com.example.backend1.foo.bar',)
|
||||
))
|
||||
|
||||
# Prefix but not a parent in the namespace
|
||||
assert not rule.matches(new_signal(
|
||||
message_bus, 'NameOwnerChanged', 's', ('com.example.backend12',)
|
||||
))
|
||||
|
||||
# Not a string
|
||||
assert not rule.matches(new_signal(
|
||||
message_bus, 'NameOwnerChanged', 'u', (1,)
|
||||
))
|
||||
@@ -0,0 +1,80 @@
|
||||
import errno
|
||||
import os
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from jeepney import FileDescriptor, NoFDError
|
||||
|
||||
def assert_not_fd(fd: int):
|
||||
"""Check that the given number is not open as a file descriptor"""
|
||||
with pytest.raises(OSError) as exc_info:
|
||||
os.stat(fd)
|
||||
assert exc_info.value.errno == errno.EBADF
|
||||
|
||||
|
||||
def test_close(tmp_path):
|
||||
fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR)
|
||||
|
||||
with FileDescriptor(fd) as wfd:
|
||||
assert wfd.fileno() == fd
|
||||
# Leaving the with block is equivalent to calling .close()
|
||||
|
||||
assert 'closed' in repr(wfd)
|
||||
with pytest.raises(NoFDError):
|
||||
wfd.fileno()
|
||||
|
||||
assert_not_fd(fd)
|
||||
|
||||
|
||||
def test_to_raw_fd(tmp_path):
|
||||
fd = os.open(tmp_path / 'a', os.O_CREAT)
|
||||
wfd = FileDescriptor(fd)
|
||||
assert wfd.fileno() == fd
|
||||
|
||||
assert wfd.to_raw_fd() == fd
|
||||
|
||||
try:
|
||||
assert 'converted' in repr(wfd)
|
||||
with pytest.raises(NoFDError):
|
||||
wfd.fileno()
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def test_to_file(tmp_path):
|
||||
fd = os.open(tmp_path / 'a', os.O_CREAT | os.O_RDWR)
|
||||
wfd = FileDescriptor(fd)
|
||||
|
||||
with wfd.to_file('w') as f:
|
||||
assert f.write('abc')
|
||||
|
||||
assert 'converted' in repr(wfd)
|
||||
with pytest.raises(NoFDError):
|
||||
wfd.fileno()
|
||||
|
||||
assert_not_fd(fd) # Check FD was closed by file object
|
||||
|
||||
assert (tmp_path / 'a').read_text() == 'abc'
|
||||
|
||||
|
||||
def test_to_socket():
|
||||
s1, s2 = socket.socketpair()
|
||||
try:
|
||||
s1.sendall(b'abcd')
|
||||
sfd = s2.detach()
|
||||
wfd = FileDescriptor(sfd)
|
||||
|
||||
with wfd.to_socket() as sock:
|
||||
b = sock.recv(16)
|
||||
assert b and b'abcd'.startswith(b)
|
||||
|
||||
assert 'converted' in repr(wfd)
|
||||
with pytest.raises(NoFDError):
|
||||
wfd.fileno()
|
||||
|
||||
assert_not_fd(sfd) # Check FD was closed by socket object
|
||||
finally:
|
||||
s1.close()
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import pytest
|
||||
from jeepney.low_level import *
|
||||
|
||||
HELLO_METHOD_CALL = (
|
||||
b'l\x01\x00\x01\x00\x00\x00\x00\x01\x00\x00\x00m\x00\x00\x00\x01\x01o\x00\x15'
|
||||
b'\x00\x00\x00/org/freedesktop/DBus\x00\x00\x00\x02\x01s\x00\x14\x00\x00\x00'
|
||||
b'org.freedesktop.DBus\x00\x00\x00\x00\x03\x01s\x00\x05\x00\x00\x00Hello\x00'
|
||||
b'\x00\x00\x06\x01s\x00\x14\x00\x00\x00org.freedesktop.DBus\x00\x00\x00\x00')
|
||||
|
||||
|
||||
def test_parser_simple():
|
||||
msg = Parser().feed(HELLO_METHOD_CALL)[0]
|
||||
assert msg.header.fields[HeaderFields.member] == 'Hello'
|
||||
|
||||
def chunks(src, size):
|
||||
pos = 0
|
||||
while pos < len(src):
|
||||
end = pos + size
|
||||
yield src[pos:end]
|
||||
pos = end
|
||||
|
||||
def test_parser_chunks():
|
||||
p = Parser()
|
||||
chunked = list(chunks(HELLO_METHOD_CALL, 16))
|
||||
for c in chunked[:-1]:
|
||||
assert p.feed(c) == []
|
||||
msg = p.feed(chunked[-1])[0]
|
||||
assert msg.header.fields[HeaderFields.member] == 'Hello'
|
||||
|
||||
def test_multiple():
|
||||
msgs = Parser().feed(HELLO_METHOD_CALL * 6)
|
||||
assert len(msgs) == 6
|
||||
for msg in msgs:
|
||||
assert msg.header.fields[HeaderFields.member] == 'Hello'
|
||||
|
||||
def test_roundtrip():
|
||||
msg = Parser().feed(HELLO_METHOD_CALL)[0]
|
||||
assert msg.serialise() == HELLO_METHOD_CALL
|
||||
|
||||
def test_serialise_dict():
|
||||
data = {
|
||||
'a': 'b',
|
||||
'de': 'f',
|
||||
}
|
||||
string_type = simple_types['s']
|
||||
sig = Array(DictEntry([string_type, string_type]))
|
||||
print(sig.serialise(data, 0, Endianness.little))
|
||||
assert sig.serialise(data, 0, Endianness.little) == (
|
||||
b'\x1e\0\0\0' + # Length
|
||||
b'\0\0\0\0' + # Padding
|
||||
b'\x01\0\0\0a\0\0\0' +
|
||||
b'\x01\0\0\0b\0\0\0' +
|
||||
b'\x02\0\0\0de\0\0' +
|
||||
b'\x01\0\0\0f\0'
|
||||
)
|
||||
|
||||
def test_parse_signature():
|
||||
sig = parse_signature(list('(a{sv}(oayays)b)'))
|
||||
print(sig)
|
||||
assert sig == Struct([
|
||||
Array(DictEntry([simple_types['s'], Variant()])),
|
||||
Struct([
|
||||
simple_types['o'],
|
||||
Array(simple_types['y']),
|
||||
Array(simple_types['y']),
|
||||
simple_types['s']
|
||||
]),
|
||||
simple_types['b'],
|
||||
])
|
||||
|
||||
class fake_list(list):
|
||||
def __init__(self, n):
|
||||
super().__init__()
|
||||
self._n = n
|
||||
|
||||
def __len__(self):
|
||||
return self._n
|
||||
|
||||
def __iter__(self):
|
||||
return iter(range(self._n))
|
||||
|
||||
def test_array_limit():
|
||||
# The spec limits arrays to 64 MiB
|
||||
a = Array(FixedType(8, 'Q')) # 'at' - array of uint64
|
||||
a.serialise(fake_list(100), 0, Endianness.little)
|
||||
with pytest.raises(SizeLimitError):
|
||||
a.serialise(fake_list(2**23 + 1), 0, Endianness.little)
|
||||
@@ -0,0 +1,32 @@
|
||||
from asyncio import Future
|
||||
import pytest
|
||||
|
||||
from jeepney.routing import Router
|
||||
from jeepney.wrappers import new_method_return, new_error, DBusErrorResponse
|
||||
from jeepney.bus_messages import message_bus
|
||||
|
||||
def test_message_reply():
|
||||
router = Router(Future)
|
||||
call = message_bus.Hello()
|
||||
future = router.outgoing(call)
|
||||
router.incoming(new_method_return(call, 's', ('test',)))
|
||||
assert future.result() == ('test',)
|
||||
|
||||
def test_error():
|
||||
router = Router(Future)
|
||||
call = message_bus.Hello()
|
||||
future = router.outgoing(call)
|
||||
router.incoming(new_error(call, 'TestError', 'u', (31,)))
|
||||
with pytest.raises(DBusErrorResponse) as e:
|
||||
future.result()
|
||||
assert e.value.name == 'TestError'
|
||||
assert e.value.data == (31,)
|
||||
|
||||
def test_unhandled():
|
||||
unhandled = []
|
||||
router = Router(Future, on_unhandled=unhandled.append)
|
||||
msg = message_bus.Hello()
|
||||
router.incoming(msg)
|
||||
assert len(unhandled) == 1
|
||||
assert unhandled[0] == msg
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
from typing import Union
|
||||
from warnings import warn
|
||||
|
||||
from .low_level import *
|
||||
|
||||
__all__ = [
|
||||
'DBusAddress',
|
||||
'new_method_call',
|
||||
'new_method_return',
|
||||
'new_error',
|
||||
'new_signal',
|
||||
'MessageGenerator',
|
||||
'Properties',
|
||||
'Introspectable',
|
||||
'DBusErrorResponse',
|
||||
]
|
||||
|
||||
class DBusAddress:
|
||||
"""This identifies the object and interface a message is for.
|
||||
|
||||
e.g. messages to display desktop notifications would have this address::
|
||||
|
||||
DBusAddress('/org/freedesktop/Notifications',
|
||||
bus_name='org.freedesktop.Notifications',
|
||||
interface='org.freedesktop.Notifications')
|
||||
"""
|
||||
def __init__(self, object_path, bus_name=None, interface=None):
|
||||
self.object_path = object_path
|
||||
self.bus_name = bus_name
|
||||
self.interface = interface
|
||||
|
||||
def __repr__(self):
|
||||
return '{}({!r}, bus_name={!r}, interface={!r})'.format(type(self).__name__,
|
||||
self.object_path, self.bus_name, self.interface)
|
||||
|
||||
def with_interface(self, interface):
|
||||
return type(self)(self.object_path, self.bus_name, interface)
|
||||
|
||||
class DBusObject(DBusAddress):
|
||||
def __init__(self, object_path, bus_name=None, interface=None):
|
||||
super().__init__(object_path, bus_name, interface)
|
||||
warn('Deprecated alias, use DBusAddress instead', stacklevel=2)
|
||||
|
||||
def new_header(msg_type):
|
||||
return Header(Endianness.little, msg_type, flags=0, protocol_version=1,
|
||||
body_length=-1, serial=-1, fields={})
|
||||
|
||||
def new_method_call(remote_obj, method, signature=None, body=()):
|
||||
"""Construct a new method call message
|
||||
|
||||
This is a relatively low-level method. In many cases, this will be called
|
||||
from a :class:`MessageGenerator` subclass which provides a more convenient
|
||||
API.
|
||||
|
||||
:param DBusAddress remote_obj: The object to call a method on
|
||||
:param str method: The name of the method to call
|
||||
:param str signature: The DBus signature of the body data
|
||||
:param tuple body: Body data (i.e. method parameters)
|
||||
"""
|
||||
header = new_header(MessageType.method_call)
|
||||
header.fields[HeaderFields.path] = remote_obj.object_path
|
||||
if remote_obj.bus_name is None:
|
||||
raise ValueError("remote_obj.bus_name cannot be None for method calls")
|
||||
header.fields[HeaderFields.destination] = remote_obj.bus_name
|
||||
if remote_obj.interface is not None:
|
||||
header.fields[HeaderFields.interface] = remote_obj.interface
|
||||
header.fields[HeaderFields.member] = method
|
||||
if signature is not None:
|
||||
header.fields[HeaderFields.signature] = signature
|
||||
|
||||
return Message(header, body)
|
||||
|
||||
def new_method_return(parent_msg, signature=None, body=()):
|
||||
"""Construct a new response message
|
||||
|
||||
:param Message parent_msg: The method call this is a reply to
|
||||
:param str signature: The DBus signature of the body data
|
||||
:param tuple body: Body data
|
||||
"""
|
||||
header = new_header(MessageType.method_return)
|
||||
header.fields[HeaderFields.reply_serial] = parent_msg.header.serial
|
||||
sender = parent_msg.header.fields.get(HeaderFields.sender, None)
|
||||
if sender is not None:
|
||||
header.fields[HeaderFields.destination] = sender
|
||||
if signature is not None:
|
||||
header.fields[HeaderFields.signature] = signature
|
||||
return Message(header, body)
|
||||
|
||||
def new_error(parent_msg, error_name, signature=None, body=()):
|
||||
"""Construct a new error response message
|
||||
|
||||
:param Message parent_msg: The method call this is a reply to
|
||||
:param str error_name: The name of the error
|
||||
:param str signature: The DBus signature of the body data
|
||||
:param tuple body: Body data
|
||||
"""
|
||||
header = new_header(MessageType.error)
|
||||
header.fields[HeaderFields.reply_serial] = parent_msg.header.serial
|
||||
header.fields[HeaderFields.error_name] = error_name
|
||||
sender = parent_msg.header.fields.get(HeaderFields.sender, None)
|
||||
if sender is not None:
|
||||
header.fields[HeaderFields.destination] = sender
|
||||
if signature is not None:
|
||||
header.fields[HeaderFields.signature] = signature
|
||||
return Message(header, body)
|
||||
|
||||
def new_signal(emitter, signal, signature=None, body=()):
|
||||
"""Construct a new signal message
|
||||
|
||||
:param DBusAddress emitter: The object sending the signal
|
||||
:param str signal: The name of the signal
|
||||
:param str signature: The DBus signature of the body data
|
||||
:param tuple body: Body data
|
||||
"""
|
||||
header = new_header(MessageType.signal)
|
||||
header.fields[HeaderFields.path] = emitter.object_path
|
||||
if emitter.interface is None:
|
||||
raise ValueError("emitter.interface cannot be None for signals")
|
||||
header.fields[HeaderFields.interface] = emitter.interface
|
||||
header.fields[HeaderFields.member] = signal
|
||||
if signature is not None:
|
||||
header.fields[HeaderFields.signature] = signature
|
||||
return Message(header, body)
|
||||
|
||||
|
||||
class MessageGenerator:
|
||||
"""Subclass this to define the methods available on a DBus interface.
|
||||
|
||||
jeepney.bindgen can automatically create subclasses using introspection.
|
||||
"""
|
||||
def __init__(self, object_path, bus_name):
|
||||
self.object_path = object_path
|
||||
self.bus_name = bus_name
|
||||
|
||||
def __repr__(self):
|
||||
return "{}({!r}, bus_name={!r})".format(type(self).__name__,
|
||||
self.object_path, self.bus_name)
|
||||
|
||||
|
||||
class ProxyBase:
|
||||
"""A proxy is an IO-aware wrapper around a MessageGenerator
|
||||
|
||||
Calling methods on a proxy object will send a message and wait for the
|
||||
reply. This is a base class for proxy implementations in jeepney.io.
|
||||
"""
|
||||
def __init__(self, msggen):
|
||||
self._msggen = msggen
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item.startswith('__'):
|
||||
raise AttributeError(item)
|
||||
|
||||
make_msg = getattr(self._msggen, item, None)
|
||||
if callable(make_msg):
|
||||
return self._method_call(make_msg)
|
||||
|
||||
raise AttributeError(item)
|
||||
|
||||
def _method_call(self, make_msg):
|
||||
raise NotImplementedError("Needs to be implemented in subclass")
|
||||
|
||||
class Properties:
|
||||
"""Build messages for accessing object properties
|
||||
|
||||
If a D-Bus object has multiple interfaces, each interface has its own
|
||||
set of properties.
|
||||
|
||||
This uses the standard DBus interface ``org.freedesktop.DBus.Properties``
|
||||
"""
|
||||
def __init__(self, obj: Union[DBusAddress, MessageGenerator]):
|
||||
self.obj = obj
|
||||
self.props_if = DBusAddress(obj.object_path, bus_name=obj.bus_name,
|
||||
interface='org.freedesktop.DBus.Properties')
|
||||
|
||||
def get(self, name):
|
||||
"""Get the value of the property *name*"""
|
||||
return new_method_call(self.props_if, 'Get', 'ss',
|
||||
(self.obj.interface, name))
|
||||
|
||||
def get_all(self):
|
||||
"""Get all property values for this interface"""
|
||||
return new_method_call(self.props_if, 'GetAll', 's',
|
||||
(self.obj.interface,))
|
||||
|
||||
def set(self, name, signature, value):
|
||||
"""Set the property *name* to *value* (with appropriate signature)"""
|
||||
return new_method_call(self.props_if, 'Set', 'ssv',
|
||||
(self.obj.interface, name, (signature, value)))
|
||||
|
||||
class Introspectable(MessageGenerator):
|
||||
interface = 'org.freedesktop.DBus.Introspectable'
|
||||
|
||||
def Introspect(self):
|
||||
"""Request D-Bus introspection XML for a remote object"""
|
||||
return new_method_call(self, 'Introspect')
|
||||
|
||||
class DBusErrorResponse(Exception):
|
||||
"""Raised by proxy method calls when the reply is an error message"""
|
||||
def __init__(self, msg):
|
||||
self.name = msg.header.fields.get(HeaderFields.error_name)
|
||||
self.data = msg.body
|
||||
|
||||
def __str__(self):
|
||||
return '[{}] {}'.format(self.name, self.data)
|
||||
|
||||
|
||||
def unwrap_msg(msg: Message):
|
||||
"""Get the body of a message, raising DBusErrorResponse for error messages
|
||||
|
||||
This is to be used with replies to method_call messages, which may be
|
||||
method_return or error.
|
||||
"""
|
||||
if msg.header.message_type == MessageType.error:
|
||||
raise DBusErrorResponse(msg)
|
||||
|
||||
return msg.body
|
||||
Reference in New Issue
Block a user