351 lines
12 KiB
Python
351 lines
12 KiB
Python
"""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)
|