usse/funda-scraper/venv/lib/python3.10/site-packages/jeepney/io/asyncio.py

234 lines
7.4 KiB
Python

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