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