class TcpTunnel(ImmortalThread): def __init__(self, vcp): #super(TcpTunnel, self).__init__(self) ImmortalThread.__init__(self) self._needs_reconfig = 0 # link to the port object self._vcp = vcp self._lock = Lock() self._op_lock = Lock() # list of operations to apply to an {out|in}bound tcp # segment. In the future this might include operations # such as encryption or compression - for now, only the # header info. that is applied by devices such as # Lantronix's UDS-10 is supported. Up refers to what is # being applied to outbound tcp data, down to received. # Methods should be added to these lists in the order they # are to be applied. self._up_segment_ops = [] self._down_segment_ops = [] # transaction identifier - used only in vcp mode. self.__tid = 0 self._pending_tid = 0 # one and only poll object. socket, serial fd and # command pipe are all polled. self._poll_obj = None # command pipe allows other threads to insert control # messages. self._cmd_pipe = None # both sides (serial & socket) of the tunnel self._sock_listen_fd = None self._sock_fd = None self._serial_port = None # tcp state management self._is_connected = 0 self.is_active = 0 # tunnel statistics self.tcp_bytes_rcvd = 0 self.tcp_bytes_sent = 0 self.serial_bytes_rcvd = 0 self.serial_bytes_sent = 0 self.connection_attempts = 0 def configure(self, config): self.tty = '/dev/tty' + config['dev'][-2:] self.tcp_port = int(config['tcp_port']) self.mode = config['mode'] self.timeout_msec = config['p_timeout_msec'] if self.mode == 'vcp': if self._up_segment_ops.count(self._add_vcp_header) == 0: self._up_segment_ops.append(self._add_vcp_header) if self._down_segment_ops.count(self._remove_vcp_header) == 0: self._down_segment_ops.append(self._remove_vcp_header) self.is_server = int(config['is_server']) if self.is_server == 0: self.host = config['host'] # who we're connecting to. if self.is_active: # tunnel is being reconfigured "in flight". self._needs_reconfig = 1 self._send_cmd('reconfig') if self._is_in_accept(): self._clear_accept() def run(self): self._needs_reconfig = 0 if self.is_active: # we've restarted due to a configuration change self.start_tunnel() else: if not self._cmd_pipe: # set up the command pipe and begin to build the poll obj. self._cmd_pipe = os.pipe() self._poll_obj = select.poll() self._poll_obj.register(self._cmd_pipe[READ], select.POLLIN | select.POLLERR | select.POLLHUP) while 1: # no poll timeout, wait here until kicked to start. evt = self._poll_obj.poll(-1) if evt[0][0] == self._cmd_pipe[READ]: if evt[0][1] == select.POLLIN: cmd = os.read(self._cmd_pipe[READ], 32) if cmd.find('start') >= 0: self.start_tunnel() # cmd pipe poll err, critical, hard restart self._cmd_pipe = None self._poll_obj = None raise ERestart() def start_tunnel(self): if self is not currentThread(): self._send_cmd('start') return self._op_lock.acquire() try: self.is_active = 1 # set up Port object for the tunnel that reads\writes to the # slave device file of the pseudo-terminal pair. if not self._serial_port: self._serial_port = Port() cfg = self._vcp.configuration() cfg['dev'] = self.tty cfg['name'] = '_slave' cfg['parent'] = self._vcp self._serial_port.configure(cfg) self._op_lock.release() except: self._op_lock.release() while self.is_active: if not self._serial_port.is_open(): self._serial_port.open() self._serial_port.drain() try: if self.is_server: self._do_listen() else: self._do_connect() except: msglog.exception() if self._serial_port and not self._serial_port.is_open(): self._serial_port.close() def stop_tunnel(self): self.is_active = 0 if self is not currentThread(): self._send_cmd('stop') if self._is_in_accept(): self._clear_accept() else: self._op_lock.acquire() try: self._tear_down_fds() finally: self._op_lock.release() #raise ERestart() def is_connected(self): self._op_lock.acquire() result = self._is_connected self._op_lock.release() return result def _create_socket(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return s def _close_socket(self): self._is_connected = 0 self._sock_fd.close() def _send_cmd(self, cmd): self._lock.acquire() try: os.write(self._cmd_pipe[WRITE], cmd) finally: self._lock.release() def _do_connect(self): while self.is_active: self._sock_fd = self._create_socket() while not self._is_connected: self.connection_attempts += 1 try: self._sock_fd.connect((self.host, self.tcp_port)) self._is_connected = 1 except socket.gaierror, e: # host related error, ie. hostname not resolving - possibly transient self.connection_attempts += 1 msglog.log('VCP', WARN, 'Error resolving hostname %s.' % self.host) time.sleep(60) raise EConnectionError except socket.error, e: # connection error, possibly transient - sleep for a bit and retry self.connection_attempts += 1 time.sleep(30) if self._needs_reconfig: self._is_connected = 0 self._tear_down_fds() raise ERestart() # loop in _do_tunnel until the tcp connection or the framework # based consumer (ie. protocol) "goes away". self._do_tunnel() self._is_connected = 0