From 588ee8eed000bf85577419713524fac738ae1084 Mon Sep 17 00:00:00 2001 From: Joe Bowen Date: Mon, 12 May 2014 08:16:37 -0600 Subject: [PATCH] Changed the way vpcs closes by using the socket to send quit message instead of pid kill --- gns3server/modules/__init__.py | 2 + gns3server/modules/vpcs/__init__.py | 58 +-- gns3server/modules/vpcs/vpcs_device.py | 60 +-- gns3server/modules/vpcs/vpcscon.py | 642 ------------------------- vpcs.hist | 5 + 5 files changed, 58 insertions(+), 709 deletions(-) delete mode 100644 gns3server/modules/vpcs/vpcscon.py create mode 100644 vpcs.hist diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 59304d19..7fd76401 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,8 +18,10 @@ import sys from .base import IModule from .dynamips import Dynamips +from .vpcs import VPCS MODULES = [Dynamips] +MODULES.append(VPCS) if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index 81209123..d532ce98 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -65,17 +65,17 @@ class VPCS(IModule): # get the VPCS location config = Config.instance() VPCS_config = config.get_section_config(name.upper()) - self._VPCS = VPCS_config.get("VPCS") + self._VPCS = VPCS_config.get("vpcs") if not self._VPCS or not os.path.isfile(self._VPCS): - VPCS_in_cwd = os.path.join(os.getcwd(), "VPCS") + VPCS_in_cwd = os.path.join(os.getcwd(), "vpcs") if os.path.isfile(VPCS_in_cwd): self._VPCS = VPCS_in_cwd else: # look for VPCS if none is defined or accessible for path in os.environ["PATH"].split(":"): try: - if "VPCS" in os.listdir(path) and os.access(os.path.join(path, "VPCS"), os.X_OK): - self._VPCS = os.path.join(path, "VPCS") + if "vpcs" in os.listdir(path) and os.access(os.path.join(path, "vpcs"), os.X_OK): + self._VPCS = os.path.join(path, "vpcs") break except OSError: continue @@ -102,8 +102,8 @@ class VPCS(IModule): self._VPCSrc = "" # check every 5 seconds - self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) - self._VPCS_callback.start() + #self._VPCS_callback = self.add_periodic_callback(self._check_VPCS_is_alive, 5000) + #self._VPCS_callback.start() def stop(self, signum=None): """ @@ -135,12 +135,12 @@ class VPCS(IModule): "id": VPCS_id, "name": VPCS_instance.name} if not VPCS_instance.is_running(): - stdout = VPCS_instance.read_VPCS_stdout() + stdout = VPCS_instance.read_vpcs_stdout() notification["message"] = "VPCS has stopped running" notification["details"] = stdout self.send_notification("{}.VPCS_stopped".format(self.name), notification) elif not VPCS_instance.is_VPCS_running(): - stdout = VPCS_instance.read_VPCS_stdout() + stdout = VPCS_instance.read_vpcs_stdout() notification["message"] = "VPCS has stopped running" notification["details"] = stdout self.send_notification("{}.VPCS_stopped".format(self.name), notification) @@ -161,7 +161,7 @@ class VPCS(IModule): return None return self._VPCS_instances[VPCS_id] - @IModule.route("VPCS.reset") + @IModule.route("vpcs.reset") def reset(self, request): """ Resets the module. @@ -184,7 +184,7 @@ class VPCS(IModule): log.info("VPCS module has been reset") - @IModule.route("VPCS.settings") + @IModule.route("vpcs.settings") def settings(self, request): """ Set or update settings. @@ -247,7 +247,7 @@ class VPCS(IModule): return {"result": result, "message": message} - @IModule.route("VPCS.test_settings") + @IModule.route("vpcs.test_settings") def test_settings(self, request): """ """ @@ -256,7 +256,7 @@ class VPCS(IModule): self.send_response(response) - @IModule.route("VPCS.create") + @IModule.route("vpcs.create") def VPCS_create(self, request): """ Creates a new VPCS instance. @@ -313,7 +313,7 @@ class VPCS(IModule): self._VPCS_instances[VPCS_instance.id] = VPCS_instance self.send_response(response) - @IModule.route("VPCS.delete") + @IModule.route("vpcs.delete") def VPCS_delete(self, request): """ Deletes an VPCS instance. @@ -345,7 +345,7 @@ class VPCS(IModule): self.send_response(True) - @IModule.route("VPCS.update") + @IModule.route("vpcs.update") def VPCS_update(self, request): """ Updates an VPCS instance @@ -355,7 +355,7 @@ class VPCS(IModule): Optional request parameters: - any setting to update - - startup_config_base64 (startup-config base64 encoded) + - script_file_base64 (script-file base64 encoded) Response parameters: - updated settings @@ -374,20 +374,20 @@ class VPCS(IModule): response = {} try: - # a new startup-config has been pushed - if "startup_config_base64" in request: - config = base64.decodestring(request["startup_config_base64"].encode("utf-8")).decode("utf-8") + # a new script-file has been pushed + if "script_file_base64" in request: + config = base64.decodestring(request["script_file_base64"].encode("utf-8")).decode("utf-8") config = "!\n" + config.replace("\r", "") config = config.replace('%h', VPCS_instance.name) - config_path = os.path.join(VPCS_instance.working_dir, "startup-config") + config_path = os.path.join(VPCS_instance.working_dir, "script-file") try: with open(config_path, "w") as f: - log.info("saving startup-config to {}".format(config_path)) + log.info("saving script-file to {}".format(config_path)) f.write(config) except OSError as e: raise VPCSError("Could not save the configuration {}: {}".format(config_path, e)) - # update the request with the new local startup-config path - request["startup_config"] = os.path.basename(config_path) + # update the request with the new local script-file path + request["script_file"] = os.path.basename(config_path) except VPCSError as e: self.send_custom_error(str(e)) @@ -405,7 +405,7 @@ class VPCS(IModule): self.send_response(response) - @IModule.route("VPCS.start") + @IModule.route("vpcs.start") def vm_start(self, request): """ Starts an VPCS instance. @@ -438,7 +438,7 @@ class VPCS(IModule): return self.send_response(True) - @IModule.route("VPCS.stop") + @IModule.route("vpcs.stop") def vm_stop(self, request): """ Stops an VPCS instance. @@ -468,7 +468,7 @@ class VPCS(IModule): return self.send_response(True) - @IModule.route("VPCS.reload") + @IModule.route("vpcs.reload") def vm_reload(self, request): """ Reloads an VPCS instance. @@ -500,7 +500,7 @@ class VPCS(IModule): return self.send_response(True) - @IModule.route("VPCS.allocate_udp_port") + @IModule.route("vpcs.allocate_udp_port") def allocate_udp_port(self, request): """ Allocates a UDP port in order to create an UDP NIO. @@ -573,7 +573,7 @@ class VPCS(IModule): raise VPCSError("{} has no privileged access to {}.".format(self._VPCS, device)) - @IModule.route("VPCS.add_nio") + @IModule.route("vpcs.add_nio") def add_nio(self, request): """ Adds an NIO (Network Input/Output) for an VPCS instance. @@ -633,7 +633,7 @@ class VPCS(IModule): self.send_response({"port_id": request["port_id"]}) - @IModule.route("VPCS.delete_nio") + @IModule.route("vpcs.delete_nio") def delete_nio(self, request): """ Deletes an NIO (Network Input/Output). @@ -668,7 +668,7 @@ class VPCS(IModule): self.send_response(True) - @IModule.route("VPCS.echo") + @IModule.route("vpcs.echo") def echo(self, request): """ Echo end point for testing purposes. diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 5581a952..2c9f8dc7 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -27,7 +27,8 @@ import subprocess import argparse import threading import configparser -from .vpcscon import start_vpcscon +import sys +import socket from .vpcs_error import VPCSError from .adapters.ethernet_adapter import EthernetAdapter from .nios.nio_udp import NIO_UDP @@ -51,11 +52,11 @@ class VPCSDevice(object): def __init__(self, path, working_dir, host="127.0.0.1", name=None): - # find an instance identifier (0 <= id < 255) + # find an instance identifier (1 <= id <= 255) # This 255 limit is due to a restriction on the number of possible # mac addresses given in VPCS using the -m option self._id = 0 - for identifier in range(0, 255): + for identifier in range(1, 256): if identifier not in self._instances: self._id = identifier self._instances.append(self._id) @@ -74,9 +75,7 @@ class VPCSDevice(object): self._command = [] self._process = None self._vpcs_stdout_file = "" - self._vpcscon_thead = None - self._vpcscon_thread_stop_event = None - self._host = host + self._host = "127.0.0.1" self._started = False # VPCS settings @@ -252,19 +251,6 @@ class VPCSDevice(object): return self._started - def _start_vpcscon(self): - """ - Starts vpcscon thread (for console connections). - """ - - if not self._vpcscon_thead: - telnet_server = "{}:{}".format(self._host, self._console) - log.info("starting vpcscon for VPCS instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) - args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) - self._vpcscon_thread_stop_event = threading.Event() - self._vpcscon_thead = threading.Thread(target=start_vpcscon, args=(args, self._vpcscon_thread_stop_event)) - self._vpcscon_thead.start() ", ".join(missing_libs))) - def start(self): """ Starts the VPCS process. @@ -297,9 +283,6 @@ class VPCSDevice(object): log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout)) - # start console support - self._start_vpcscon() - def stop(self): """ Stops the VPCS process. @@ -309,23 +292,16 @@ class VPCSDevice(object): if self.is_running(): log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) try: - self._process.terminate() - self._process.wait(1) - except subprocess.TimeoutExpired: - self._process.kill() - if self._process.poll() == None: - log.warn("VPCS instance {} PID={} is still running".format(self._id, - self._process.pid)) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self._host, self._console)) + sock.send(bytes("quit\n", 'UTF-8')) + sock.close() + except TypeError as e: + log.warn("VPCS instance {} PID={} is still running. Error: {}".format(self._id, + self._process.pid, e)) self._process = None self._started = False - # stop console support - if self._vpcscon_thead: - self._vpcscon_thread_stop_event.set() - if self._vpcscon_thead.is_alive(): - self._vpcscon_thead.join(timeout=0.10) - self._vpcscon_thead = None - def read_vpcs_stdout(self): """ @@ -349,8 +325,16 @@ class VPCSDevice(object): :returns: True or False """ - if self._process and self._process.poll() == None: - return True + if self._process: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self._host, self._console)) + sock.close() + return True + except: + e = sys.exc_info()[0] + log.warn("Could not connect to {}:{}. Error: {}".format(self._host, self._console, e)) + return False return False diff --git a/gns3server/modules/vpcs/vpcscon.py b/gns3server/modules/vpcs/vpcscon.py deleted file mode 100644 index b481175d..00000000 --- a/gns3server/modules/vpcs/vpcscon.py +++ /dev/null @@ -1,642 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Copyright (C) 2013, 2014 James E. Carpenter -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import socket -import sys -import os -import select -import fcntl -import struct -import termios -import tty -import time -import argparse -import traceback - - -import logging -log = logging.getLogger(__name__) - - -# Escape characters -ESC_CHAR = '^^' # can be overriden from command line -ESC_QUIT = 'q' - -# VPCS seems to only send *1* byte at a time. If -# they ever fix that we'll be ready for it. -BUFFER_SIZE = 1024 - -# How long to wait before retrying a connection (seconds) -RETRY_DELAY = 3 - -# How often to test an idle connection (seconds) -POLL_TIMEOUT = 3 - - -EXIT_SUCCESS = 0 -EXIT_FAILURE = 1 -EXIT_ABORT = 2 - -# Mostly from: -# https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py -#--[ Telnet Commands ]--------------------------------------------------------- -SE = 240 # End of subnegotiation parameters -NOP = 241 # No operation -DATMK = 242 # Data stream portion of a sync. -BREAK = 243 # NVT Character BRK -IP = 244 # Interrupt Process -AO = 245 # Abort Output -AYT = 246 # Are you there -EC = 247 # Erase Character -EL = 248 # Erase Line -GA = 249 # The Go Ahead Signal -SB = 250 # Sub-option to follow -WILL = 251 # Will; request or confirm option begin -WONT = 252 # Wont; deny option request -DO = 253 # Do = Request or confirm remote option -DONT = 254 # Don't = Demand or confirm option halt -IAC = 255 # Interpret as Command -SEND = 1 # Sub-process negotiation SEND command -IS = 0 # Sub-process negotiation IS command -#--[ Telnet Options ]---------------------------------------------------------- -BINARY = 0 # Transmit Binary -ECHO = 1 # Echo characters back to sender -RECON = 2 # Reconnection -SGA = 3 # Suppress Go-Ahead -TMARK = 6 # Timing Mark -TTYPE = 24 # Terminal Type -NAWS = 31 # Negotiate About Window Size -LINEMO = 34 # Line Mode - - -class FileLock: - - # struct flock { /* from fcntl(2) */ - # ... - # short l_type; /* Type of lock: F_RDLCK, - # F_WRLCK, F_UNLCK */ - # short l_whence; /* How to interpret l_start: - # SEEK_SET, SEEK_CUR, SEEK_END */ - # off_t l_start; /* Starting offset for lock */ - # off_t l_len; /* Number of bytes to lock */ - # pid_t l_pid; /* PID of process blocking our lock - # (F_GETLK only) */ - # ... - # }; - _flock = struct.Struct('hhqql') - - def __init__(self, fname=None): - self.fd = None - self.fname = fname - - def get_lock(self): - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, - 0, 0, os.getpid()) - flk = self._flock.unpack( - fcntl.fcntl(self.fd, fcntl.F_GETLK, flk)) - - # If it's not locked (or is locked by us) then return None, - # otherwise return the PID of the owner. - if flk[0] == fcntl.F_UNLCK: - return None - return flk[4] - - def lock(self): - try: - self.fd = open('{}.lck'.format(self.fname), 'a') - except Exception as e: - raise LockError("Couldn't get lock on {}: {}" - .format(self.fname, e)) - - flk = self._flock.pack(fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) - try: - fcntl.fcntl(self.fd, fcntl.F_SETLK, flk) - except BlockingIOError: - raise LockError("Already connected. PID {} has lock on {}" - .format(self.get_lock(), self.fname)) - - # If we got here then we must have the lock. Store the PID. - self.fd.truncate(0) - self.fd.write('{}\n'.format(os.getpid())) - self.fd.flush() - - def unlock(self): - if self.fd: - # Deleting first prevents a race condition - os.unlink(self.fd.name) - self.fd.close() - - def __enter__(self): - self.lock() - - def __exit__(self, exc_type, exc_val, exc_tb): - self.unlock() - return False - - -class Console: - def fileno(self): - raise NotImplementedError("Only routers have fileno()") - - -class Router: - pass - - -class TTY(Console): - - def read(self, fileno, bufsize): - return self.fd.read(bufsize) - - def write(self, buf): - return self.fd.write(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def __enter__(self): - try: - self.fd = open('/dev/tty', 'r+b', buffering=0) - except OSError as e: - raise TTYError("Couldn't open controlling TTY: {}".format(e)) - - # Save original flags - self.termios = termios.tcgetattr(self.fd) - self.fcntl = fcntl.fcntl(self.fd, fcntl.F_GETFL) - - # Update flags - tty.setraw(self.fd, termios.TCSANOW) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl | os.O_NONBLOCK) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - - # Restore flags to original settings - termios.tcsetattr(self.fd, termios.TCSANOW, self.termios) - fcntl.fcntl(self.fd, fcntl.F_SETFL, self.fcntl) - - self.fd.close() - - return False - - -class TelnetServer(Console): - - def __init__(self, addr, port, stop_event): - self.addr = addr - self.port = port - self.fd_dict = {} - self.stop_event = stop_event - - def read(self, fileno, bufsize): - # Someone wants to connect? - if fileno == self.sock_fd.fileno(): - self._accept() - return None - - self._cur_fileno = fileno - - # Read a maximum of _bufsize_ bytes without blocking. When it - # would want to block it means there's no more data. An empty - # buffer normally means that we've been disconnected. - try: - buf = self._read_cur(bufsize, socket.MSG_DONTWAIT) - except BlockingIOError: - return None - if not buf: - self._disconnect(fileno) - - # Process and remove any telnet commands from the buffer - if IAC in buf: - buf = self._IAC_parser(buf) - - return buf - - def write(self, buf): - for fd in self.fd_dict.values(): - fd.send(buf) - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.sock_fd, select.EPOLLIN) - - def _read_block(self, bufsize): - buf = self._read_cur(bufsize, socket.MSG_WAITALL) - # If we don't get everything we were looking for then the - # client probably disconnected. - if len(buf) < bufsize: - self._disconnect(self._cur_fileno) - return buf - - def _read_cur(self, bufsize, flags): - return self.fd_dict[self._cur_fileno].recv(bufsize, flags) - - def _write_cur(self, buf): - return self.fd_dict[self._cur_fileno].send(buf) - - def _IAC_parser(self, buf): - skip_to = 0 - while not self.stop_event.is_set(): - # Locate an IAC to process - iac_loc = buf.find(IAC, skip_to) - if iac_loc < 0: - break - - # Get the TELNET command - iac_cmd = bytearray([IAC]) - try: - iac_cmd.append(buf[iac_loc + 1]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 1]) - - # Is this just a 2-byte TELNET command? - if iac_cmd[1] not in [WILL, WONT, DO, DONT]: - if iac_cmd[1] == AYT: - log.debug("Telnet server received Are-You-There (AYT)") - self._write_cur( - b'\r\nYour Are-You-There received. I am here.\r\n' - ) - elif iac_cmd[1] == IAC: - # It's data, not an IAC - iac_cmd.pop() - # This prevents the 0xff from being - # interputed as yet another IAC - skip_to = iac_loc + 1 - log.debug("Received IAC IAC") - elif iac_cmd[1] == NOP: - pass - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x}".format(*iac_cmd)) - - # This must be a 3-byte TELNET command - else: - try: - iac_cmd.append(buf[iac_loc + 2]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 2]) - # We do ECHO, SGA, and BINARY. Period. - if (iac_cmd[1] == DO - and iac_cmd[2] not in [ECHO, SGA, BINARY]): - - self._write_cur(bytes([IAC, WONT, iac_cmd[2]])) - log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) - - # Remove the entire TELNET command from the buffer - buf = buf.replace(iac_cmd, b'', 1) - - # Return the new copy of the buffer, minus telnet commands - return buf - - def _accept(self): - fd, addr = self.sock_fd.accept() - self.fd_dict[fd.fileno()] = fd - self.epoll.register(fd, select.EPOLLIN | select.EPOLLET) - - log.info("Telnet connection from {}:{}".format(addr[0], addr[1])) - - # This is a one-way negotiation. This is very basic so there - # shouldn't be any problems with any decent client. - fd.send(bytes([IAC, WILL, ECHO, - IAC, WILL, SGA, - IAC, WILL, BINARY, - IAC, DO, BINARY])) - - if args.telnet_limit and len(self.fd_dict) > args.telnet_limit: - fd.send(b'\r\nToo many connections\r\n') - self._disconnect(fd.fileno()) - log.warn("Client disconnected because of too many connections. " - "(limit currently {})".format(args.telnet_limit)) - - def _disconnect(self, fileno): - fd = self.fd_dict.pop(fileno) - log.info("Telnet client disconnected") - fd.shutdown(socket.SHUT_RDWR) - fd.close() - - def __enter__(self): - # Open a socket and start listening - sock_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - sock_fd.bind((self.addr, self.port)) - except OSError: - raise TelnetServerError("Cannot bind to {}:{}" - .format(self.addr, self.port)) - - sock_fd.listen(socket.SOMAXCONN) - self.sock_fd = sock_fd - log.info("Telnet server ready for connections on {}:{}".format(self.addr, self.port)) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - for fileno in list(self.fd_dict.keys()): - self._disconnect(fileno) - self.sock_fd.close() - return False - - -class VPCS(Router): - - def __init__(self, ttyC, ttyS, stop_event): - self.ttyC = ttyC - self.ttyS = ttyS - self.stop_event = stop_event - - def read(self, bufsize): - try: - buf = self.fd.recv(bufsize) - except BlockingIOError: - return None - return buf - - def write(self, buf): - self.fd.send(buf) - - def _open(self): - self.fd = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) - self.fd.setblocking(False) - - def _bind(self): - try: - os.unlink(self.ttyC) - except FileNotFoundError: - pass - except Exception as e: - raise NetioError("Couldn't unlink socket {}: {}" - .format(self.ttyC, e)) - - try: - self.fd.bind(self.ttyC) - except Exception as e: - raise NetioError("Couldn't create socket {}: {}" - .format(self.ttyC, e)) - - def _connect(self): - # Keep trying until we connect or die trying - while not self.stop_event.is_set(): - try: - self.fd.connect(self.ttyS) - except FileNotFoundError: - log.debug("Waiting to connect to {}".format(self.ttyS)) - time.sleep(RETRY_DELAY) - except Exception as e: - raise NetioError("Couldn't connect to socket {}: {}" - .format(self.ttyS, e)) - else: - break - - def register(self, epoll): - self.epoll = epoll - epoll.register(self.fd, select.EPOLLIN | select.EPOLLET) - - def fileno(self): - return self.fd.fileno() - - def __enter__(self): - self._open() - self._bind() - self._connect() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - os.unlink(self.ttyC) - self.fd.close() - return False - - -class VPCSConError(Exception): - pass - - -class LockError(VPCSConError): - pass - - -class NetioError(VPCSConError): - pass - - -class TTYError(VPCSConError): - pass - - -class TelnetServerError(VPCSConError): - pass - - -class ConfigError(VPCSConError): - pass - - -def mkdir_netio(netio_dir): - try: - os.mkdir(netio_dir) - except FileExistsError: - pass - except Exception as e: - raise NetioError("Couldn't create directory {}: {}" - .format(netio_dir, e)) - - -def send_recv_loop(console, router, esc_char, stop_event): - - epoll = select.epoll() - router.register(epoll) - console.register(epoll) - - router_fileno = router.fileno() - esc_quit = bytes(ESC_QUIT.upper(), 'ascii') - esc_state = False - - while not stop_event.is_set(): - event_list = epoll.poll(timeout=POLL_TIMEOUT) - - # When/if the poll times out we send an empty datagram. If VPCS - # has gone away then this will toss a ConnectionRefusedError - # exception. - if not event_list: - router.write(b'') - continue - - for fileno, event in event_list: - buf = bytearray() - - # VPCS --> tty(s) - if fileno == router_fileno: - while not stop_event.is_set(): - data = router.read(BUFFER_SIZE) - if not data: - break - buf.extend(data) - console.write(buf) - - # tty --> VPCS - else: - while not stop_event.is_set(): - data = console.read(fileno, BUFFER_SIZE) - if not data: - break - buf.extend(data) - - # If we just received the escape character then - # enter the escape state. - # - # If we are in the escape state then check for a - # quit command. Or if it's the escape character then - # send the escape character. Else, send the escape - # character we ate earlier and whatever character we - # just got. Exit escape state. - # - # If we're not in the escape state and this isn't an - # escape character then just send it to VPCS. - if esc_state: - if buf.upper() == esc_quit: - sys.exit(EXIT_SUCCESS) - elif buf == esc_char: - router.write(esc_char) - else: - router.write(esc_char) - router.write(buf) - esc_state = False - elif buf == esc_char: - esc_state = True - else: - router.write(buf) - - -def get_args(): - parser = argparse.ArgumentParser( - description='Connect to an VPCS console port.') - parser.add_argument('-d', '--debug', action='store_true', - help='display some debugging information') - parser.add_argument('-e', '--escape', - help='set escape character (default: %(default)s)', - default=ESC_CHAR, metavar='CHAR') - parser.add_argument('-t', '--telnet-server', - help='start telnet server listening on ADDR:PORT', - metavar='ADDR:PORT', default=False) - parser.add_argument('-l', '--telnet-limit', - help='maximum number of simultaneous ' - 'telnet connections (default: %(default)s)', - metavar='LIMIT', type=int, default=1) - parser.add_argument('appl_id', help='VPCS instance identifier') - return parser.parse_args() - - -def get_escape_character(escape): - - # Figure out the escape character to use. - # Can be any ASCII character or a spelled out control - # character, like "^e". The string "none" disables it. - if escape.lower() == 'none': - esc_char = b'' - elif len(escape) == 2 and escape[0] == '^': - c = ord(escape[1].upper()) - 0x40 - if not 0 <= c <= 0x1f: # control code range - raise ConfigError("Invalid control code") - esc_char = bytes([c]) - elif len(escape) == 1: - try: - esc_char = bytes(escape, 'ascii') - except ValueError as e: - raise ConfigError("Invalid escape character") from e - else: - raise ConfigError("Invalid length for escape character") - - return esc_char - - -def start_VPCScon(cmdline_args, stop_event): - - global args - args = cmdline_args - - if args.debug: - logging.basicConfig(level=logging.DEBUG) - else: - # default logging level - logging.basicConfig(level=logging.INFO) - - # Create paths for the Unix domain sockets - netio = '/tmp/netio{}'.format(os.getuid()) - ttyC = '{}/ttyC{}'.format(netio, args.appl_id) - ttyS = '{}/ttyS{}'.format(netio, args.appl_id) - - try: - mkdir_netio(netio) - with FileLock(ttyC): - esc_char = get_escape_character(args.escape) - - if args.telnet_server: - addr, _, port = args.telnet_server.partition(':') - nport = 0 - try: - nport = int(port) - except ValueError: - pass - if (addr == '' or nport == 0): - raise ConfigError('format for --telnet-server must be ' - 'ADDR:PORT (like 127.0.0.1:20000)') - - while not stop_event.is_set(): - try: - if args.telnet_server: - with TelnetServer(addr, nport, stop_event) as console: - with VPCS(ttyC, ttyS, stop_event) as router: - send_recv_loop(console, router, b'', stop_event) - else: - with VPCS(ttyC, ttyS, stop_event) as router, TTY() as console: - send_recv_loop(console, router, esc_char, stop_event) - except ConnectionRefusedError: - pass - except KeyboardInterrupt: - sys.exit(EXIT_ABORT) - finally: - # Put us at the beginning of a line - if not args.telnet_server: - print() - - except VPCSConError as e: - if args.debug: - traceback.print_exc(file=sys.stderr) - else: - print(e, file=sys.stderr) - sys.exit(EXIT_FAILURE) - - log.info("exiting...") - - -def main(): - - import threading - stop_event = threading.Event() - args = get_args() - start_VPCScon(args, stop_event) - -if __name__ == '__main__': - main() diff --git a/vpcs.hist b/vpcs.hist new file mode 100644 index 00000000..f19c8fa4 --- /dev/null +++ b/vpcs.hist @@ -0,0 +1,5 @@ +show +quit +show +quot +quit