Skip to content

Commit

Permalink
remote/client: Provide an internal console
Browse files Browse the repository at this point in the history
At present Labgrid uses microcom as its console. This has some
limitations:

- console output is lost between when the board is reset and microcom
  connects
- txdelay cannot be handled in microcom, meaning that boards may fail
  to receive expected output
- the console may echo a few characters back to the caller in the time
  between when 'labgrid-client console' is executed and when microcom
  starts (which causes failures with U-Boot test system)

For many use cases, microcom is more than is needed, so provide a simple
internal terminal which resolved the above problems.

It is enabled by a '-i' option to the 'console' command, as well as an
environment variable, so that it can be adjustly without updating a lot
of scripts.

To exit, press Ctrl-] twice, quickly.

Signed-off-by: Simon Glass <sjg@chromium.org>
  • Loading branch information
sjg20 committed Jun 10, 2024
1 parent afbfcc3 commit 3f6d723
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 56 deletions.
92 changes: 36 additions & 56 deletions labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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',
Expand Down
124 changes: 124 additions & 0 deletions labgrid/util/term.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 3f6d723

Please sign in to comment.