Exemple #1
0
class ClientSocket(QObject):
    """
    A class wrapping a Python socket and integrated into the Qt event loop.
    """
    def __init__(self, logger, parent=None):
        """
        Initializes the client socket.

        :param logger: the logger to user
        :param parent: the parent object
        """
        QObject.__init__(self, parent)
        self._logger = logger
        self._socket = None

        self._read_buffer = b''
        self._read_notifier = None

        self._write_buffer = b''
        self._write_notifier = None

        self._connected = False
        self._outgoing = collections.deque()
        self._incoming = collections.deque()
        self._container = None

    @staticmethod
    def _chunkify(bs, n=65535):
        """
        Creates chunks of a specified size from a bytes string.

        :param bs: the bytes
        :param n: the size of a chunk
        :return: generator of chunks
        """
        for i in range(0, len(bs), n):
            yield bs[i:i + n]

    @property
    def connected(self):
        """
        Returns if the socket is connected.

        :return: is connected?
        """
        return self._connected

    def connect(self, sock):
        """
        Wraps the socket with the current object.

        :param sock: the socket
        """
        self._read_notifier = QSocketNotifier(sock.fileno(),
                                              QSocketNotifier.Read, self)
        self._read_notifier.activated.connect(self._notify_read)
        self._read_notifier.setEnabled(True)

        self._write_notifier = QSocketNotifier(sock.fileno(),
                                               QSocketNotifier.Write, self)
        self._write_notifier.activated.connect(self._notify_write)
        self._write_notifier.setEnabled(False)

        self._socket = sock
        self._connected = True

    def disconnect(self, err=None):
        """
        Terminates the current connection.

        :param err: the reason or None
        """
        if not self._socket:
            return
        if err:
            self._logger.warning("Connection lost")
            self._logger.exception(err)
        self._read_notifier.setEnabled(False)
        self._write_notifier.setEnabled(False)
        try:
            self._socket.close()
        except socket.error:
            pass
        self._socket = None
        self._connected = False

    def _notify_read(self):
        """
        Callback called when some data is ready to be read on the socket.
        """
        while True:
            try:
                data = self._socket.recv(4096)
            except socket.error as e:
                if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
                    self.disconnect(e)
                break
            if not data:
                break
            self._incoming.append(data)
        if self._incoming:
            QCoreApplication.instance().postEvent(self, PacketEvent())

    def _notify_write(self):
        """
        Callback called when some data is ready to written on the socket.
        """
        while True:
            if not self._write_buffer:
                if not self._outgoing:
                    break
                data = self._outgoing.popleft()
                if not data:
                    continue
                self._write_buffer = data
            try:
                count = self._socket.send(self._write_buffer)
            except socket.error as e:
                if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
                    self.disconnect(e)
                break
            self._write_buffer = self._write_buffer[count:]
        if not self._write_buffer:
            self._write_notifier.setEnabled(False)

    def event(self, event):
        """
        Callback called when a Qt event is fired.

        :param event: the event
        :return: was the event handled?
        """
        if isinstance(event, PacketEvent):
            self._dispatch()
            event.accept()
            return True
        else:
            event.ignore()
            return False

    def _dispatch(self):
        """
        Callback called when a packet event is fired.
        """
        while self._incoming:
            data = self._incoming.popleft()
            self._read_raw(data)

    def _read_raw(self, data):
        """
        Reads some raw from the underlying socket.

        :param data: the raw bytes
        """
        self._read_buffer += data

        while b'\n' in self._read_buffer and not self._container:
            lines = self._read_buffer.split(b'\n')
            self._read_buffer = b'\n'.join(lines[1:])
            self._read_line(lines[0])

        if self._container:
            # Append raw data to content already received
            if self._container.downback:  # trigger download callback
                self._container.downback(len(self._read_buffer),
                                         len(self._container))
            if len(self._read_buffer) >= len(self._container):
                content = self._read_buffer[:len(self._container)]
                self._read_buffer = self._read_buffer[len(content):]
                self._container.content = content
                self._handle_packet(self._container)
                self._container = None

    def _write_raw(self, data):
        """
        Writes some raw bytes to the underlying socket.

        :param data: the raw bytes
        """
        if not self._socket:
            return
        self._outgoing.append(data)
        if not self._write_notifier.isEnabled():
            self._write_notifier.setEnabled(True)

    def _read_line(self, line):
        """
        Reads a line from the underlying socket.

        :param line: the line
        """
        # Try to parse the line as a packet
        try:
            dct = json.loads(line.decode('utf-8'))
            packet = Packet.parse_packet(dct)
        except Exception as e:
            self._logger.warning("Invalid packet received: %s" % line)
            self._logger.exception(e)
            return

        # Wait for raw data if it is a container
        if isinstance(packet, Container):
            self._container = packet
            return  # do not go any further

        self._handle_packet(packet)

    def _write_line(self, line):
        """
        Writes a line to the underlying socket.

        :param line: the line
        """
        self._write_raw(line.encode('utf-8') + b'\n')

    def _handle_packet(self, packet):
        """
        Handle an incoming packet (used for replies).

        :param packet: the packet
        """
        self._logger.debug("Received packet: %s" % packet)

        # Notify for replies
        if isinstance(packet, Reply):
            packet.trigger_callback()

        # Otherwise forward to the subclass
        elif not self.recv_packet(packet):
            self._logger.warning("Unhandled packet received: %s" % packet)

    def send_packet(self, packet):
        """
        Sends a packet the other party.

        :param packet: the packet
        :return: a packet deferred if a reply is expected
        """
        if not self._connected:
            self._logger.warning("Sending packet while disconnected")
            return None

        # Try to build then sent the line
        try:
            line = json.dumps(packet.build_packet())
            self._write_line(line)
        except Exception as e:
            self._logger.warning("Invalid packet being sent: %s" % packet)
            self._logger.exception(e)

        self._logger.debug("Sending packet: %s" % packet)

        # Write raw data for containers
        if isinstance(packet, Container):
            data = packet.content
            count, total = 0, len(data)
            for chunk in self._chunkify(data):
                self._write_raw(chunk)
                count += len(chunk)
                if packet.upback:  # trigger upload callback
                    packet.upback(count, total)

        # Queries return a packet deferred
        if isinstance(packet, Query):
            d = PacketDeferred()
            packet.register_callback(d)
            return d
        return None

    def recv_packet(self, packet):
        """
        Receives a packet from the other party.

        :param packet: the packet
        :return: has the packet been handled?
        """
        raise NotImplementedError("recv_packet() not implemented")
Exemple #2
0
class ClientSocket(QObject):
    """
    This class is acts a bridge between a client socket and the Qt event loop.
    By using a QSocketNotifier, we can be notified when some data is ready to
    be read or written on the socket, not requiring an extra thread.
    """

    MAX_DATA_SIZE = 65535

    def __init__(self, logger, parent=None):
        QObject.__init__(self, parent)
        self._logger = logger
        self._socket = None
        self._server = parent and isinstance(parent, ServerSocket)

        self._read_buffer = bytearray()
        self._read_notifier = None
        self._read_packet = None

        self._write_buffer = bytearray()
        self._write_notifier = None
        self._write_packet = None

        self._connected = False
        self._outgoing = collections.deque()
        self._incoming = collections.deque()

    @property
    def connected(self):
        """Is the underlying socket connected?"""
        return self._connected

    def wrap_socket(self, sock):
        """Sets the underlying socket to use."""
        self._read_notifier = QSocketNotifier(sock.fileno(),
                                              QSocketNotifier.Read, self)
        self._read_notifier.activated.connect(self._notify_read)
        self._read_notifier.setEnabled(True)

        self._write_notifier = QSocketNotifier(sock.fileno(),
                                               QSocketNotifier.Write, self)
        self._write_notifier.activated.connect(self._notify_write)
        self._write_notifier.setEnabled(True)

        self._socket = sock

    def disconnect(self, err=None):
        """Terminates the current connection."""
        if not self._socket:
            return

        self._logger.debug("Disconnected")
        if err:
            self._logger.exception(err)
        self._read_notifier.setEnabled(False)
        self._write_notifier.setEnabled(False)
        try:
            self._socket.shutdown(socket.SHUT_RDWR)
            self._socket.close()
        except socket.error:
            pass
        self._socket = None
        self._connected = False

    def set_keep_alive(self, cnt, intvl, idle):
        """
        Set the TCP keep-alive of the underlying socket.

        It activates after idle seconds of idleness, sends a keep-alive ping
        once every intvl seconds, and disconnects after `cnt`failed pings.
        """
        # Taken from https://github.com/markokr/skytools/
        tcp_keepcnt = getattr(socket, "TCP_KEEPCNT", None)
        tcp_keepintvl = getattr(socket, "TCP_KEEPINTVL", None)
        tcp_keepidle = getattr(socket, "TCP_KEEPIDLE", None)
        tcp_keepalive = getattr(socket, "TCP_KEEPALIVE", None)
        sio_keeplive_vals = getattr(socket, "SIO_KEEPALIVE_VALS", None)
        if (tcp_keepidle is None and tcp_keepalive is None
                and sys.platform == "darwin"):
            tcp_keepalive = 0x10

        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        if tcp_keepcnt is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, tcp_keepcnt, cnt)
        if tcp_keepintvl is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, tcp_keepintvl, intvl)
        if tcp_keepidle is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, tcp_keepidle, idle)
        elif tcp_keepalive is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, tcp_keepalive, idle)
        elif sio_keeplive_vals is not None:
            self._socket.ioctl(sio_keeplive_vals,
                               (1, idle * 1000, intvl * 1000))

    def _check_socket(self):
        """Check if the connection has been established yet."""
        # Ignore if you're already connected
        if self._connected:
            return True

        # Check if the connection was successful
        ret = self._socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
        if ret != 0 and ret != errno.EINPROGRESS and ret != errno.EWOULDBLOCK:
            self.disconnect(socket.error(ret, os.strerror(ret)))
            return False
        else:
            # Do SSL handshake if needed
            if isinstance(self._socket, ssl.SSLSocket):
                try:
                    self._socket.do_handshake()
                except socket.error as e:
                    if not isinstance(e,
                                      ssl.SSLWantReadError) and not isinstance(
                                          e, ssl.SSLWantReadError):
                        self.disconnect(e)
                    return False

            self._connected = True
            self._logger.debug("Connected")
            return True

    def _notify_read(self):
        """Callback called when some data is ready to be read on the socket."""
        if not self._check_socket():
            return

        # Read as many bytes as possible
        try:
            data = self._socket.recv(ClientSocket.MAX_DATA_SIZE)
            if not data:
                self.disconnect()
                return
        except socket.error as e:
            if (e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK)
                    and not isinstance(e, ssl.SSLWantReadError)
                    and not isinstance(e, ssl.SSLWantWriteError)):
                self.disconnect(e)
            return  # No more data available
        self._read_buffer.extend(data)

        # Split the received data on new lines (= packets)
        while True:
            if self._read_packet is None:
                if b"\n" in self._read_buffer:
                    pos = self._read_buffer.index(b"\n")
                    line = self._read_buffer[:pos]
                    self._read_buffer = self._read_buffer[pos +
                                                          1:  # noqa: E203
                                                          ]

                    # Try to parse the line (= packet)
                    try:
                        dct = json.loads(line.decode("utf-8"))
                        self._read_packet = Packet.parse_packet(
                            dct, self._server)
                    except Exception as e:
                        msg = "Invalid packet received: %s" % line
                        self._logger.warning(msg)
                        self._logger.exception(e)
                        continue
                else:
                    break  # Not enough data for a packet

            else:
                if isinstance(self._read_packet, Container):
                    avail = len(self._read_buffer)
                    total = self._read_packet.size

                    # Trigger the downback
                    if self._read_packet.downback:
                        self._read_packet.downback(min(avail, total), total)

                    # Read the container's content
                    if avail >= total:
                        self._read_packet.content = self._read_buffer[:total]
                        self._read_buffer = self._read_buffer[total:]
                    else:
                        break  # Not enough data for a packet

                self._incoming.append(self._read_packet)
                self._read_packet = None

        if self._incoming:
            QCoreApplication.instance().postEvent(self, PacketEvent())

    def _notify_write(self):
        """Callback called when some data is ready to written on the socket."""
        if not self._check_socket():
            return

        if not self._write_buffer:
            if not self._outgoing:
                return  # No more packets to send
            self._write_packet = self._outgoing.popleft()

            # Dump the packet as a line
            try:
                line = json.dumps(self._write_packet.build_packet())
                line = line.encode("utf-8") + b"\n"
            except Exception as e:
                msg = "Invalid packet being sent: %s" % self._write_packet
                self._logger.warning(msg)
                self._logger.exception(e)
                return

            # Write the container's content
            self._write_buffer.extend(bytearray(line))
            if isinstance(self._write_packet, Container):
                data = self._write_packet.content
                self._write_buffer.extend(bytearray(data))
                self._write_packet.size += len(line)

        # Send as many bytes as possible
        try:
            count = min(len(self._write_buffer), ClientSocket.MAX_DATA_SIZE)
            sent = self._socket.send(self._write_buffer[:count])
            self._write_buffer = self._write_buffer[sent:]
        except socket.error as e:
            if (e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK)
                    and not isinstance(e, ssl.SSLWantReadError)
                    and not isinstance(e, ssl.SSLWantWriteError)):
                self.disconnect(e)
            return  # Can't write anything

        # Trigger the upback
        if (isinstance(self._write_packet, Container)
                and self._write_packet.upback):
            self._write_packet.size -= count
            total = len(self._write_packet.content)
            sent = max(total - self._write_packet.size, 0)
            self._write_packet.upback(sent, total)

        if not self._write_buffer and not self._outgoing:
            self._write_notifier.setEnabled(False)

    def event(self, event):
        """Callback called when a Qt event is fired."""
        if isinstance(event, PacketEvent):
            self._dispatch()
            event.accept()
            return True
        else:
            event.ignore()
            return False

    def _dispatch(self):
        """Callback called when a PacketEvent is received."""
        while self._incoming:
            packet = self._incoming.popleft()
            self._logger.debug("Received packet: %s" % packet)

            # Notify for replies
            if isinstance(packet, Reply):
                packet.trigger_callback()

            # Otherwise forward to the subclass
            elif not self.recv_packet(packet):
                self._logger.warning("Unhandled packet received: %s" % packet)

    def send_packet(self, packet):
        """Sends a packet the other party."""
        if not self._connected:
            self._logger.warning("Sending packet while disconnected")
            return None

        self._logger.debug("Sending packet: %s" % packet)

        # Enqueue the packet
        self._outgoing.append(packet)
        if not self._write_notifier.isEnabled():
            self._write_notifier.setEnabled(True)

        # Queries return a packet deferred
        if isinstance(packet, Query):
            d = PacketDeferred()
            packet.register_callback(d)
            return d
        return None

    def recv_packet(self, packet):
        """Receives a packet from the other party."""
        raise NotImplementedError("recv_packet() not implemented")
Exemple #3
0
class ClientSocket(QObject):
    """
    A class wrapping a Python socket and integrated into the Qt event loop.
    """
    MAX_READ_SIZE = 4096
    MAX_WRITE_SIZE = 65535

    def __init__(self, logger, parent=None):
        """
        Initializes the client socket.

        :param logger: the logger to user
        """
        QObject.__init__(self, parent)
        self._logger = logger
        self._socket = None
        self._server = parent and isinstance(parent, ServerSocket)

        self._read_buffer = bytearray()
        self._read_notifier = None
        self._read_packet = None

        self._write_buffer = bytearray()
        self._write_notifier = None
        self._write_packet = None

        self._connected = False
        self._outgoing = collections.deque()
        self._incoming = collections.deque()

    @property
    def connected(self):
        """
        Returns if the socket is connected.

        :return: is connected?
        """
        return self._connected

    def connect(self, sock):
        """
        Wraps the socket with the current object.

        :param sock: the socket
        """
        self._read_notifier = QSocketNotifier(sock.fileno(),
                                              QSocketNotifier.Read, self)
        self._read_notifier.activated.connect(self._notify_read)
        self._read_notifier.setEnabled(True)

        self._write_notifier = QSocketNotifier(sock.fileno(),
                                               QSocketNotifier.Write, self)
        self._write_notifier.activated.connect(self._notify_write)
        self._write_notifier.setEnabled(False)

        self._socket = sock
        self._connected = True

    def disconnect(self, err=None):
        """
        Terminates the current connection.

        :param err: the reason or None
        """
        if not self._socket:
            return
        if err:
            self._logger.warning("Connection lost")
            self._logger.exception(err)
        self._read_notifier.setEnabled(False)
        self._write_notifier.setEnabled(False)
        try:
            self._socket.close()
        except socket.error:
            pass
        self._socket = None
        self._connected = False

    def set_keep_alive(self, cnt, intvl, idle):
        """
        Set the TCP keep-alive of the underlying socket.

         It activates after `idle` seconds of idleness, then sends a
         keep-alive ping once every `intvl` seconds, and closes the connection
         after `cnt` failed ping.
        """
        # Taken from https://github.com/markokr/skytools/
        TCP_KEEPCNT = getattr(socket, 'TCP_KEEPCNT', None)
        TCP_KEEPINTVL = getattr(socket, 'TCP_KEEPINTVL', None)
        TCP_KEEPIDLE = getattr(socket, 'TCP_KEEPIDLE', None)
        TCP_KEEPALIVE = getattr(socket, 'TCP_KEEPALIVE', None)
        SIO_KEEPALIVE_VALS = getattr(socket, 'SIO_KEEPALIVE_VALS', None)
        if TCP_KEEPIDLE is None and TCP_KEEPALIVE is None \
                and sys.platform == 'darwin':
            TCP_KEEPALIVE = 0x10

        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        if TCP_KEEPCNT is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPCNT, cnt)
        if TCP_KEEPINTVL is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPINTVL, intvl)
        if TCP_KEEPIDLE is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, idle)
        elif TCP_KEEPALIVE is not None:
            self._socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPALIVE, idle)
        elif SIO_KEEPALIVE_VALS is not None:
            self._socket.ioctl(SIO_KEEPALIVE_VALS,
                               (1, idle * 1000, intvl * 1000))

    def _notify_read(self):
        """
        Callback called when some data is ready to be read on the socket.
        """
        # Read as much data as is available
        while True:
            try:
                data = self._socket.recv(ClientSocket.MAX_READ_SIZE)
                if not data:
                    self.disconnect()
                    break
            except socket.error as e:
                if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK) \
                        and not isinstance(e, ssl.SSLWantReadError) \
                        and not isinstance(e, ssl.SSLWantWriteError):
                    self.disconnect(e)
                break  # No more data available
            self._read_buffer.extend(data)

        while True:
            if self._read_packet is None:
                if b'\n' in self._read_buffer:
                    pos = self._read_buffer.index(b'\n')
                    line = self._read_buffer[:pos]
                    self._read_buffer = self._read_buffer[pos + 1:]

                    # Try to parse the line as a packet
                    try:
                        dct = json.loads(line.decode('utf-8'))
                        self._read_packet = Packet.parse_packet(
                            dct, self._server)
                    except Exception as e:
                        msg = "Invalid packet received: %s" % line
                        self._logger.warning(msg)
                        self._logger.exception(e)
                        continue
                else:
                    break  # Not enough data for a packet

            else:
                if isinstance(self._read_packet, Container):
                    avail = len(self._read_buffer)
                    total = self._read_packet.size

                    # Trigger the downback
                    if self._read_packet.downback:
                        self._read_packet.downback(min(avail, total), total)

                    # Read the container's content
                    if avail >= total:
                        self._read_packet.content = self._read_buffer[:total]
                        self._read_buffer = self._read_buffer[total:]
                    else:
                        break  # Not enough data for a packet

                self._incoming.append(self._read_packet)
                self._read_packet = None

        if self._incoming:
            QCoreApplication.instance().postEvent(self, PacketEvent())

    def _notify_write(self):
        """
        Callback called when some data is ready to written on the socket.
        """
        while True:
            if not self._write_buffer:
                if not self._outgoing:
                    break  # No more packets to send
                self._write_packet = self._outgoing.popleft()

                try:
                    line = json.dumps(self._write_packet.build_packet())
                    line = line.encode('utf-8') + b'\n'
                except Exception as e:
                    msg = "Invalid packet being sent: %s" % self._write_packet
                    self._logger.warning(msg)
                    self._logger.exception(e)
                    continue

                # Write the container's content
                self._write_buffer.extend(bytearray(line))
                if isinstance(self._write_packet, Container):
                    data = self._write_packet.content
                    self._write_buffer.extend(bytearray(data))
                    self._write_packet.size += len(line)

            # Send as many bytes as possible
            try:
                count = min(len(self._write_buffer),
                            ClientSocket.MAX_WRITE_SIZE)
                sent = self._socket.send(self._write_buffer[:count])
                self._write_buffer = self._write_buffer[sent:]
            except socket.error as e:
                if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK) \
                        and not isinstance(e, ssl.SSLWantReadError) \
                        and not isinstance(e, ssl.SSLWantWriteError):
                    self.disconnect(e)
                break  # Can't write anything

            # Trigger the upback
            if isinstance(self._write_packet, Container) \
                    and self._write_packet.upback:
                self._write_packet.size -= count
                total = len(self._write_packet.content)
                sent = max(total - self._write_packet.size, 0)
                self._write_packet.upback(sent, total)
                break

        if not self._write_buffer:
            self._write_notifier.setEnabled(False)

    def event(self, event):
        """
        Callback called when a Qt event is fired.

        :param event: the event
        :return: was the event handled?
        """
        if isinstance(event, PacketEvent):
            self._dispatch()
            event.accept()
            return True
        else:
            event.ignore()
            return False

    def _dispatch(self):
        """
        Callback called when a packet event is fired.
        """
        while self._incoming:
            packet = self._incoming.popleft()
            self._logger.debug("Received packet: %s" % packet)

            # Notify for replies
            if isinstance(packet, Reply):
                packet.trigger_callback()

            # Otherwise forward to the subclass
            elif not self.recv_packet(packet):
                self._logger.warning("Unhandled packet received: %s" % packet)

    def send_packet(self, packet):
        """
        Sends a packet the other party.

        :param packet: the packet
        :return: a packet deferred if a reply is expected
        """
        if not self._connected:
            self._logger.warning("Sending packet while disconnected")
            return None

        self._logger.debug("Sending packet: %s" % packet)

        # Enqueue the packet
        self._outgoing.append(packet)
        if not self._write_notifier.isEnabled():
            self._write_notifier.setEnabled(True)

        # Queries return a packet deferred
        if isinstance(packet, Query):
            d = PacketDeferred()
            packet.register_callback(d)
            return d
        return None

    def recv_packet(self, packet):
        """
        Receives a packet from the other party.

        :param packet: the packet
        :return: has the packet been handled?
        """
        raise NotImplementedError("recv_packet() not implemented")