This commit is contained in:
2024-11-29 18:15:30 +00:00
parent 40aade2d8e
commit bc9415586e
5298 changed files with 1938676 additions and 80 deletions

View File

@ -0,0 +1 @@
from .common import RouterClosed

View 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

View 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)

View 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")

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View 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

View File

@ -0,0 +1,3 @@
import os
have_session_bus = bool(os.environ.get('DBUS_SESSION_BUS_ADDRESS'))

View 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

View 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