Skip to content

Commit 90f71fe

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 10b5326 commit 90f71fe

File tree

3 files changed

+173
-30
lines changed

3 files changed

+173
-30
lines changed

doc/usage.rst

Lines changed: 11 additions & 0 deletions
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

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

948948
async def _console(self, place, target, timeout, *, logfile=None, loop=False, listen_only=False):
949+
from ..protocol import ConsoleProtocol
950+
949951
name = self.args.name
950-
from ..resource import NetworkSerialPort
951952

952-
# deactivate console drivers so we are able to connect with microcom
953-
try:
954-
con = target.get_active_driver("ConsoleProtocol")
955-
target.deactivate(con)
956-
except NoDriverFoundError:
957-
pass
953+
if not place.acquired:
954+
print("place released")
955+
return 255
958956

959-
resource = target.get_resource(NetworkSerialPort, name=name, wait_avail=False)
957+
if self.args.internal or os.environ.get("LG_CONSOLE") == "internal":
958+
console = target.get_driver(ConsoleProtocol, name=name)
959+
returncode = await term.internal(lambda: self.is_allowed(place), console, logfile, listen_only)
960+
else:
961+
from ..resource import NetworkSerialPort
960962

961-
# async await resources
962-
timeout = Timeout(timeout)
963-
while True:
964-
target.update_resources()
965-
if resource.avail or (not loop and timeout.expired):
966-
break
967-
await asyncio.sleep(0.1)
963+
# deactivate console drivers so we are able to connect with microcom
964+
try:
965+
con = target.get_active_driver("ConsoleProtocol")
966+
target.deactivate(con)
967+
except NoDriverFoundError:
968+
pass
968969

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

972-
if not place.acquired:
973-
print("place released")
974-
return 255
972+
# async await resources
973+
timeout = Timeout(timeout)
974+
while True:
975+
target.update_resources()
976+
if resource.avail or (not loop and timeout.expired):
977+
break
978+
await asyncio.sleep(0.1)
975979

976-
host, port = proxymanager.get_host_and_port(resource)
980+
# use zero timeout to prevent blocking sleeps
981+
target.await_resources([resource], timeout=0.0)
982+
host, port = proxymanager.get_host_and_port(resource)
977983

978-
# check for valid resources
979-
assert port is not None, "Port is not set"
980-
try:
981-
returncode = await term.external(
982-
lambda: self.is_allowed(place), host, port, resource, logfile, listen_only
983-
)
984-
except FileNotFoundError as e:
985-
raise ServerError(f"failed to execute remote console command: {e}")
984+
# check for valid resources
985+
assert port is not None, "Port is not set"
986+
try:
987+
returncode = await term.external(
988+
lambda: self.is_allowed(place), host, port, resource, logfile, listen_only
989+
)
990+
except FileNotFoundError as e:
991+
raise ServerError(f"failed to execute remote console command: {e}")
986992

987993
# Raise an exception if the place was released
988994
self._check_allowed(place)
@@ -1817,6 +1823,7 @@ def main():
18171823
subparser.set_defaults(func=ClientSession.digital_io)
18181824

18191825
subparser = subparsers.add_parser("console", aliases=("con",), help="connect to the console")
1826+
subparser.add_argument("-i", "--internal", action="store_true", help="use an internal console instead of microcom")
18201827
subparser.add_argument(
18211828
"-l", "--loop", action="store_true", help="keep trying to connect if the console is unavailable"
18221829
)

labgrid/util/term.py

Lines changed: 126 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,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)