Skip to content

Commit c0d1177

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 <[email protected]>
1 parent 69f4568 commit c0d1177

File tree

3 files changed

+177
-30
lines changed

3 files changed

+177
-30
lines changed

doc/usage.rst

+11
Original file line numberDiff line numberDiff line change
@@ -872,3 +872,14 @@ like this:
872872
$ labgrid-client -p example allow sirius/john
873873
874874
To remove the allow it is currently necessary to unlock and lock the place.
875+
876+
Internal console
877+
^^^^^^^^^^^^^^^^
878+
879+
Labgrid uses microcom as its console by default. For situations where this is
880+
not suitable, an internal console is provided. To use this, provide the
881+
``--internal`` flag to the ``labgrid client` command.
882+
883+
When the internal console is used, the console transitions cleanly between use
884+
within a strategy or driver, and interactive use for the user. The console is
885+
not closed and therefore there is no loss of data.

labgrid/remote/client.py

+40-29
Original file line numberDiff line numberDiff line change
@@ -929,48 +929,57 @@ def digital_io(self):
929929
drv.set(False)
930930

931931
async def _console(self, place, target, timeout, *, logfile=None, loop=False, listen_only=False):
932+
from ..protocol import ConsoleProtocol
933+
932934
name = self.args.name
933-
from ..resource import NetworkSerialPort
934935

935-
# deactivate console drivers so we are able to connect with microcom
936-
try:
937-
con = target.get_active_driver("ConsoleProtocol")
938-
target.deactivate(con)
939-
except NoDriverFoundError:
940-
pass
936+
if not place.acquired:
937+
print("place released")
938+
return 255
941939

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

944-
# async await resources
945-
timeout = Timeout(timeout)
946-
while True:
947-
target.update_resources()
948-
if resource.avail or (not loop and timeout.expired):
949-
break
950-
await asyncio.sleep(0.1)
947+
# deactivate console drivers so we are able to connect with microcom
948+
try:
949+
con = target.get_active_driver("ConsoleProtocol")
950+
target.deactivate(con)
951+
except NoDriverFoundError:
952+
pass
951953

952-
# use zero timeout to prevent blocking sleeps
953-
target.await_resources([resource], timeout=0.0)
954+
resource = target.get_resource(NetworkSerialPort, name=name,
955+
wait_avail=False)
954956

955-
if not place.acquired:
956-
print("place released")
957-
return 255
957+
# async await resources
958+
timeout = Timeout(timeout)
959+
while True:
960+
target.update_resources()
961+
if resource.avail or (not loop and timeout.expired):
962+
break
963+
await asyncio.sleep(0.1)
958964

959-
host, port = proxymanager.get_host_and_port(resource)
965+
# use zero timeout to prevent blocking sleeps
966+
target.await_resources([resource], timeout=0.0)
967+
host, port = proxymanager.get_host_and_port(resource)
960968

961-
# check for valid resources
962-
assert port is not None, "Port is not set"
963-
try:
964-
returncode = await term.external(lambda: self.is_allowed(place),
965-
host, port, resource, logfile,
966-
listen_only)
967-
except FileNotFoundError as e:
968-
raise ServerError(f"failed to execute remote console command: {e}")
969+
# check for valid resources
970+
assert port is not None, "Port is not set"
971+
try:
972+
returncode = await term.external(lambda: self.is_allowed(place),
973+
host, port, resource, logfile,
974+
listen_only)
975+
except FileNotFoundError as e:
976+
raise ServerError(f"failed to execute remote console command: {e}")
969977

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

982+
974983
async def console(self, place, target):
975984
while True:
976985
res = await self._console(
@@ -1803,6 +1812,8 @@ def main():
18031812
subparser.set_defaults(func=ClientSession.digital_io)
18041813

18051814
subparser = subparsers.add_parser("console", aliases=("con",), help="connect to the console")
1815+
subparser.add_argument('-i', '--internal', action='store_true',
1816+
help="use an internal console instead of microcom")
18061817
subparser.add_argument(
18071818
"-l", "--loop", action="store_true", help="keep trying to connect if the console is unavailable"
18081819
)

labgrid/util/term.py

+126-1
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,121 @@ 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+
try:
170+
if not listen_only and os.isatty(sys.stdout.fileno()):
171+
fd = sys.stdin.fileno()
172+
old = termios.tcgetattr(fd)
173+
new = termios.tcgetattr(fd)
174+
new[3] = new[3] & ~(termios.ICANON | termios.ECHO | termios.ISIG)
175+
new[6][termios.VMIN] = 0
176+
new[6][termios.VTIME] = 0
177+
termios.tcsetattr(fd, termios.TCSANOW, new)
178+
179+
log_fd = None
180+
if logfile:
181+
log_fd = open(logfile, 'wb')
182+
183+
logging.info('Console start:')
184+
await run(check_allowed, cons, log_fd, listen_only)
185+
186+
except OSError as err:
187+
print('error', err)
188+
returncode = 1
189+
190+
finally:
191+
if old:
192+
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
193+
if log_fd:
194+
log_fd.close()
195+
196+
return returncode

0 commit comments

Comments
 (0)