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

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)