23daa3d9ad
两件事一起做:把"没人用又已经失效"的一次性探针清掉,把"台架现场脏"这件事的口径写进文档。 **删除(5 个文件,全库零引用)**:`MDK-ARM/ftp_retr_abort_probe.py`(其职能已被 `test/host/ftp_abort_probe.py` 取代,Trap 记录引用的也是后者)与整个 `MDK-ARM/bench_diag/` (4 个诊断脚本:abort_gap / pcref / recover / sink_recycle)。删除判据三条同时成立:① 全库 `git grep` 除自身外零引用;② 自 2026-10-11 设备启用 FTP 鉴权起,脚本里写死的匿名登录必然 530 → 已经跑不动 11 天;③ 都是"跑一次拿现场"的诊断件,价值在结论而结论已沉淀进 Trap 记录。 需要时从 git 历史取回(`test/host/README.md` 与 `MDK-ARM/README.md` 都写明了这一点)。 **刻意保留并修好**:`MDK-ARM/ftp_freeze_probe.py`——它不是一次性件: `CH395F_Trap_Records.md` 拿它当"177 轮 SHA-256 全匹配"的**可信复现配方**、 `BSD_Socket_API_使用指南.md` 把它列进排障判据、`Drivers/BSP/NET/lftpd/README.md` 也指它。 删掉它等于删掉文档指定的真值工具。改为 `--user/--password`(默认出厂 admin/admin,模块常量 `FTP_USER`/`FTP_PASS`,两处登录点都读它),实测一轮:`ROUND 1 size=4096 stor=OK retr=OK hash=MATCH`、`SUMMARY pass=1 fail=0`——修前它连 530 都过不去。 **台架设备清理(不动仓库内容)**:`/` 根目录实测残留 **27 个探针文件 / 262 KB**(`AB000x`、 `A000x`、`R000x`、`SZ2K~64K`、`E12A`、`DIAG_A`、`PP0~PP4`——正是刚删的那批一次性探针留下的)。 逐个 `DELE` 后剩 `LOG` + `CONFIG.INI` + `PROT_SET.BIN` 三条目。**判据:只删显式点名的文件, 绝不 glob**;保留清单必须含 `/config.ini`(网口 + FTP 鉴权)、`/PROT_SET.BIN`(保护定值 blob, `PROT_SET_BLOB_SZ` = 176 B,删了等于定值清零)、`/log` 目录。清完两步验收:`@test` PASS (630 ms,定值巡检与配置加载都在这条链上)+ `ftp_list_fill_test.py` PASS(29 行 / 1851 B / 完成回包 226,26 个新文件全列出)。口径写进 `docs/测试说明/存储维护与容量恢复.md` 新 §4.1 (明确它是"动作 D 清文件",与 A 重建 BBT / B 全片擦除 / C 容量恢复区分——那三个会丢卷内容)。 门禁:`py_compile`(freeze 探针)通过;`check_doc_links.py --all --sections-strict` 146 文档 0 问题(删除的 5 个文件确认无任何文档引用);`check_ids.py --strict` 增量拦截通过(可判定 82.82%);`layer-deps` 通过;4 个改动文件字节卫生 BOM 无 / CR 0 / U+FFFD 0 / 无行尾空格。 本轮无固件改动,未跑 `@build`(设备侧清理后的 `@test` 已跑,镜像仍是上一笔的构建)。未推送。
124 lines
5.1 KiB
Python
124 lines
5.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Trustworthy TC-NET-604 freeze reproduction via the REAL production path.
|
|
#
|
|
# Why this one is trustworthy (unlike the 2026-09-22 probe batteries):
|
|
# * Production firmware, no in-firmware readers/probes -> the "a test reader ate the
|
|
# peer's bytes and the PC saw a short stream" artifact class cannot occur.
|
|
# * Judgement = the FTP client's own protocol result (STOR/RETR replies) plus an
|
|
# end-to-end SHA-256 taken from a RETR of the uploaded file (content ground truth).
|
|
# * Data-channel stall vs whole-net-layer stall is distinguished by a NOOP on the
|
|
# CONTROL channel (a different socket) while the data channel is stuck.
|
|
# * Only the device's own production logs are used for the device-side timeline
|
|
# (window them by the per-round "TS <epoch>" line printed here).
|
|
#
|
|
# Usage: ftp_freeze_probe.py [--ip 192.168.1.100] [--port 21] [--size 65536]
|
|
# [--rounds 10] [--gap 3] [--keep]
|
|
# Output lines (machine-auditable):
|
|
# TS <epoch>
|
|
# ROUND <n> size=<bytes> stor=<OK|FAIL:reason> t_stor=<s> retr=<OK|FAIL:reason>
|
|
# t_retr=<s> hash=<MATCH|MISMATCH|NA> [ctrl=<alive|dead|na>]
|
|
# SUMMARY rounds=<n> pass=<n> fail=<n>
|
|
# Exit: 0 all pass / 1 any failure / 2 usage or connect error.
|
|
import argparse, hashlib, io, os, random, socket, sys, time
|
|
from ftplib import FTP, error_reply, error_temp, error_perm, all_errors
|
|
|
|
# Device-side FTP auth: lftpd validates against /config.ini [ftp] (user / pwd_hash).
|
|
# This device has NO anonymous channel (USER anonymous -> 530), so the probe must log
|
|
# in with the factory account. Override with --user/--password if it was changed.
|
|
FTP_USER = "admin"
|
|
FTP_PASS = "admin"
|
|
|
|
|
|
def log(msg):
|
|
sys.stdout.write(msg + "\n")
|
|
sys.stdout.flush()
|
|
|
|
def liveness(ip, port, timeout=3.0):
|
|
"""Control-channel liveness on a FRESH connection (production lftpd control path)."""
|
|
try:
|
|
f = FTP()
|
|
f.connect(ip, port, timeout=timeout)
|
|
f.login(FTP_USER, FTP_PASS)
|
|
f.voidcmd("NOOP")
|
|
f.quit()
|
|
return "alive"
|
|
except Exception:
|
|
return "dead"
|
|
|
|
def main():
|
|
global FTP_USER, FTP_PASS # 两处登录点都读模块常量,命令行覆盖后即刻生效
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--ip", default="192.168.1.100")
|
|
ap.add_argument("--port", type=int, default=21)
|
|
ap.add_argument("--size", type=int, default=65536)
|
|
ap.add_argument("--rounds", type=int, default=10)
|
|
ap.add_argument("--gap", type=float, default=3.0)
|
|
ap.add_argument("--keep", action="store_true", help="do not DELE the uploaded file")
|
|
ap.add_argument("--seed", type=int, default=604)
|
|
ap.add_argument("--user", default="admin",
|
|
help="FTP user (device auth; this device has no anonymous channel)")
|
|
ap.add_argument("--password", default="admin", help="FTP password (factory default)")
|
|
a = ap.parse_args()
|
|
FTP_USER, FTP_PASS = a.user, a.password
|
|
|
|
rng = random.Random(a.seed)
|
|
npass = nfail = 0
|
|
for n in range(1, a.rounds + 1):
|
|
data = bytes(rng.getrandbits(8) for _ in range(a.size))
|
|
want = hashlib.sha256(data).hexdigest()
|
|
name = "/fz%04d.bin" % n
|
|
log("TS %d" % int(time.time()))
|
|
f = FTP()
|
|
stor = retr = "NA"
|
|
t_stor = t_retr = 0.0
|
|
got_hash = "NA"
|
|
ctrl = "na"
|
|
try:
|
|
f.connect(a.ip, a.port, timeout=15.0)
|
|
f.login(FTP_USER, FTP_PASS)
|
|
t0 = time.time()
|
|
try:
|
|
f.storbinary("STOR " + name, io.BytesIO(data), blocksize=8192)
|
|
stor = "OK"
|
|
except all_errors as e:
|
|
stor = "FAIL:%s" % type(e).__name__
|
|
ctrl = liveness(a.ip, a.port)
|
|
t_stor = time.time() - t0
|
|
if stor == "OK":
|
|
buf = bytearray()
|
|
t0 = time.time()
|
|
try:
|
|
f.retrbinary("RETR " + name, buf.extend, blocksize=8192)
|
|
retr = "OK"
|
|
got_hash = "MATCH" if hashlib.sha256(bytes(buf)).hexdigest() == want else "MISMATCH"
|
|
except all_errors as e:
|
|
retr = "FAIL:%s" % type(e).__name__
|
|
ctrl = liveness(a.ip, a.port)
|
|
t_retr = time.time() - t0
|
|
if not a.keep:
|
|
try:
|
|
f.delete(name)
|
|
except all_errors:
|
|
pass
|
|
try:
|
|
f.quit()
|
|
except all_errors:
|
|
try:
|
|
f.close()
|
|
except Exception:
|
|
pass
|
|
except all_errors as e:
|
|
stor = "FAIL:connect-%s" % type(e).__name__
|
|
ctrl = liveness(a.ip, a.port)
|
|
ok = (stor == "OK" and retr == "OK" and got_hash == "MATCH")
|
|
npass += 1 if ok else 0
|
|
nfail += 0 if ok else 1
|
|
log("ROUND %d size=%d stor=%s t_stor=%.2f retr=%s t_retr=%.2f hash=%s ctrl=%s" %
|
|
(n, a.size, stor, t_stor, retr, t_retr, got_hash, ctrl))
|
|
if a.gap > 0 and n < a.rounds:
|
|
time.sleep(a.gap)
|
|
log("SUMMARY rounds=%d pass=%d fail=%d" % (a.rounds, npass, nfail))
|
|
return 0 if nfail == 0 else 1
|
|
|
|
sys.exit(main())
|