Trap 23: 4个可复用台架探针迁入MDK-ARM/bench_diag/并提交,10个一次性甄别脚本已删除
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
# Reusable bench probe (Trap 23 boundary scan; lives in MDK-ARM/bench_diag/).
|
||||
# Boundary probe for Trap 23 residual: abort an in-flight STOR, wait G ms, then run a
|
||||
# fresh STOR(4096) and measure BOTH directions INDEPENDENTLY:
|
||||
# ab_stored / fu_stored = device-side byte count, authoritative, parsed from the
|
||||
# device log line "UPLOAD '<name>' <n> bytes OK"
|
||||
# fu_retr = client-side received length + sha256 (fresh session)
|
||||
# The abort->reopen gap G is swept so the temporal boundary can be read off directly.
|
||||
#
|
||||
# Round:
|
||||
# 1. abort phase : login -> PASV -> STOR /aNNNN.bin (8192B burst) -> abort (RST or FIN)
|
||||
# 2. gap : sleep G ms (also measured: abort -> next control/data connect)
|
||||
# 3. follow-up : login -> PASV -> STOR /fNNNN.bin (4096B, graceful data close) -> 226
|
||||
# 4. verify : fresh session -> RETR /fNNNN.bin -> length + sha256 -> DELE
|
||||
# 5. classify : UP_TRUNC (device stored != 4096) / DN_TRUNC (stored ok, client short)
|
||||
#
|
||||
# --control drops phase 1 entirely (no preceding abort) = baseline defect rate.
|
||||
import argparse, hashlib, os, random, re, socket, struct, sys, time
|
||||
|
||||
DEVLOG = os.path.join("tools", "serial_tool", "state", "serial.log")
|
||||
|
||||
|
||||
def log(m):
|
||||
sys.stdout.write(m + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
class Ctl:
|
||||
"""Minimal raw FTP control channel (no ftplib) so close semantics/timing are exact."""
|
||||
|
||||
def __init__(self, ip, port, timeout=10.0):
|
||||
self.s = socket.create_connection((ip, port), timeout=timeout)
|
||||
self.f = self.s.makefile("rb")
|
||||
self.banner = self.resp()
|
||||
|
||||
def resp(self):
|
||||
line = self.f.readline()
|
||||
if not line:
|
||||
raise EOFError("control closed by peer")
|
||||
txt = line.decode("latin1").rstrip("\r\n")
|
||||
code = txt[:3]
|
||||
if len(txt) > 3 and txt[3] == "-":
|
||||
while True:
|
||||
l2 = self.f.readline()
|
||||
if not l2:
|
||||
break
|
||||
t2 = l2.decode("latin1").rstrip("\r\n")
|
||||
txt += "\n" + t2
|
||||
if t2.startswith(code + " "):
|
||||
break
|
||||
return txt
|
||||
|
||||
def cmd(self, line):
|
||||
self.s.sendall((line + "\r\n").encode())
|
||||
return self.resp()
|
||||
|
||||
def login(self):
|
||||
self.cmd("USER anonymous")
|
||||
self.cmd("PASS bench@local")
|
||||
self.cmd("TYPE I")
|
||||
|
||||
def pasv(self, timeout=10.0):
|
||||
r = self.cmd("PASV")
|
||||
m = re.search(r"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)", r)
|
||||
if not m:
|
||||
raise RuntimeError("PASV failed: %r" % r[:80])
|
||||
host = ".".join(m.group(i) for i in (1, 2, 3, 4))
|
||||
port = (int(m.group(5)) << 8) | int(m.group(6))
|
||||
return socket.create_connection((host, port), timeout=timeout)
|
||||
|
||||
def close(self):
|
||||
"""Close BOTH the makefile handle and the socket: leaving the makefile open keeps
|
||||
the fd alive, no FIN is sent, and the device holds the stale connection until its
|
||||
chip TCP timeout (~2.5s of :21 refusals) -- an artefact, not a device defect."""
|
||||
try:
|
||||
self.f.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.s.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def abort(self, fin):
|
||||
"""Tear the control channel down with the same semantic as the trigger."""
|
||||
if fin:
|
||||
try:
|
||||
self.s.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
||||
self.close()
|
||||
|
||||
def quit(self):
|
||||
try:
|
||||
self.cmd("QUIT")
|
||||
except Exception:
|
||||
pass
|
||||
self.close()
|
||||
|
||||
|
||||
def abort_sock(s, fin, plain=False):
|
||||
if plain:
|
||||
# pure FIN: plain close, no shutdown(SHUT_RD) -- so the peer's FIN arriving later is
|
||||
# NOT answered with an RST (which is what shutdown(SHUT_RDWR) causes on Windows)
|
||||
try:
|
||||
s.close()
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
if fin:
|
||||
try:
|
||||
s.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
||||
try:
|
||||
s.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def connect_ctl(ip, port, trace=False, tries=4, spacing=0.15):
|
||||
"""Connect the control channel, tolerating a short post-abort refusal window.
|
||||
|
||||
Returns (Ctl, seconds_since_first_attempt_or_None)."""
|
||||
t0 = time.monotonic()
|
||||
last = None
|
||||
for k in range(tries):
|
||||
try:
|
||||
return Ctl(ip, port), (time.monotonic() - t0)
|
||||
except Exception as e:
|
||||
last = e
|
||||
if trace:
|
||||
log(" [trace] connect attempt %d failed: %s" % (k + 1, type(e).__name__))
|
||||
time.sleep(spacing)
|
||||
raise last
|
||||
|
||||
|
||||
def read_until_eof(s, deadline_s):
|
||||
got = bytearray()
|
||||
s.settimeout(deadline_s)
|
||||
while True:
|
||||
try:
|
||||
b = s.recv(65536)
|
||||
except socket.timeout:
|
||||
break
|
||||
except OSError:
|
||||
break
|
||||
if not b:
|
||||
break
|
||||
got.extend(b)
|
||||
return bytes(got)
|
||||
|
||||
|
||||
def devlog_offset(path):
|
||||
try:
|
||||
return os.path.getsize(path)
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
|
||||
def devlog_new(path, off):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
f.seek(off)
|
||||
return f.read().decode("utf-8", "replace")
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def parse_uploads(text):
|
||||
"""name -> stored size, from device log 'UPLOAD '<name>' <n> bytes OK'."""
|
||||
out = {}
|
||||
for m in re.finditer(r"UPLOAD '([^']*)'\s+(\d+) bytes OK", text):
|
||||
out[os.path.basename(m.group(1))] = int(m.group(2))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--ip", default="192.168.1.100")
|
||||
ap.add_argument("--port", type=int, default=21)
|
||||
ap.add_argument("--n", type=int, default=8)
|
||||
ap.add_argument("--gap", type=int, default=0, help="abort -> reopen gap in ms")
|
||||
ap.add_argument("--size", type=int, default=4096, help="follow-up STOR payload bytes")
|
||||
ap.add_argument("--abort-send", type=int, default=8192, help="aborted STOR payload bytes")
|
||||
ap.add_argument("--fin", action="store_true", help="graceful FIN abort instead of RST")
|
||||
ap.add_argument("--ctl-abort", action="store_true",
|
||||
help="also RST the control channel (aggressive, kills :21 for ~19s)")
|
||||
ap.add_argument("--fast-teardown", action="store_true",
|
||||
help="close the control channel ~30ms after the abort instead of 500ms")
|
||||
ap.add_argument("--followups", type=int, default=1,
|
||||
help="consecutive STORs after one abort (is the damage one-shot?)")
|
||||
ap.add_argument("--plain-close", action="store_true",
|
||||
help="close the data socket with a plain close (pure FIN, no SHUT_RD)")
|
||||
ap.add_argument("--control", action="store_true", help="no abort phase (baseline)")
|
||||
ap.add_argument("--trace", action="store_true", help="per-phase progress + traceback")
|
||||
ap.add_argument("--devlog", default=DEVLOG)
|
||||
a = ap.parse_args()
|
||||
|
||||
off = devlog_offset(a.devlog)
|
||||
payload = bytes(random.getrandbits(8) for _ in range(a.size))
|
||||
want = hashlib.sha256(payload).hexdigest()
|
||||
|
||||
res = {"OK": 0, "UP_TRUNC": 0, "DN_TRUNC": 0, "ERR": 0}
|
||||
log("== gap sweep: gap=%dms abort=%s control=%s n=%d size=%d"
|
||||
% (a.gap, "FIN" if a.fin else "RST", a.control, a.n, a.size))
|
||||
for i in range(1, a.n + 1):
|
||||
ab_name = "/a%04d.bin" % i
|
||||
fu_name = "/f%04d.bin" % i
|
||||
t_abort = None
|
||||
dt_ctl = dt_data = -1.0
|
||||
try:
|
||||
# ---- 1. abort phase ----
|
||||
if not a.control:
|
||||
c = Ctl(a.ip, a.port)
|
||||
if a.trace:
|
||||
log(" [trace] abort-phase control connected: %r" % c.banner[:40])
|
||||
c.login()
|
||||
d = c.pasv()
|
||||
c.cmd("STOR " + ab_name)
|
||||
d.sendall(bytes(random.getrandbits(8) for _ in range(a.abort_send)))
|
||||
abort_sock(d, a.fin, a.plain_close)
|
||||
t_abort = time.monotonic()
|
||||
if a.ctl_abort:
|
||||
# aggressive variant: RST the control channel too (kills the :21
|
||||
# listener for ~19s until the chip's own TCP timeout fires -- see
|
||||
# device log 'sock1 TIMEOUT' / 'sock1 relisten quarantine')
|
||||
c.abort(True)
|
||||
else:
|
||||
# faithful to ftp_abort_probe/real client: data channel aborted,
|
||||
# control channel closed gracefully. --fast-teardown shrinks the
|
||||
# settle sleep 500ms -> 30ms so the follow-up can land inside the
|
||||
# post-abort window (this is the boundary variable under test).
|
||||
time.sleep(0.03 if a.fast_teardown else 0.5)
|
||||
try:
|
||||
c.resp()
|
||||
except Exception:
|
||||
pass
|
||||
c.quit()
|
||||
# ---- 2. gap ----
|
||||
if a.gap > 0:
|
||||
time.sleep(a.gap / 1000.0)
|
||||
# ---- 3./4. follow-up STOR(s) + verify ----
|
||||
fu_names = [("/f%04d_%d.bin" % (i, k)) if a.followups > 1 else ("/f%04d.bin" % i)
|
||||
for k in range(a.followups)]
|
||||
for k, fu_name in enumerate(fu_names):
|
||||
c2, t_accept = connect_ctl(a.ip, a.port, a.trace)
|
||||
if a.trace:
|
||||
log(" [trace] follow-up control connected: %r" % c2.banner[:40])
|
||||
dt_ctl = (time.monotonic() - t_abort) if t_abort else -1.0
|
||||
c2.login()
|
||||
d2 = c2.pasv()
|
||||
dt_data = (time.monotonic() - t_abort) if t_abort else -1.0
|
||||
c2.cmd("STOR " + fu_name)
|
||||
d2.sendall(payload)
|
||||
try:
|
||||
d2.shutdown(socket.SHUT_WR)
|
||||
except OSError:
|
||||
pass
|
||||
r = c2.resp()
|
||||
d2.close()
|
||||
c2.quit()
|
||||
# verify: fresh session, RETR
|
||||
c3, _ = connect_ctl(a.ip, a.port, a.trace)
|
||||
if a.trace:
|
||||
log(" [trace] verify control connected: %r" % c3.banner[:40])
|
||||
c3.login()
|
||||
d3 = c3.pasv()
|
||||
c3.cmd("RETR " + fu_name)
|
||||
got = read_until_eof(d3, 15.0)
|
||||
d3.close()
|
||||
try:
|
||||
c3.cmd("DELE " + fu_name)
|
||||
if not a.control:
|
||||
c3.cmd("DELE " + ab_name)
|
||||
except Exception:
|
||||
pass
|
||||
c3.quit()
|
||||
retr = len(got)
|
||||
sha_ok = hashlib.sha256(got).hexdigest() == want
|
||||
# stored sizes are resolved after the run from the device log; make a
|
||||
# provisional classification from the client side only
|
||||
cls = "OK" if (retr == a.size and sha_ok) else "DN_TRUNC"
|
||||
log(" %2d.%d resp=%-4s retr=%5d/%d sha=%-4s t_accept=%6.0fms dt_data=%6.0fms provisional=%s"
|
||||
% (i, k + 1, r[:3], retr, a.size, "OK" if sha_ok else "BAD",
|
||||
(t_accept or 0.0) * 1e3, dt_data * 1e3, cls))
|
||||
res[cls] += 1
|
||||
except Exception as e:
|
||||
res["ERR"] += 1
|
||||
log(" %2d EXC %s: %s" % (i, type(e).__name__, str(e)[:70]))
|
||||
if a.trace:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
time.sleep(0.3)
|
||||
|
||||
# ---- device-log truth: authoritative stored sizes ----
|
||||
up = parse_uploads(devlog_new(a.devlog, off))
|
||||
log("-- device log: %d UPLOAD records" % len(up))
|
||||
up_trunc = 0
|
||||
for i in range(1, a.n + 1):
|
||||
for k in range(a.followups):
|
||||
nm = ("f%04d_%d.bin" % (i, k)) if a.followups > 1 else ("f%04d.bin" % i)
|
||||
sz = up.get(nm)
|
||||
if sz is None:
|
||||
log(" %s : (no UPLOAD record)" % nm)
|
||||
elif sz != a.size:
|
||||
up_trunc += 1
|
||||
log(" %s : stored=%d <== UPLOAD-SIDE TRUNCATION" % (nm, sz))
|
||||
ab = [(k, v) for k, v in up.items() if k.startswith("a")]
|
||||
if ab:
|
||||
log("-- aborted-phase stored sizes (expected partial): %s"
|
||||
% ", ".join("%s=%d" % kv for kv in sorted(ab)))
|
||||
log("SUMMARY gap=%dms abort=%s control=%s n=%d client_ok=%d client_dn_trunc=%d "
|
||||
"device_up_trunc=%d err=%d"
|
||||
% (a.gap, "FIN" if a.fin else "RST", a.control, a.n, res["OK"],
|
||||
res["DN_TRUNC"], up_trunc, res["ERR"]))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
# PC-side control experiment (no device involved; lives in MDK-ARM/bench_diag/).
|
||||
# Question: is "sendall(4096) then close()" against a SLOW receiver (small SO_RCVBUF,
|
||||
# no prompt read) lossless on a normal TCP stack? If yes here but lossy against the
|
||||
# device, the loss is device-side; if lossy here too, it is generic TCP/client behaviour.
|
||||
import socket, sys, threading, time
|
||||
|
||||
def log(m):
|
||||
sys.stdout.write(m + "\n"); sys.stdout.flush()
|
||||
|
||||
def run(rcvbuf, read_delay, send_size=4096):
|
||||
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, rcvbuf)
|
||||
srv.bind(("127.0.0.1", 0))
|
||||
srv.listen(1)
|
||||
port = srv.getsockname()[1]
|
||||
got = bytearray()
|
||||
res = {}
|
||||
|
||||
def server():
|
||||
c, _ = srv.accept()
|
||||
if read_delay:
|
||||
time.sleep(read_delay) # receiver does not read promptly
|
||||
c.settimeout(3.0)
|
||||
try:
|
||||
while True:
|
||||
b = c.recv(65536)
|
||||
if not b:
|
||||
break
|
||||
got.extend(b)
|
||||
except Exception as e:
|
||||
res["err"] = repr(e)
|
||||
res["n"] = len(got)
|
||||
c.close()
|
||||
|
||||
t = threading.Thread(target=server, daemon=True)
|
||||
t.start()
|
||||
time.sleep(0.15)
|
||||
cli = socket.create_connection(("127.0.0.1", port), timeout=5.0)
|
||||
cli.sendall(bytes([(i * 13 + 5) & 0xFF for i in range(send_size)]))
|
||||
cli.close() # close immediately after sendall
|
||||
t.join(timeout=8)
|
||||
srv.close()
|
||||
ok = res.get("n") == send_size
|
||||
log(" PC-control rcvbuf=%-6d read_delay=%-4s sent=%d received=%s %s %s"
|
||||
% (rcvbuf, read_delay, send_size, res.get("n"),
|
||||
"OK" if ok else "LOSS", res.get("err", "")))
|
||||
return res.get("n")
|
||||
|
||||
log("=== PC-side reference (loopback): does an immediate close() lose data when the")
|
||||
log("=== receiver reads late / has a tiny buffer? Expected on a correct stack: no loss.")
|
||||
run(2048, 0.0)
|
||||
run(2048, 1.5)
|
||||
run(65536, 0.0)
|
||||
run(65536, 1.5)
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
# Reusable bench probe (post-abort recovery timeline; lives in MDK-ARM/bench_diag/).
|
||||
# RECOVERY TIMELINE after an aborted transfer: how long is the device impaired, and is
|
||||
# the impairment specific to the FTP application (lftpd) or common to the net/chip layer?
|
||||
#
|
||||
# Trigger (t=0):
|
||||
# --target ftp : raw control login -> PASV -> data conn -> STOR -> 8192B burst -> RST the
|
||||
# DATA channel; control channel teardown is either
|
||||
# clean = 0.5s settle, read the reply, QUIT (what real clients do)
|
||||
# fast = QUIT ~20ms after the abort (events coincide)
|
||||
# --target sink: :7003 connect -> INIT -> LCDMEM -> N keepalive frames (RX in flight,
|
||||
# unread) -> RST (NO lftpd code involved at all)
|
||||
#
|
||||
# Then every --step ms a full short session is attempted until it succeeds --first-ok
|
||||
# marks the moment the device is usable again-- and every attempt's payload integrity is
|
||||
# recorded (FTP: NOOP reply; sink: 3209B LCDMEM frame length + CRC, and the echo count of a
|
||||
# keepalive burst = RX direction integrity).
|
||||
import argparse, re, socket, struct, sys, time
|
||||
|
||||
TAG_CLIENT = 0xAA
|
||||
CMD_KEEP = 0
|
||||
CMD_INIT = 1
|
||||
CMD_LCDMEM = 3
|
||||
MEMSIZE = 3200
|
||||
LCDMEM_TOTAL = 3209
|
||||
ACK_LEN = 5
|
||||
|
||||
|
||||
def log(m):
|
||||
sys.stdout.write(m + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def xor_bytes(b):
|
||||
c = 0
|
||||
for x in b:
|
||||
c ^= x
|
||||
return c
|
||||
|
||||
|
||||
def req(cmd, data=b""):
|
||||
body = bytes(data)
|
||||
return (bytes([TAG_CLIENT, cmd, (len(body) >> 8) & 0xFF, len(body) & 0xFF]) + body
|
||||
+ bytes([xor_bytes(body)]))
|
||||
|
||||
|
||||
def read_exact(s, want, deadline):
|
||||
buf = bytearray()
|
||||
while len(buf) < want:
|
||||
left = deadline - time.monotonic()
|
||||
if left <= 0:
|
||||
break
|
||||
s.settimeout(left)
|
||||
try:
|
||||
d = s.recv(want - len(buf))
|
||||
except (socket.timeout, OSError):
|
||||
break
|
||||
if not d:
|
||||
break
|
||||
buf.extend(d)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def read_frame(s, deadline):
|
||||
"""Read exactly one RemoDispBus frame -> (got, total, crc_ok)."""
|
||||
buf = bytearray()
|
||||
total = None
|
||||
while True:
|
||||
want = (4 - len(buf)) if total is None else (total - len(buf))
|
||||
if want <= 0:
|
||||
break
|
||||
left = deadline - time.monotonic()
|
||||
if left <= 0:
|
||||
break
|
||||
s.settimeout(left)
|
||||
try:
|
||||
d = s.recv(min(want, 4096))
|
||||
except (socket.timeout, OSError):
|
||||
break
|
||||
if not d:
|
||||
break
|
||||
buf.extend(d)
|
||||
if total is None and len(buf) >= 4:
|
||||
total = 4 + (((buf[2] << 8) | buf[3])) + 1
|
||||
if total is None or len(buf) < total:
|
||||
return len(buf), (total or 0), False
|
||||
body = bytes(buf[4:total - 1])
|
||||
return len(buf), total, (buf[total - 1] == xor_bytes(body))
|
||||
|
||||
|
||||
def rst(s):
|
||||
try:
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
s.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ------------------------------- FTP target -------------------------------
|
||||
|
||||
def ftp_recv(s, timeout=2.0):
|
||||
s.settimeout(timeout)
|
||||
try:
|
||||
return s.recv(4096).decode("latin1", "replace")
|
||||
except (socket.timeout, OSError):
|
||||
return ""
|
||||
|
||||
|
||||
def ftp_trigger(ip, port, teardown, name, send=8192):
|
||||
ctl = socket.create_connection((ip, port), timeout=5.0)
|
||||
ftp_recv(ctl)
|
||||
ctl.sendall(b"USER anonymous\r\n"); ftp_recv(ctl)
|
||||
ctl.sendall(b"TYPE I\r\n"); ftp_recv(ctl)
|
||||
ctl.sendall(b"PASV\r\n")
|
||||
r = ftp_recv(ctl)
|
||||
m = re.search(r"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)", r)
|
||||
if not m:
|
||||
raise RuntimeError("PASV failed: %r" % r[:60])
|
||||
dhost = ".".join(m.group(i) for i in (1, 2, 3, 4))
|
||||
dport = (int(m.group(5)) << 8) | int(m.group(6))
|
||||
d = socket.create_connection((dhost, dport), timeout=5.0)
|
||||
ctl.sendall(("STOR " + name + "\r\n").encode())
|
||||
ftp_recv(ctl, 1.0)
|
||||
d.sendall(bytes((i * 7 + 3) & 0xFF for i in range(send)))
|
||||
t = time.monotonic()
|
||||
rst(d) # abort mid-upload
|
||||
if teardown == "fast":
|
||||
time.sleep(0.02)
|
||||
ftp_recv(ctl, 0.2)
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
ftp_recv(ctl, 1.0)
|
||||
try:
|
||||
ctl.sendall(b"QUIT\r\n")
|
||||
ftp_recv(ctl, 0.5)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
ctl.close()
|
||||
except OSError:
|
||||
pass
|
||||
return t
|
||||
|
||||
|
||||
def ftp_probe(ip, port, budget=0.4):
|
||||
"""One short real session: connect -> NOOP reply integrity -> QUIT."""
|
||||
t = time.monotonic()
|
||||
try:
|
||||
s = socket.create_connection((ip, port), timeout=budget)
|
||||
except Exception as e:
|
||||
return False, "REFUSED" if isinstance(e, ConnectionRefusedError) else type(e).__name__
|
||||
banner = ftp_recv(s, budget)
|
||||
if not banner.startswith("220"):
|
||||
rst(s)
|
||||
return False, "nobanner"
|
||||
s.sendall(b"NOOP\r\n")
|
||||
r = ftp_recv(s, budget)
|
||||
ok = r.startswith("2")
|
||||
try:
|
||||
s.sendall(b"QUIT\r\n"); ftp_recv(s, 0.3)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
s.close()
|
||||
except OSError:
|
||||
pass
|
||||
return True, ("ok" if ok else "noop=%r" % r[:16])
|
||||
|
||||
|
||||
# ------------------------------- sink target -------------------------------
|
||||
|
||||
def sink_trigger(ip, port, burst):
|
||||
s = socket.create_connection((ip, port), timeout=5.0)
|
||||
s.sendall(req(CMD_INIT))
|
||||
read_frame(s, time.monotonic() + 5.0)
|
||||
if burst:
|
||||
s.sendall(req(CMD_KEEP) * burst)
|
||||
s.sendall(req(CMD_LCDMEM, bytes(4)))
|
||||
t = time.monotonic()
|
||||
rst(s)
|
||||
return t
|
||||
|
||||
|
||||
def sink_probe(ip, port, burst=0, budget=2.0):
|
||||
"""One real session: INIT + keepalive burst echo count + LCDMEM frame integrity."""
|
||||
try:
|
||||
s = socket.create_connection((ip, port), timeout=budget)
|
||||
except Exception as e:
|
||||
return False, "REFUSED" if isinstance(e, ConnectionRefusedError) else type(e).__name__
|
||||
dl = time.monotonic() + budget
|
||||
s.sendall(req(CMD_INIT))
|
||||
ini_got, _, ini_crc = read_frame(s, dl)
|
||||
ack = -1
|
||||
if burst:
|
||||
s.sendall(req(CMD_KEEP) * burst)
|
||||
ack = len(read_exact(s, burst * ACK_LEN, time.monotonic() + budget))
|
||||
s.sendall(req(CMD_LCDMEM, bytes(4)))
|
||||
got, total, crc = read_frame(s, time.monotonic() + budget)
|
||||
try:
|
||||
s.sendall(b"") # no-op: close gracefully (FIN) so we never re-trigger
|
||||
s.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
s.close()
|
||||
except OSError:
|
||||
pass
|
||||
ok = (ini_got == 13 and got == LCDMEM_TOTAL and crc and (not burst or ack == burst * ACK_LEN))
|
||||
return True, ("ok" if ok else "init=%d lcd=%d/%d crc=%s ack=%s"
|
||||
% (ini_got, got, LCDMEM_TOTAL, "OK" if crc else "BAD",
|
||||
ack if burst else "-"))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--target", choices=("ftp", "sink"), default="ftp")
|
||||
ap.add_argument("--ip", default="192.168.1.100")
|
||||
ap.add_argument("--port", type=int, default=0, help="0 = 21 for ftp, 7003 for sink")
|
||||
ap.add_argument("--rounds", type=int, default=3)
|
||||
ap.add_argument("--span", type=int, default=4000, help="timeline length in ms")
|
||||
ap.add_argument("--step", type=int, default=100)
|
||||
ap.add_argument("--teardown", choices=("clean", "fast"), default="clean")
|
||||
ap.add_argument("--burst", type=int, default=400, help="sink: keepalive frames in flight")
|
||||
a = ap.parse_args()
|
||||
port = a.port or (21 if a.target == "ftp" else 7003)
|
||||
|
||||
log("== recovery timeline target=%s port=%d teardown=%s burst=%d step=%dms span=%dms"
|
||||
% (a.target, port, a.teardown, a.burst, a.step, a.span))
|
||||
for r in range(1, a.rounds + 1):
|
||||
if a.target == "ftp":
|
||||
t0 = ftp_trigger(a.ip, port, a.teardown, "/r%04d.bin" % r)
|
||||
else:
|
||||
t0 = sink_trigger(a.ip, port, a.burst)
|
||||
seq = []
|
||||
first_ok = None
|
||||
t = 0
|
||||
while t < a.span:
|
||||
time.sleep(a.step / 1000.0)
|
||||
t = int((time.monotonic() - t0) * 1e3)
|
||||
if a.target == "ftp":
|
||||
up, det = ftp_probe(a.ip, port)
|
||||
else:
|
||||
up, det = sink_probe(a.ip, port, a.burst)
|
||||
seq.append("%d:%s" % (t, det))
|
||||
if up:
|
||||
if first_ok is None:
|
||||
first_ok = t
|
||||
if det == "ok" and first_ok is not None and t > first_ok + 400:
|
||||
break
|
||||
log(" round %d first_usable=%s ms" % (r, first_ok if first_ok is not None else ">%d" % a.span))
|
||||
log(" " + " | ".join(seq))
|
||||
time.sleep(6.0) # let the device fully settle before the next trigger
|
||||
log("SUMMARY target=%s teardown=%s rounds=%d done" % (a.target, a.teardown, a.rounds))
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
# Reusable bench probe (non-FTP :7003 close->reopen boundary; lives in MDK-ARM/bench_diag/).
|
||||
# Non-FTP boundary probe: the same close->reopen recycle, but on the raw RemoDispBus
|
||||
# service (:7003, Drivers/BSP/UI/ui_net_sink.c), which shares the net/chip layer with FTP
|
||||
# and none of lftpd's code. If a rapid recycle damages data here too, the defect lives in
|
||||
# the net/chip layer and FTP only makes it visible.
|
||||
#
|
||||
# Session A (the trigger): connect -> CMD_INIT -> CMD_LCDMEM (3209B, CRC) -> [--burst N
|
||||
# keepalive frames queued unread = data still in flight at abort] -> abort (RST / --fin).
|
||||
# Gap G ms.
|
||||
# Session B (the victim): connect -> [--burst N keepalive frames -> count the echoed acks,
|
||||
# this is the RX direction analogue of "upload truncated to 2048B"] -> CMD_INIT ->
|
||||
# CMD_LCDMEM -> frame length + CRC (device->client analogue).
|
||||
#
|
||||
# Classification per round:
|
||||
# RX_SHORT : session B echoed fewer acks than frames sent (client->device data cut short)
|
||||
# TX_SHORT : session B LCDMEM frame short of 3209 (device->client data cut short)
|
||||
# CORRUPT : full length but CRC mismatch / unexpected frame
|
||||
# OK : everything intact
|
||||
import argparse, socket, struct, sys, time
|
||||
|
||||
TAG_CLIENT = 0xAA
|
||||
TAG_DEVICE = 0xBB
|
||||
CMD_KEEP = 0
|
||||
CMD_INIT = 1
|
||||
CMD_LCDMEM = 3
|
||||
MEMSIZE = 3200
|
||||
LCDMEM_TOTAL = 4 + 4 + MEMSIZE + 1 # 3209
|
||||
ACK_LEN = 5 # empty frame reply to a keepalive
|
||||
|
||||
|
||||
def log(m):
|
||||
sys.stdout.write(m + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def xor_bytes(b):
|
||||
c = 0
|
||||
for x in b:
|
||||
c ^= x
|
||||
return c
|
||||
|
||||
|
||||
def req(cmd, data=b""):
|
||||
body = bytes(data)
|
||||
return (bytes([TAG_CLIENT, cmd, (len(body) >> 8) & 0xFF, len(body) & 0xFF]) + body
|
||||
+ bytes([xor_bytes(body)]))
|
||||
|
||||
|
||||
def read_frame(s, deadline):
|
||||
"""Read exactly one device frame; returns total/got/crc_ok/short."""
|
||||
buf = bytearray()
|
||||
total = None
|
||||
while True:
|
||||
want = (4 - len(buf)) if total is None else (total - len(buf))
|
||||
if want <= 0:
|
||||
break
|
||||
left = deadline - time.monotonic()
|
||||
if left <= 0:
|
||||
return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True}
|
||||
s.settimeout(left)
|
||||
try:
|
||||
d = s.recv(min(want, 4096))
|
||||
except socket.timeout:
|
||||
return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True}
|
||||
except OSError:
|
||||
d = b""
|
||||
if not d:
|
||||
return {"total": (total or 0), "got": len(buf), "crc_ok": False, "short": True}
|
||||
buf.extend(d)
|
||||
if total is None and len(buf) >= 4:
|
||||
total = 4 + (((buf[2] << 8) | buf[3])) + 1
|
||||
body = bytes(buf[4:total - 1])
|
||||
return {"total": total, "got": len(buf), "crc_ok": (buf[total - 1] == xor_bytes(body)),
|
||||
"short": (len(buf) != total)}
|
||||
|
||||
|
||||
def read_n(s, want, deadline):
|
||||
"""Read up to `want` bytes; stops at deadline / EOF. Returns bytes."""
|
||||
buf = bytearray()
|
||||
while len(buf) < want:
|
||||
left = deadline - time.monotonic()
|
||||
if left <= 0:
|
||||
break
|
||||
s.settimeout(left)
|
||||
try:
|
||||
d = s.recv(min(want - len(buf), 4096))
|
||||
except socket.timeout:
|
||||
break
|
||||
except OSError:
|
||||
break
|
||||
if not d:
|
||||
break
|
||||
buf.extend(d)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def abort_sock(s, fin):
|
||||
try:
|
||||
if fin:
|
||||
s.shutdown(socket.SHUT_RDWR)
|
||||
else:
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
s.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--ip", default="192.168.1.100")
|
||||
ap.add_argument("--port", type=int, default=7003)
|
||||
ap.add_argument("--n", type=int, default=8)
|
||||
ap.add_argument("--gap", type=int, default=0, help="abort -> reconnect gap in ms")
|
||||
ap.add_argument("--fin", action="store_true")
|
||||
ap.add_argument("--burst", type=int, default=400,
|
||||
help="keepalive frames used to load the RX direction (0 = TCP-only test)")
|
||||
ap.add_argument("--no-abort", action="store_true", help="no session A (baseline)")
|
||||
a = ap.parse_args()
|
||||
|
||||
res = {"OK": 0, "RX_SHORT": 0, "TX_SHORT": 0, "CORRUPT": 0, "ERR": 0}
|
||||
log("== sink recycle: port=%d gap=%dms abort=%s burst=%d n=%d"
|
||||
% (a.port, a.gap, "NONE" if a.no_abort else ("FIN" if a.fin else "RST"),
|
||||
a.burst, a.n))
|
||||
for i in range(1, a.n + 1):
|
||||
try:
|
||||
# ---- session A: trigger ----
|
||||
if not a.no_abort:
|
||||
s1 = socket.create_connection((a.ip, a.port), timeout=10.0)
|
||||
s1.sendall(req(CMD_INIT))
|
||||
read_frame(s1, time.monotonic() + 10.0)
|
||||
if a.burst:
|
||||
s1.sendall(req(CMD_KEEP) * a.burst)
|
||||
s1.sendall(req(CMD_LCDMEM, bytes(4)))
|
||||
f1 = read_frame(s1, time.monotonic() + 10.0)
|
||||
abort_sock(s1, a.fin)
|
||||
t_ab = time.monotonic()
|
||||
else:
|
||||
f1 = {"got": -1, "crc_ok": True}
|
||||
t_ab = time.monotonic()
|
||||
# ---- gap ----
|
||||
if a.gap > 0:
|
||||
time.sleep(a.gap / 1000.0)
|
||||
# ---- session B: victim ----
|
||||
s2 = socket.create_connection((a.ip, a.port), timeout=10.0)
|
||||
rx_bytes = 0
|
||||
if a.burst:
|
||||
s2.sendall(req(CMD_KEEP) * a.burst)
|
||||
got = read_n(s2, a.burst * ACK_LEN, time.monotonic() + 10.0)
|
||||
rx_bytes = len(got)
|
||||
s2.sendall(req(CMD_INIT))
|
||||
ini = read_frame(s2, time.monotonic() + 10.0)
|
||||
s2.sendall(req(CMD_LCDMEM, bytes(4)))
|
||||
f2 = read_frame(s2, time.monotonic() + 10.0)
|
||||
dt = (time.monotonic() - t_ab) * 1e3
|
||||
abort_sock(s2, a.fin)
|
||||
rx_want = a.burst * ACK_LEN
|
||||
rx_short = a.burst and rx_bytes < rx_want
|
||||
tx_short = (f2["got"] != LCDMEM_TOTAL) or (ini["got"] != 13)
|
||||
if rx_short:
|
||||
cls = "RX_SHORT"
|
||||
elif tx_short:
|
||||
cls = "TX_SHORT"
|
||||
elif not (f2["crc_ok"] and ini["crc_ok"]):
|
||||
cls = "CORRUPT"
|
||||
else:
|
||||
cls = "OK"
|
||||
res[cls] += 1
|
||||
log(" %2d A lcd=%4d crc=%-4s | B ack=%5d/%5d init=%2dB lcd=%4d/%d crc=%-4s "
|
||||
"dt=%6.0fms -> %s"
|
||||
% (i, f1["got"], "OK" if f1["crc_ok"] else "BAD", rx_bytes, rx_want,
|
||||
ini["got"], f2["got"], LCDMEM_TOTAL,
|
||||
"OK" if f2["crc_ok"] else "BAD", dt, cls))
|
||||
except Exception as e:
|
||||
res["ERR"] += 1
|
||||
log(" %2d EXC %s: %s" % (i, type(e).__name__, str(e)[:70]))
|
||||
time.sleep(0.3)
|
||||
log("SUMMARY port=%d gap=%dms abort=%s burst=%d n=%d ok=%d rx_short=%d tx_short=%d "
|
||||
"corrupt=%d err=%d"
|
||||
% (a.port, a.gap, "NONE" if a.no_abort else ("FIN" if a.fin else "RST"), a.burst,
|
||||
a.n, res["OK"], res["RX_SHORT"], res["TX_SHORT"], res["CORRUPT"], res["ERR"]))
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user