diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index d892f0a53..547f4ace5 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -28,7 +28,7 @@ from .. import Environment, Target, target_factory from ..exceptions import NoDriverFoundError, NoResourceFoundError, InvalidConfigError from ..resource.remote import RemotePlaceManager, RemotePlace -from ..util import diff_dict, flat_dict, filter_dict, dump, atomic_replace, labgrid_version, Timeout +from ..util import diff_dict, flat_dict, filter_dict, dump, atomic_replace, labgrid_version, Timeout, term from ..util.proxy import proxymanager from ..util.helper import processwrapper from ..driver import Mode, ExecutionError @@ -807,71 +807,49 @@ def digital_io(self): drv.set(False) async def _console(self, place, target, timeout, *, logfile=None, loop=False, listen_only=False): - name = self.args.name - from ..resource import NetworkSerialPort - - # deactivate console drivers so we are able to connect with microcom - try: - con = target.get_active_driver("ConsoleProtocol") - target.deactivate(con) - except NoDriverFoundError: - pass - - resource = target.get_resource(NetworkSerialPort, name=name, wait_avail=False) + from ..protocol import ConsoleProtocol - # async await resources - timeout = Timeout(timeout) - while True: - target.update_resources() - if resource.avail or (not loop and timeout.expired): - break - await asyncio.sleep(0.1) - - # use zero timeout to prevent blocking sleeps - target.await_resources([resource], timeout=0.0) + name = self.args.name if not place.acquired: print("place released") return 255 - host, port = proxymanager.get_host_and_port(resource) + if self.args.internal or os.environ.get('LG_CONSOLE') == 'internal': + console = target.get_driver(ConsoleProtocol, name=name) + try: + await term.internal(console, logfile, listen_only) + except OSError as err: + print('error') + else: + from ..resource import NetworkSerialPort, SerialPort - # check for valid resources - assert port is not None, "Port is not set" + # deactivate console drivers so we are able to connect with microcom + try: + con = target.get_active_driver("ConsoleProtocol") + target.deactivate(con) + except NoDriverFoundError: + pass - call = ['microcom', '-s', str(resource.speed), '-t', f"{host}:{port}"] + resource = target.get_resource(NetworkSerialPort, name=name, + wait_avail=False) - if listen_only: - call.append("--listenonly") + # async await resources + timeout = Timeout(timeout) + while True: + target.update_resources() + if resource.avail or (not loop and timeout.expired): + break + await asyncio.sleep(0.1) - if logfile: - call.append(f"--logfile={logfile}") - logging.info(f"connecting to {resource} calling {' '.join(call)}") - try: - p = await asyncio.create_subprocess_exec(*call) - except FileNotFoundError as e: - raise ServerError(f"failed to execute microcom: {e}") - while p.returncode is None: - try: - await asyncio.wait_for(p.wait(), 1.0) - except asyncio.TimeoutError: - # subprocess is still running - pass + # use zero timeout to prevent blocking sleeps + target.await_resources([resource], timeout=0.0) + host, port = proxymanager.get_host_and_port(resource) - try: - self._check_allowed(place) - except UserError: - p.terminate() - try: - await asyncio.wait_for(p.wait(), 1.0) - except asyncio.TimeoutError: - # try harder - p.kill() - await asyncio.wait_for(p.wait(), 1.0) - raise - if p.returncode: - print("connection lost", file=sys.stderr) - return p.returncode + # check for valid resources + assert port is not None, "Port is not set" + await term.microcom(self, host, port, place, resource, logfile, + listen_only) async def console(self, place, target): while True: @@ -882,7 +860,7 @@ async def console(self, place, target): break if not self.args.loop: if res: - exc = InteractiveCommandError("microcom error") + exc = InteractiveCommandError("console error") exc.exitcode = res raise exc break @@ -1721,6 +1699,8 @@ def main(): subparser = subparsers.add_parser('console', aliases=('con',), help="connect to the console") + subparser.add_argument('-i', '--internal', action='store_true', + help="use an internal console instead of microcom") subparser.add_argument('-l', '--loop', action='store_true', help="keep trying to connect if the console is unavailable") subparser.add_argument('-o', '--listenonly', action='store_true', diff --git a/labgrid/util/term.py b/labgrid/util/term.py new file mode 100644 index 000000000..24daa9e1d --- /dev/null +++ b/labgrid/util/term.py @@ -0,0 +1,124 @@ +import asyncio +import collections +import logging +import os +import sys +from pexpect import TIMEOUT +from serial.serialutil import SerialException +import termios +import time + +EXIT_CHAR = 0x1d # FS (Ctrl + ]) + +async def microcom(session, host, port, place, resource, logfile, listen_only): + call = ['microcom', '-q', '-s', str(resource.speed), '-t', f"{host}:{port}"] + + if listen_only: + call.append("--listenonly") + + if logfile: + call.append(f"--logfile={logfile}") + logging.info(f"connecting to {resource} calling {' '.join(call)}") + try: + p = await asyncio.create_subprocess_exec(*call) + except FileNotFoundError as e: + raise ServerError(f"failed to execute microcom: {e}") + while p.returncode is None: + try: + await asyncio.wait_for(p.wait(), 1.0) + except asyncio.TimeoutError: + # subprocess is still running + pass + + try: + session._check_allowed(place) + except UserError: + p.terminate() + try: + await asyncio.wait_for(p.wait(), 1.0) + except asyncio.TimeoutError: + # try harder + p.kill() + await asyncio.wait_for(p.wait(), 1.0) + raise + if p.returncode: + print("connection lost", file=sys.stderr) + return p.returncode + +BUF_SIZE = 1024 + +async def run(serial, log_fd, listen_only): + prev = collections.deque(maxlen=2) + + deadline = None + to_serial = b'' + next_serial = time.monotonic() + txdelay = serial.txdelay + while True: + activity = bool(to_serial) + try: + data = serial.read(size=BUF_SIZE, timeout=0.001) + if data: + activity = True + sys.stdout.buffer.write(data) + sys.stdout.buffer.flush() + if log_fd: + log_fd.write(data) + log_fd.flush() + + except TIMEOUT: + pass + + except SerialException: + break + + if not listen_only: + data = os.read(sys.stdin.fileno(), BUF_SIZE) + if data: + activity = True + if not deadline: + deadline = time.monotonic() + .5 # seconds + prev.extend(data) + count = prev.count(EXIT_CHAR) + if count == 2: + break + + to_serial += data + + if to_serial and time.monotonic() > next_serial: + serial._write(to_serial[:1]) + to_serial = to_serial[1:] + next_serial += txdelay + + if deadline and time.monotonic() > deadline: + prev.clear() + deadline = None + if not activity: + time.sleep(.001) + + # Blank line to move past any partial output + print() + +async def internal(serial, logfile, listen_only): + try: + if not listen_only: + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + new = termios.tcgetattr(fd) + new[3] = new[3] & ~(termios.ICANON | termios.ECHO | termios.ISIG) + new[6][termios.VMIN] = 0 + new[6][termios.VTIME] = 0 + termios.tcsetattr(fd, termios.TCSANOW, new) + + log_fd = None + if logfile: + log_fd = open(logfile, 'wb') + + logging.info('Console start:') + await run(serial, log_fd, listen_only) + + finally: + if not listen_only: + termios.tcsetattr(fd, termios.TCSAFLUSH, old) + if log_fd: + log_fd.close()