Skip to content

Commit 410e900

Browse files
committed
remote/client: Provide an internal console
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 <sjg@chromium.org>
1 parent f2a683f commit 410e900

4 files changed

Lines changed: 180 additions & 31 deletions

File tree

doc/usage.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,3 +839,14 @@ like this:
839839
$ labgrid-client -p example allow sirius/john
840840
841841
To remove the allow it is currently necessary to unlock and lock the place.
842+
843+
Internal console
844+
^^^^^^^^^^^^^^^^
845+
846+
Labgrid uses microcom as its console by default. For situations where this is
847+
not suitable, an internal console is provided. To use this, provide the
848+
``--internal`` flag to the ``labgrid client`` command.
849+
850+
When the internal console is used, the console transitions cleanly between use
851+
within a strategy or driver, and interactive use for the user. The console is
852+
not closed and therefore there is no loss of data.

labgrid/remote/client.py

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,43 +1040,49 @@ def digital_io(self):
10401040
drv.set(False)
10411041

10421042
async def _console(self, place, target, timeout, *, logfile=None, loop=False, listen_only=False):
1043+
from ..protocol import ConsoleProtocol
1044+
10431045
name = self.args.name
1044-
from ..resource import NetworkSerialPort
10451046

1046-
# deactivate console drivers so we are able to connect with microcom
1047-
try:
1048-
con = target.get_active_driver("ConsoleProtocol")
1049-
target.deactivate(con)
1050-
except NoDriverFoundError:
1051-
pass
1047+
if not place.acquired:
1048+
print("place released")
1049+
return 255
10521050

1053-
resource = target.get_resource(NetworkSerialPort, name=name, wait_avail=False)
1051+
if self.args.internal or os.environ.get("LG_CONSOLE") == "internal":
1052+
console = target.get_driver(ConsoleProtocol, name=name)
1053+
returncode = await term.internal(lambda: self.is_allowed(place), console, logfile, listen_only)
1054+
else:
1055+
from ..resource import NetworkSerialPort
10541056

1055-
# async await resources
1056-
timeout = Timeout(timeout)
1057-
while True:
1058-
target.update_resources()
1059-
if resource.avail or (not loop and timeout.expired):
1060-
break
1061-
await asyncio.sleep(0.1)
1057+
# deactivate console drivers so we are able to connect with microcom
1058+
try:
1059+
con = target.get_active_driver("ConsoleProtocol")
1060+
target.deactivate(con)
1061+
except NoDriverFoundError:
1062+
pass
10621063

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

1066-
if not place.acquired:
1067-
print("place released")
1068-
return 255
1066+
# async await resources
1067+
timeout = Timeout(timeout)
1068+
while True:
1069+
target.update_resources()
1070+
if resource.avail or (not loop and timeout.expired):
1071+
break
1072+
await asyncio.sleep(0.1)
10691073

1070-
host, port = proxymanager.get_host_and_port(resource)
1074+
# use zero timeout to prevent blocking sleeps
1075+
target.await_resources([resource], timeout=0.0)
1076+
host, port = proxymanager.get_host_and_port(resource)
10711077

1072-
# check for valid resources
1073-
assert port is not None, "Port is not set"
1074-
try:
1075-
returncode = await term.external(
1076-
lambda: self.is_allowed(place), host, port, resource, logfile, listen_only
1077-
)
1078-
except FileNotFoundError as e:
1079-
raise ServerError(f"failed to execute remote console command: {e}")
1078+
# check for valid resources
1079+
assert port is not None, "Port is not set"
1080+
try:
1081+
returncode = await term.external(
1082+
lambda: self.is_allowed(place), host, port, resource, logfile, listen_only
1083+
)
1084+
except FileNotFoundError as e:
1085+
raise ServerError(f"failed to execute remote console command: {e}")
10801086

10811087
# Raise an exception if the place was released
10821088
self._check_allowed(place)
@@ -1988,6 +1994,7 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg
19881994
subparser.set_defaults(func=ClientSession.digital_io)
19891995

19901996
subparser = subparsers.add_parser("console", aliases=("con",), help="connect to the console")
1997+
subparser.add_argument("-i", "--internal", action="store_true", help="use an internal console instead of microcom")
19911998
subparser.add_argument(
19921999
"-l", "--loop", action="store_true", help="keep trying to connect if the console is unavailable"
19932000
)

labgrid/util/term.py

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
"""Terminal handling, using microcom or telnet"""
1+
"""Terminal handling, using microcom, telnet or an internal function"""
22

33
import asyncio
4+
import collections
45
import logging
6+
import os
57
import sys
68
import shutil
9+
import termios
10+
import time
11+
12+
from pexpect import TIMEOUT
13+
from serial.serialutil import SerialException
714

815
EXIT_CHAR = 0x1d # FS (Ctrl + ])
916

@@ -69,3 +76,122 @@ async def external(check_allowed, host, port, resource, logfile, listen_only):
6976
if p.returncode:
7077
print("connection lost", file=sys.stderr)
7178
return p.returncode
79+
80+
81+
BUF_SIZE = 1024
82+
83+
async def run(check_allowed, cons, log_fd, listen_only):
84+
prev = collections.deque(maxlen=2)
85+
86+
deadline = None
87+
to_cons = b''
88+
next_cons = time.monotonic()
89+
txdelay = cons.txdelay
90+
91+
# Show a message to indicate we are waiting for output from the board
92+
msg = 'Terminal ready...press Ctrl-] twice to exit'
93+
sys.stdout.write(msg)
94+
sys.stdout.flush()
95+
erase_msg = '\b' * len(msg) + ' ' * len(msg) + '\b' * len(msg)
96+
have_output = False
97+
98+
while True:
99+
activity = bool(to_cons)
100+
try:
101+
data = cons.read(size=BUF_SIZE, timeout=0.001)
102+
if data:
103+
activity = True
104+
if not have_output:
105+
# Erase our message
106+
sys.stdout.write(erase_msg)
107+
sys.stdout.flush()
108+
have_output = True
109+
sys.stdout.buffer.write(data)
110+
sys.stdout.buffer.flush()
111+
if log_fd:
112+
log_fd.write(data)
113+
log_fd.flush()
114+
115+
except TIMEOUT:
116+
pass
117+
118+
except SerialException:
119+
break
120+
121+
if not listen_only:
122+
data = os.read(sys.stdin.fileno(), BUF_SIZE)
123+
if data:
124+
activity = True
125+
if not deadline:
126+
deadline = time.monotonic() + .5 # seconds
127+
prev.extend(data)
128+
count = prev.count(EXIT_CHAR)
129+
if count == 2:
130+
break
131+
132+
to_cons += data
133+
134+
if to_cons and time.monotonic() > next_cons:
135+
cons._write(to_cons[:1])
136+
to_cons = to_cons[1:]
137+
if txdelay:
138+
next_cons += txdelay
139+
140+
if deadline and time.monotonic() > deadline:
141+
prev.clear()
142+
deadline = None
143+
if check_allowed():
144+
break
145+
if not activity:
146+
time.sleep(.001)
147+
148+
# Blank line to move past any partial output
149+
print()
150+
151+
152+
async def internal(check_allowed, cons, logfile, listen_only):
153+
"""Start an external terminal sessions
154+
155+
This uses microcom if available, otherwise falls back to telnet.
156+
157+
Args:
158+
check_allowed (lambda): Function to call to make sure the terminal is
159+
still accessible. No args. Returns True if allowed, False if not.
160+
cons (str): ConsoleProtocol device to read/write
161+
logfile (str): Logfile to write output too, or None
162+
listen_only (bool): True to ignore keyboard input
163+
164+
Return:
165+
int: Result code
166+
"""
167+
returncode = 0
168+
old = None
169+
log_fd = None
170+
try:
171+
if not listen_only and os.isatty(sys.stdout.fileno()):
172+
fd = sys.stdin.fileno()
173+
old = termios.tcgetattr(fd)
174+
new = termios.tcgetattr(fd)
175+
new[3] = new[3] & ~(termios.ICANON | termios.ECHO | termios.ISIG)
176+
new[6][termios.VMIN] = 0
177+
new[6][termios.VTIME] = 0
178+
termios.tcsetattr(fd, termios.TCSANOW, new)
179+
180+
log_fd = None
181+
if logfile:
182+
log_fd = open(logfile, 'wb')
183+
184+
logging.info('Console start:')
185+
await run(check_allowed, cons, log_fd, listen_only)
186+
187+
except OSError as err:
188+
print('error', err)
189+
returncode = 1
190+
191+
finally:
192+
if old:
193+
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
194+
if log_fd:
195+
log_fd.close()
196+
197+
return returncode

man/labgrid-client.1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ connect to the console
243243
.INDENT 3.5
244244
.sp
245245
.EX
246-
usage: labgrid\-client console|con [\-l] [\-o] [\-\-logfile FILE] [name]
246+
usage: labgrid\-client console|con [\-i] [\-l] [\-o] [\-\-logfile FILE] [name]
247247
.EE
248248
.UNINDENT
249249
.UNINDENT
@@ -254,6 +254,11 @@ optional resource name
254254
.UNINDENT
255255
.INDENT 0.0
256256
.TP
257+
.B \-i, \-\-internal
258+
use an internal console instead of microcom
259+
.UNINDENT
260+
.INDENT 0.0
261+
.TP
257262
.B \-l, \-\-loop
258263
keep trying to connect if the console is unavailable
259264
.UNINDENT

0 commit comments

Comments
 (0)