def __receive_thread_worker(self): waitobj = IOWait() waitobj.watch( self._socket, read=True) ### BMX poller = select.poll() ### BMX poller.register(self._socket, select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR) __buffer = b'' self._is_receiving = True self._receiving_callback and self._receiving_callback(self) while self._is_connected and self.__running: ### BMX events = poller.poll(1000) events = waitobj.wait(1000) ### BMX ### BMX for fd, event in events: for fileno, read, write in events: ### BMX ### BMX if event & select.POLLHUP: ### BMX self.logger.warning("Client socket closed") # Check if POLLIN event triggered ### BMX if event & (select.POLLIN | select.POLLPRI): if read: msg = self._socket.recv(4096) # Check if incoming message is not empty if msg: # If we transfer in text mode decode message to string if not self._binary: msg = str.rstrip(str(msg, 'utf-8')) # If we work in line mode (with a terminator) slice buffer into single chunks based on terminator if self.terminator: __buffer += msg while True: # terminator = int means fixed size chunks if isinstance(self.terminator, int): i = self.terminator if i > len(__buffer): break # terminator is str or bytes means search for it else: i = __buffer.find(self.terminator) if i == -1: break i += len(self.terminator) line = __buffer[:i] __buffer = __buffer[i:] if self._data_received_callback is not None: self._data_received_callback(self, line) # If not in terminator mode just forward what we received else: if self._data_received_callback is not None: self._data_received_callback(self, msg) # If empty peer has closed the connection else: # Peer connection closed self.logger.warning("Connection closed by peer {}".format(self._host)) self._is_connected = False ### BMX poller.unregister() waitobj.unwatch(self._socket) self._disconnected_callback and self._disconnected_callback(self) if self._autoreconnect: self.logger.debug("Autoreconnect enabled for {}".format(self._host)) self.connect() self._is_receiving = False
class Connections(Base): """ Within SmartHome.py there is one instance of this class The filenumber of a connection is the key to the contained dicts of _connections and _servers Additionally the filenumber is used for either epoll or kqueue depending on the environment found for select. A filenumber of value -1 is an error value. """ _connections = {} _servers = {} if hasattr(select, 'epoll'): _ro = select.EPOLLIN | select.EPOLLHUP | select.EPOLLERR _rw = _ro | select.EPOLLOUT def __init__(self): Base.__init__(self) Base._poller = self if hasattr(select, 'epoll'): self._epoll = select.epoll() elif hasattr(select, 'kqueue'): self._kqueue = select.kqueue() else: logger.debug("Init connections using IOWait") self._connections_found = 0 self._waitobj = IOWait() def register_server(self, fileno, obj): if fileno == -1: logger.error("{} tried to register a server with filenumber == -1".format(obj)) return self._servers[fileno] = obj self._connections[fileno] = obj if hasattr(select, 'epoll'): self._epoll.register(fileno, self._ro) elif hasattr(select, 'kqueue'): event = [ select.kevent(fileno, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD) ] self._kqueue.control(event, 0, 0) else: logger.debug("register_server: put watch on fileno {}".format(fileno)) self._waitobj.watch(fileno, read=True) def register_connection(self, fileno, obj): if fileno == -1: logger.error("tried to register a connection with filenumber == -1") return self._connections[fileno] = obj if hasattr(select, 'epoll'): self._epoll.register(fileno, self._ro) elif hasattr(select, 'kqueue'): event = [ select.kevent(fileno, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD) ] self._kqueue.control(event, 0, 0) else: logger.debug("register_connection: put watch on fileno {}".format(fileno)) self._waitobj.watch(fileno, read=True) def unregister_connection(self, fileno): if fileno == -1: logger.error("tried to unregister a connection with filenumber == -1") return try: if hasattr(select, 'epoll'): self._epoll.unregister(fileno) elif hasattr(select, 'kqueue'): pass else: logger.debug("unregister_connection: unwatch fileno {}".format(fileno)) self._waitobj.unwatch(fileno) except Exception as e: logger.error("unregister a connection with filenumber == {} for epoll failed".format(fileno)) try: del(self._connections[fileno]) except: pass try: del(self._servers[fileno]) except: pass def monitor(self, obj): self._monitor.append(obj) def check(self): for obj in self._monitor: if not obj.connected: obj.connect() def trigger(self, fileno): if fileno == -1: logger.error("tried to trigger a connection with filenumber == -1") return if self._connections[fileno].outbuffer: if hasattr(select, 'epoll'): self._epoll.modify(fileno, self._rw) elif hasattr(select, 'kqueue'): event = [ select.kevent(fileno, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_ADD | KQ_EV_ONESHOT) ] self._kqueue.control(event, 0, 0) else: logger.error("trigger: Operating System without epoll or kqueue is currently not supported, please report this error") def poll(self): time.sleep(0.0000000001) # give epoll.modify a chance if not self._connections: time.sleep(1) return if -1 in self._connections: logger.error("fileno -1 was found, please report to SmartHomeNG team") del( self._connections[-1]) if hasattr(select, 'epoll') or hasattr(select, 'kqueue'): for fileno in self._connections: # Fix for: "RuntimeError: dictionary changed size during iteration" #connections_keys = self._connections.keys() #for fileno in connections_keys: if fileno not in self._servers: if hasattr(select, 'epoll'): if self._connections[fileno].outbuffer: try: self._epoll.modify(fileno, self._rw) except OSError as e: # as with python 3.6 an OSError will be raised when a socket is already closed like with a settimeout # the socket will need to be recreated then logger.error("OSError {} for epoll.modify(RW) with fileno {} for object {}, please report to SmartHomeNG team".format(e, fileno, self._connections[fileno])) # here we could try to get rid of the connection that causes the headache except PermissionError as e: logger.error("PermissionError {} for epoll.modify(RW) with fileno {} for object {}, please report to SmartHomeNG team".format(e, fileno, self._connections[fileno])) # here we could try to get rid of the connection that causes the headache except FileNotFoundError as e: logger.error("FileNotFoundError {} for epoll.modify(RW) with fileno {} for object {}, please report to SmartHomeNG team".format(e, fileno, self._connections[fileno])) # here we could try to get rid of the connection that causes the headache else: try: self._epoll.modify(fileno, self._ro) except OSError as e: # as with python 3.6 an OSError will be raised when a socket is already closed like with a settimeout # the socket will need to be recreated then logger.error("OSError {} for epoll.modify(RO) with fileno {} for object {}, please report to SmartHomeNG team".format(e, fileno, self._connections[fileno])) # here we could try to get rid of the connection that causes the headache except PermissionError as e: logger.error("PermissionError {} for epoll.modify(RO) with fileno {} for object {}, please report to SmartHomeNG team".format(e, fileno, self._connections[fileno])) # here we could try to get rid of the connection that causes the headache except FileNotFoundError as e: logger.error("FileNotFoundError {} for epoll.modify(RO) with fileno {} for object {}, please report to SmartHomeNG team".format(e, fileno, self._connections[fileno])) # here we could try to get rid of the connection that causes the headache elif hasattr(select, 'kqueue'): event = [] if self._connections[fileno].outbuffer: event.append(select.kevent(fileno, filter=select.KQ_FILTER_WRITE, flags=select.KQ_EV_ADD | KQ_EV_ONESHOT)) else: event.append(select.kevent(fileno, filter=select.KQ_FILTER_READ, flags=select.KQ_EV_ADD)) self._kqueue.control(event, 0, 0) if hasattr(select, 'epoll'): for fileno, event in self._epoll.poll(timeout=1): if fileno in self._servers: server = self._servers[fileno] server.handle_connection() else: if event & select.EPOLLIN: try: con = self._connections[fileno] con._in() except Exception as e: con.close() continue if event & select.EPOLLOUT: try: con = self._connections[fileno] con._out() except Exception as e: con.close() continue if event & (select.EPOLLHUP | select.EPOLLERR): try: con = self._connections[fileno] con.close() continue except: pass elif hasattr(select, 'kqueue'): for event in self._kqueue.control(None, 1): fileno = event.ident if fileno in self._servers: server = self._servers[fileno] server.handle_connection() else: if event.filter == select.KQ_FILTER_READ: try: con = self._connections[fileno] con._in() except Exception as e: # noqa con.close() continue if event.filter == select.KQ_FILTER_WRITE: try: con = self._connections[fileno] con._out() except Exception as e: # noqa con.close() continue if event.flags & select.KQ_EV_EOF: try: con = self._connections[fileno] con.close() continue except: pass else: # not using epoll or kqueue n_connections = len(self._connections) if self._connections_found != n_connections: logger.debug("lib/connection.py poll() for len(self._connections)={}".format(n_connections)) self._connections_found = n_connections watched = self._waitobj.get_watched() nwatched = len(watched) logger.debug("iowait in alpha status with {} watched connections".format(nwatched)) events = self._waitobj.wait() nevents = len(events) logger.debug("iowait reports {} events".format(nevents)) for fileno, read, write in events: logger.debug("event for fileno={}, read={}, write={}".format(fileno, read, write)) if fileno in self._servers: server = self._servers[fileno] server.handle_connection() else: logger.debug("fileno {} not in self._servers".format(fileno)) if read: try: con = self._connections[fileno] con._in() except Exception as e: # noqa con.close() continue if write: try: con = self._connections[fileno] con._out() except Exception as e: # noqa con.close() continue def close(self): if -1 in self._connections: logger.error("Connections.close() tried to close a filenumber == -1") try: for fileno in self._connections: try: self._connections[fileno].close() except: pass except: pass