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.

Series-changes: 4
- Get internal console working with qemu
- Show a prompt when starting, to indicate it is waiting for the board

Signed-off-by: Simon Glass <[email protected]>
  • Loading branch information
sjg20 committed Jan 20, 2025
1 parent 69f4568 commit 398a779
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 30 deletions.
69 changes: 40 additions & 29 deletions labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,48 +929,57 @@ def digital_io(self):
drv.set(False)

async def _console(self, place, target, timeout, *, logfile=None, loop=False, listen_only=False):
from ..protocol import ConsoleProtocol

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
if not place.acquired:
print("place released")
return 255

resource = target.get_resource(NetworkSerialPort, name=name, wait_avail=False)
if self.args.internal or os.environ.get('LG_CONSOLE') == 'internal':
console = target.get_driver(ConsoleProtocol, name=name)
returncode = await term.internal(lambda: self.is_allowed(place),
console, logfile, listen_only)
else:
from ..resource import NetworkSerialPort

# 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)
# deactivate console drivers so we are able to connect with microcom
try:
con = target.get_active_driver("ConsoleProtocol")
target.deactivate(con)
except NoDriverFoundError:
pass

# use zero timeout to prevent blocking sleeps
target.await_resources([resource], timeout=0.0)
resource = target.get_resource(NetworkSerialPort, name=name,
wait_avail=False)

if not place.acquired:
print("place released")
return 255
# 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)

host, port = proxymanager.get_host_and_port(resource)
# use zero timeout to prevent blocking sleeps
target.await_resources([resource], timeout=0.0)
host, port = proxymanager.get_host_and_port(resource)

# check for valid resources
assert port is not None, "Port is not set"
try:
returncode = await term.external(lambda: self.is_allowed(place),
host, port, resource, logfile,
listen_only)
except FileNotFoundError as e:
raise ServerError(f"failed to execute remote console command: {e}")
# check for valid resources
assert port is not None, "Port is not set"
try:
returncode = await term.external(lambda: self.is_allowed(place),
host, port, resource, logfile,
listen_only)
except FileNotFoundError as e:
raise ServerError(f"failed to execute remote console command: {e}")

# Raise an exception if the place was released
self._check_allowed(place)
return returncode


async def console(self, place, target):
while True:
res = await self._console(
Expand Down Expand Up @@ -1803,6 +1812,8 @@ def main():
subparser.set_defaults(func=ClientSession.digital_io)

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"
)
Expand Down
127 changes: 126 additions & 1 deletion labgrid/util/term.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
"""Terminal handling, using microcom or telnet"""
"""Terminal handling, using microcom, telnet or an internal function"""

import asyncio
import collections
import logging
import os
import sys
import shutil
import termios
import time

from pexpect import TIMEOUT
from serial.serialutil import SerialException

EXIT_CHAR = 0x1d # FS (Ctrl + ])

Expand Down Expand Up @@ -69,3 +76,121 @@ async def external(check_allowed, host, port, resource, logfile, listen_only):
if p.returncode:
print("connection lost", file=sys.stderr)
return p.returncode


BUF_SIZE = 1024

async def run(check_allowed, cons, log_fd, listen_only):
prev = collections.deque(maxlen=2)

deadline = None
to_cons = b''
next_cons = time.monotonic()
txdelay = cons.txdelay

# Show a message to indicate we are waiting for output from the board
msg = 'Terminal ready...press Ctrl-] twice to exit'
sys.stdout.write(msg)
sys.stdout.flush()
erase_msg = '\b' * len(msg) + ' ' * len(msg) + '\b' * len(msg)
have_output = False

while True:
activity = bool(to_cons)
try:
data = cons.read(size=BUF_SIZE, timeout=0.001)
if data:
activity = True
if not have_output:
# Erase our message
sys.stdout.write(erase_msg)
sys.stdout.flush()
have_output = 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_cons += data

if to_cons and time.monotonic() > next_cons:
cons._write(to_cons[:1])
to_cons = to_cons[1:]
if txdelay:
next_cons += txdelay

if deadline and time.monotonic() > deadline:
prev.clear()
deadline = None
if check_allowed():
break
if not activity:
time.sleep(.001)

# Blank line to move past any partial output
print()


async def internal(check_allowed, cons, logfile, listen_only):
"""Start an external terminal sessions
This uses microcom if available, otherwise falls back to telnet.
Args:
check_allowed (lambda): Function to call to make sure the terminal is
still accessible. No args. Returns True if allowed, False if not.
cons (str): ConsoleProtocol device to read/write
logfile (str): Logfile to write output too, or None
listen_only (bool): True to ignore keyboard input
Return:
int: Result code
"""
returncode = 0
old = None
try:
if not listen_only and os.isatty(sys.stdout.fileno()):
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(check_allowed, cons, log_fd, listen_only)

except OSError as err:
print('error', err)
returncode = 1

finally:
if old:
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
if log_fd:
log_fd.close()

return returncode

0 comments on commit 398a779

Please sign in to comment.