asd
This commit is contained in:
13
venv/lib/python3.12/site-packages/jeepney/__init__.py
Normal file
13
venv/lib/python3.12/site-packages/jeepney/__init__.py
Normal file
@ -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'
|
136
venv/lib/python3.12/site-packages/jeepney/auth.py
Normal file
136
venv/lib/python3.12/site-packages/jeepney/auth.py
Normal file
@ -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
venv/lib/python3.12/site-packages/jeepney/bindgen.py
Normal file
126
venv/lib/python3.12/site-packages/jeepney/bindgen.py
Normal file
@ -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
venv/lib/python3.12/site-packages/jeepney/bus.py
Normal file
62
venv/lib/python3.12/site-packages/jeepney/bus.py
Normal file
@ -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())
|
235
venv/lib/python3.12/site-packages/jeepney/bus_messages.py
Normal file
235
venv/lib/python3.12/site-packages/jeepney/bus_messages.py
Normal file
@ -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
venv/lib/python3.12/site-packages/jeepney/fds.py
Normal file
158
venv/lib/python3.12/site-packages/jeepney/fds.py
Normal file
@ -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
|
1
venv/lib/python3.12/site-packages/jeepney/io/__init__.py
Normal file
1
venv/lib/python3.12/site-packages/jeepney/io/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .common import RouterClosed
|
233
venv/lib/python3.12/site-packages/jeepney/io/asyncio.py
Normal file
233
venv/lib/python3.12/site-packages/jeepney/io/asyncio.py
Normal file
@ -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
|
350
venv/lib/python3.12/site-packages/jeepney/io/blocking.py
Normal file
350
venv/lib/python3.12/site-packages/jeepney/io/blocking.py
Normal file
@ -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)
|
88
venv/lib/python3.12/site-packages/jeepney/io/common.py
Normal file
88
venv/lib/python3.12/site-packages/jeepney/io/common.py
Normal file
@ -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")
|
@ -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
|
114
venv/lib/python3.12/site-packages/jeepney/io/tests/test_trio.py
Normal file
114
venv/lib/python3.12/site-packages/jeepney/io/tests/test_trio.py
Normal file
@ -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'))
|
273
venv/lib/python3.12/site-packages/jeepney/io/threading.py
Normal file
273
venv/lib/python3.12/site-packages/jeepney/io/threading.py
Normal file
@ -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
venv/lib/python3.12/site-packages/jeepney/io/trio.py
Normal file
420
venv/lib/python3.12/site-packages/jeepney/io/trio.py
Normal file
@ -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
|
585
venv/lib/python3.12/site-packages/jeepney/low_level.py
Normal file
585
venv/lib/python3.12/site-packages/jeepney/low_level.py
Normal file
@ -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))
|
76
venv/lib/python3.12/site-packages/jeepney/routing.py
Normal file
76
venv/lib/python3.12/site-packages/jeepney/routing.py
Normal file
@ -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)
|
@ -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>
|
24
venv/lib/python3.12/site-packages/jeepney/tests/test_auth.py
Normal file
24
venv/lib/python3.12/site-packages/jeepney/tests/test_auth.py
Normal file
@ -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'
|
24
venv/lib/python3.12/site-packages/jeepney/tests/test_bus.py
Normal file
24
venv/lib/python3.12/site-packages/jeepney/tests/test_bus.py
Normal file
@ -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,)
|
||||
))
|
80
venv/lib/python3.12/site-packages/jeepney/tests/test_fds.py
Normal file
80
venv/lib/python3.12/site-packages/jeepney/tests/test_fds.py
Normal file
@ -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
|
||||
|
216
venv/lib/python3.12/site-packages/jeepney/wrappers.py
Normal file
216
venv/lib/python3.12/site-packages/jeepney/wrappers.py
Normal file
@ -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