Files
STM32F4-Base/MDK-ARM/ftp_freeze_probe.py
T
wandering1 23daa3d9ad
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m34s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 21s
chore(台架): 删零引用的一次性探针、修可信判据工具登录、清理口径落文
两件事一起做:把"没人用又已经失效"的一次性探针清掉,把"台架现场脏"这件事的口径写进文档。

**删除(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` 已跑,镜像仍是上一笔的构建)。未推送。
2026-10-22 22:51:21 +08:00

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())