Public Access
One firmware: the Debug Console in every build, off until switched on, with the device's own token
There is no Debug Build any more (ADR 0010, issue #68, Q188 to Q195). The console and the test commands are compiled into every firmware. It listens only while Settings > Debug Console is on, which isn't the default; off, neither its task nor its 4 KB ring exists. The token is made by the device and shown on that page; a client proves it knows it by answering a challenge with an HMAC, so it never crosses the network, and five wrong answers close the console for a minute. DBG in the Status Bar while it listens. Over USB serial only: debug on, debug token <value>, debug token new. scripts/flash.sh --debug uses them to set a device up with the developer's token. scripts/rdbg.py takes the token from -t, $RORO_DEBUG_TOKEN or the file, answers the challenge, and fetches a release's ELF to decode a crash. Gone: the cardputer-adv-debug environment, RORO_DEBUG, the +debug version, scripts/debug_flags.py, update install ... force, and the rule that a Debug Build doesn't install releases. Old clients and old firmwares don't talk to each other. Against the builds it replaces: 30 KB more flash and 88 bytes more static RAM than the release, 4 KB less RAM than the Debug Build. 468 host tests. Checked on the device: off by default, login, the pause after wrong tokens, Safe Mode with the console, the setting surviving an update, debug off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
+85
-12
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The Debug Console of a Debug Build, over Wi-Fi (TCP 2323, see ADR 0004).
|
||||
"""The Debug Console, over Wi-Fi (TCP 2323, see ADR 0010). Switch it on first: Settings > Debug Console.
|
||||
|
||||
Usage: scripts/rdbg.py [-H host] [-b] [command ...]
|
||||
Usage: scripts/rdbg.py [-H host] [-t token] [-b] [command ...]
|
||||
no command interactive: type commands, see every console line live; Ctrl-D or `quit` leaves
|
||||
command runs it and prints what follows, until the console has been quiet for a moment
|
||||
-H host the device's IP (Settings > Firmware), default $RORO_OTA_HOST
|
||||
-H host the device's IP (Settings > Debug Console), default $RORO_OTA_HOST
|
||||
-t token the device's token (also --token), as Settings > Debug Console shows it: dashes and
|
||||
case don't matter. Otherwise $RORO_DEBUG_TOKEN, otherwise ~/.config/roro9stack/debug-token
|
||||
-b also print the backlog the device sends on connecting (boot messages and so on)
|
||||
|
||||
Commands handled here as well as on the device:
|
||||
@@ -14,9 +16,11 @@ Commands handled here as well as on the device:
|
||||
put <file> [card path] copy a file to the SD card (default /updates/<name>), checked with SHA-256
|
||||
screenshot [file.png] what the screen shows (default screen-<date>.png), at 2x
|
||||
reset restart at once, even if the main loop is stuck
|
||||
The token is read from ~/.config/roro9stack/debug-token (made by the first build).
|
||||
The token never crosses the network: the device sends a challenge, and this answers with its HMAC.
|
||||
"""
|
||||
import gzip
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
@@ -26,9 +30,53 @@ import zlib
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
PORT = 2323
|
||||
TOKEN_FILE = os.path.expanduser("~/.config/roro9stack/debug-token")
|
||||
RELEASES = "https://git.twis.la/twisla/roro9stack/releases/download"
|
||||
|
||||
|
||||
def tidy_token(typed):
|
||||
"""As the device stores it (lib/debug/src/debug_auth.cpp): no dashes or spaces, in capitals."""
|
||||
tidy = "".join(c for c in typed if c not in "- \t\r\n").upper()
|
||||
return tidy.replace("O", "0").replace("I", "1").replace("L", "1") # read as the digits they look like
|
||||
|
||||
|
||||
def answer_for(token, nonce):
|
||||
"""What the device expects back for a challenge: HMAC-SHA256 of the nonce, keyed by the token."""
|
||||
return hmac.new(token.encode(), nonce, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def find_token(given):
|
||||
token = given or os.environ.get("RORO_DEBUG_TOKEN")
|
||||
if not token and os.path.exists(TOKEN_FILE):
|
||||
token = open(TOKEN_FILE).read()
|
||||
token = tidy_token(token or "")
|
||||
if not token:
|
||||
sys.exit("No token: pass -t <token>, set RORO_DEBUG_TOKEN, or put it in " + TOKEN_FILE +
|
||||
"\n(the device shows it in Settings > Debug Console)")
|
||||
return token
|
||||
|
||||
|
||||
def log_in(sock, token):
|
||||
"""Answers the device's challenge; returns the banner line, or exits saying what went wrong."""
|
||||
first = read_until(sock, b"\n", 10)
|
||||
if first is None:
|
||||
sys.exit("device: nothing came back. Is the console switched on (Settings > Debug Console)?")
|
||||
line = first.decode(errors="replace").strip()
|
||||
if line.startswith("locked"):
|
||||
sys.exit("device: closed for a minute after too many wrong tokens")
|
||||
challenge = re.search(r"challenge ([0-9a-f]{32})$", line)
|
||||
if not challenge:
|
||||
sys.exit("device: no challenge (an older firmware?): " + line[:60])
|
||||
sock.sendall((answer_for(token, bytes.fromhex(challenge.group(1))) + "\n").encode())
|
||||
banner = read_until(sock, b"\n", 15)
|
||||
if banner is None:
|
||||
sys.exit("device: no answer")
|
||||
if banner.startswith(b"denied"):
|
||||
sys.exit("device: wrong token (Settings > Debug Console shows the right one)")
|
||||
return banner
|
||||
|
||||
|
||||
def read_until(sock, marker, timeout):
|
||||
@@ -102,17 +150,40 @@ def crash_firmware(reply):
|
||||
return version.group(1) if version else None
|
||||
|
||||
|
||||
def have_elf(reply):
|
||||
"""Makes sure .pio/elves/ holds the ELF of the firmware that crashed: a release's is on Gitea."""
|
||||
folder = os.path.join(os.path.dirname(SCRIPTS), ".pio", "elves")
|
||||
key = crash_firmware(reply)
|
||||
version = re.search(r"last one in (\S+)", reply)
|
||||
version = version.group(1) if version else None
|
||||
names = os.listdir(folder) if os.path.isdir(folder) else []
|
||||
if not key or any(key in n for n in names) or not version or not re.fullmatch(r"v\d+\.\d+\.\d+", version):
|
||||
return # there already, or not a release: only tags are published
|
||||
url = f"{RELEASES}/{version}/roro9stack-{version}.elf.gz"
|
||||
try:
|
||||
elf = gzip.decompress(urllib.request.urlopen(url, timeout=60).read())
|
||||
except Exception as e:
|
||||
return print(f"(no ELF for {version} here, and none fetched from {url}: {e})")
|
||||
digest = hashlib.sha256(elf).hexdigest()[:16]
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
path = os.path.join(folder, f"{version}.{digest}.elf") # as scripts/version.py names them
|
||||
open(path, "wb").write(elf)
|
||||
print(f"(fetched the ELF of {version} from its release: {os.path.relpath(path)})")
|
||||
|
||||
|
||||
def crash(sock):
|
||||
reply = run(sock, "crash")
|
||||
trace = re.search(r"backtrace((?: 0x[0-9a-f]+)+)", reply)
|
||||
key = crash_firmware(reply)
|
||||
if trace and key:
|
||||
have_elf(reply)
|
||||
print()
|
||||
subprocess.call([os.path.join(SCRIPTS, "decode_backtrace.sh"), key] + trace.group(1).split())
|
||||
|
||||
|
||||
def coredump(sock, path):
|
||||
info = run(sock, "crash", out=None)
|
||||
have_elf(info)
|
||||
sock.sendall(b"coredump get\n")
|
||||
# One buffer throughout: the header, the size and the first bytes often share a packet.
|
||||
buf = b""
|
||||
@@ -237,25 +308,27 @@ def interactive(sock):
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
host, backlog = os.environ.get("RORO_OTA_HOST"), False
|
||||
host, backlog, token = os.environ.get("RORO_OTA_HOST"), False, None
|
||||
while args and args[0].startswith("-"):
|
||||
flag = args.pop(0)
|
||||
if flag == "-H" and args:
|
||||
host = args.pop(0)
|
||||
elif flag in ("-t", "--token") and args:
|
||||
token = args.pop(0)
|
||||
elif flag == "-b":
|
||||
backlog = True
|
||||
else:
|
||||
sys.exit(__doc__)
|
||||
if not host:
|
||||
sys.exit("No device: pass -H <ip> or set RORO_OTA_HOST\n\n" + __doc__)
|
||||
token = open(TOKEN_FILE).read().strip()
|
||||
token = find_token(token)
|
||||
|
||||
with socket.create_connection((host, PORT), timeout=10) as sock:
|
||||
sock.sendall((token + "\n").encode())
|
||||
# The device may still be finishing a previous client: wait for this connection's banner.
|
||||
banner = read_until(sock, b"Backlog follows.\n", 15)
|
||||
if banner is None:
|
||||
sys.exit("device: no banner (wrong token, or another client is connected)")
|
||||
try:
|
||||
sock = socket.create_connection((host, PORT), timeout=10)
|
||||
except OSError as e:
|
||||
sys.exit(f"device: can't connect to {host}:{PORT} ({e}). Is the console switched on (Settings > Debug Console)?")
|
||||
with sock:
|
||||
banner = log_in(sock, token)
|
||||
show = sys.stdout if backlog or not args else None
|
||||
if show:
|
||||
show.write(banner.decode(errors="replace"))
|
||||
|
||||
Reference in New Issue
Block a user