Skip to content

Commit ac1ee68

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 9080620 commit ac1ee68

File tree

3 files changed

+173
-30
lines changed

3 files changed

+173
-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

+36-29
Original file line numberDiff line numberDiff line change
@@ -955,43 +955,49 @@ def digital_io(self):
955955
drv.set(False)
956956

957957
async def _console(self, place, target, timeout, *, logfile=None, loop=False, listen_only=False):
958+
from ..protocol import ConsoleProtocol
959+
958960
name = self.args.name
959-
from ..resource import NetworkSerialPort
960961

961-
# deactivate console drivers so we are able to connect with microcom
962-
try:
963-
con = target.get_active_driver("ConsoleProtocol")
964-
target.deactivate(con)
965-
except NoDriverFoundError:
966-
pass
962+
if not place.acquired:
963+
print("place released")
964+
return 255
967965

968-
resource = target.get_resource(NetworkSerialPort, name=name, wait_avail=False)
966+
if self.args.internal or os.environ.get("LG_CONSOLE") == "internal":
967+
console = target.get_driver(ConsoleProtocol, name=name)
968+
returncode = await term.internal(lambda: self.is_allowed(place), console, logfile, listen_only)
969+
else:
970+
from ..resource import NetworkSerialPort
969971

970-
# async await resources
971-
timeout = Timeout(timeout)
972-
while True:
973-
target.update_resources()
974-
if resource.avail or (not loop and timeout.expired):
975-
break
976-
await asyncio.sleep(0.1)
972+
# deactivate console drivers so we are able to connect with microcom
973+
try:
974+
con = target.get_active_driver("ConsoleProtocol")
975+
target.deactivate(con)
976+
except NoDriverFoundError:
977+
pass
977978

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

981-
if not place.acquired:
982-
print("place released")
983-
return 255
981+
# async await resources
982+
timeout = Timeout(timeout)
983+
while True:
984+
target.update_resources()
985+
if resource.avail or (not loop and timeout.expired):
986+
break
987+
await asyncio.sleep(0.1)
984988

985-
host, port = proxymanager.get_host_and_port(resource)
989+
# use zero timeout to prevent blocking sleeps
990+
target.await_resources([resource], timeout=0.0)
991+
host, port = proxymanager.get_host_and_port(resource)
986992

987-
# check for valid resources
988-
assert port is not None, "Port is not set"
989-
try:
990-
returncode = await term.external(
991-
lambda: self.is_allowed(place), host, port, resource, logfile, listen_only
992-
)
993-
except FileNotFoundError as e:
994-
raise ServerError(f"failed to execute remote console command: {e}")
993+
# check for valid resources
994+
assert port is not None, "Port is not set"
995+
try:
996+
returncode = await term.external(
997+
lambda: self.is_allowed(place), host, port, resource, logfile, listen_only
998+
)
999+
except FileNotFoundError as e:
1000+
raise ServerError(f"failed to execute remote console command: {e}")
9951001

9961002
# Raise an exception if the place was released
9971003
self._check_allowed(place)
@@ -1826,6 +1832,7 @@ def main():
18261832
subparser.set_defaults(func=ClientSession.digital_io)
18271833

18281834
subparser = subparsers.add_parser("console", aliases=("con",), help="connect to the console")
1835+
subparser.add_argument("-i", "--internal", action="store_true", help="use an internal console instead of microcom")
18291836
subparser.add_argument(
18301837
"-l", "--loop", action="store_true", help="keep trying to connect if the console is unavailable"
18311838
)

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)