asd
This commit is contained in:
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
|
Reference in New Issue
Block a user