3a2f9b21fb
SNTP 客户端已随 #73/PR #77 上线合入(merge 2b848c4a),用户裁定
8888 对时任务退出历史舞台(gitea:77 线程原话,跟踪 Issue #78)。
生产侧(App/sync/time_sync.c v2.0,任务入口名不变,freertos.c 零改动):
- 删除 :8888 监听、TIME 包重组、sys_clock 写入、OPER 落痕、
net_drop_conn 队头自救整条链(TIME_SYNC_PORT 宏随退役删除)
- 任务体收敛为:就绪屏障(100ms 分片、osFlagsNoClear)→
200ms 校时节拍 task_wd_beat → time_sntp_run(net_get_generation())
→ 分片延时;喂狗节奏(TASK_WD_TIMESYNC=5,required_mask)不变
- 时钟写入口自此 = SNTP / UI 手工 / 104 C_CS,双写竞态(后写者赢)
随之消失(墙钟说明 §15 退役公告、§16 现势口径)
工具与测试:
- 删除 test/host/time_sync_server.py(对时源)与 e02_probe.py
(队头阻塞探针,载体随监听退役;判据脚本 v5_log_gap_report.py
保留供旧日志解读)
- time_sync 主机单测重写 v2.0:33 断言覆盖分片延时 / 屏障 /
SNTP 节拍与网络代次当轮交接 / 未出网守门;stub 的
net_socket.h 裁剪至 net_get_generation 作编译期绊线
- 覆盖率基线 --update-baseline 收口(time_sync.c 101→20 行为刻意
缩减,全仓 48/48 套绿)
文档与图:墙钟说明 §15 改退役公告(教训保留:CH395F FIN 丢未读
缓冲)、§16 单一网络来源;产品需求 FR-TIME-2 退役注记、§6.3 重写;
架构总览 / 软件架构规格 / 网络配置 / 网络自愈 §9.3 / BSD 指南 /
看门狗说明心跳与白名单行 / 实机测试指南 / 部署环境 / 继保规划 /
UI补齐时基口径 / 页面补齐规划 SNTP 页行 / 任务规划 README 叶数与
暂缓项行(SNTP 叶已绑、UI_PAGE_MAP_COUNT=31 口径)等约 30 处现势
口径同步;arch / net_stack 两图生成器与 SVG 同笔重跑。
台账:ram_baseline --update + check_mem_numbers --write + 体积凭证
(Code 254,648→253,428,−1,220 B)。静态链复测发现 timeSyncTask
1,904 B / iec104Task 1,600 B 低于 ISR 硬线:master 2b848c4a 同判据
timeSyncTask 即 2,056 B(本 PR 只减不增,−152 B 旧任务自身帧),
双 FAIL 为 #74/#77 后既有债;抬栈属 Ask-first 未擅自执行,登记簿
新增「静态链硬线双 FAIL」待台架复采,复测快照入 docs/内存管理/data。
验证:keil-build 0 错 0 警;time_sync/sntp 主机单测绿;
ci_local 全绿;doc-links --all 0。
177 lines
7.2 KiB
Python
177 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""FTP 鉴权台架回归(需真机在线)
|
||
|
||
验证固件侧 lftpd 鉴权集成的完整行为矩阵(主机单测 test/host/ftp_auth 覆盖
|
||
不到的"协议接线"部分:命令门、530/331/230/550 响应码、跨会话锁定):
|
||
|
||
场景「匿名登录」(anonymous) → 期望 530 拒
|
||
场景「未登录直接 NLST」 → 期望 530(命令门)
|
||
场景「对用户名 + 错口令 ×5」 → 期望 530 ×5(并触发锁定)
|
||
T4 锁定窗内正确口令 → 期望 530(LOCKED,不算哈希)
|
||
T5 等待锁定期满后正确口令 → 期望 230(60s + 余量)
|
||
T6 登录后 NLST / STOR / RETR / DELE → 期望全通(出厂 write=1)
|
||
T7 会话结束重连、错口令一次 → 期望 530 且计数已清零(T5 成功清零)
|
||
|
||
注意:CH395F 单连接——每步用完必须 quit() 释放 socket,否则下一步连不上;
|
||
跑之前确保 UI 远程显示(7003)客户端未占用 socket(对时 :8888 已退役,
|
||
SNTP 只占 UDP 通道,与 TCP 单连接语义无关)。
|
||
|
||
用法(设备默认见 bench_defaults.DEVICE_IP,出厂口令 admin/admin):
|
||
& '.venv\\Scripts\\python.exe' test/host/ftp_auth_bench.py
|
||
改过口令: ... --pwd <新口令>
|
||
跳过 60s 锁定等待(只验锁定触发不验解锁): ... --skip-unlock
|
||
|
||
退出码:0=全部通过 / 1=任一失败 / 2=环境错误(连不上设备)
|
||
创建日期:2026-10-11
|
||
"""
|
||
|
||
import argparse
|
||
import io
|
||
import socket
|
||
import sys
|
||
import time
|
||
from ftplib import FTP, error_perm
|
||
|
||
import bench_defaults # 台架连接默认值单一真源
|
||
|
||
LOCK_WAIT_S = 62 # 固件锁定 60 s + 余量
|
||
|
||
|
||
def _try_login(host, port, timeout, user, pwd):
|
||
"""尝试一次登录;返回 (ok, code_or_exc)。失败不断言连接状态。
|
||
|
||
单连接 socket:上一次会话结束后 net 层 auto_relisten ~50 ms,但 accept
|
||
失败会走"毁监听→重建"路径(3 s 重试窗)→ connect 被拒时重试 3 次。
|
||
"""
|
||
f = FTP()
|
||
last_err = None
|
||
for attempt in range(3):
|
||
try:
|
||
time.sleep(0.5)
|
||
f.connect(host, port, timeout=timeout)
|
||
break
|
||
except (ConnectionRefusedError, socket.timeout, OSError) as e:
|
||
last_err = e
|
||
f = FTP()
|
||
else:
|
||
return None, repr(last_err), None
|
||
try:
|
||
try:
|
||
f.login(user, pwd)
|
||
return True, "230", f
|
||
except error_perm as e:
|
||
f.close()
|
||
return False, str(e), None
|
||
except (socket.timeout, OSError) as e:
|
||
return None, repr(e), None
|
||
|
||
|
||
def main(argv=None):
|
||
ap = argparse.ArgumentParser(description="FTP auth bench matrix")
|
||
ap.add_argument("--host", default=bench_defaults.DEVICE_IP)
|
||
ap.add_argument("--port", type=int, default=21)
|
||
ap.add_argument("--timeout", type=int, default=15)
|
||
ap.add_argument("--user", default="admin")
|
||
ap.add_argument("--pwd", default="admin")
|
||
ap.add_argument("--skip-unlock", action="store_true",
|
||
help="不验 60s 解锁(省 1 分钟;锁定触发仍验)")
|
||
args = ap.parse_args(argv)
|
||
|
||
fails = []
|
||
|
||
def expect(name, cond, detail=""):
|
||
tag = "PASS" if cond else "FAIL"
|
||
print(" [%s] %s %s" % (tag, name, detail))
|
||
if not cond:
|
||
fails.append(name)
|
||
|
||
# ---- 场景「匿名登录被拒」----
|
||
ok, code, conn = _try_login(args.host, args.port, args.timeout, "anonymous", "x@x")
|
||
if ok is None:
|
||
print("[env] 连不上 %s:%d → %s" % (args.host, args.port, code))
|
||
return 2
|
||
expect("匿名登录 → 530", (ok is False) and code.startswith("530"), code)
|
||
if conn:
|
||
conn.quit()
|
||
|
||
# ---- 场景「未登录直接 NLST → 530(命令门)」----
|
||
time.sleep(0.5) # 单连接 socket:等 auto_relisten 沉降(~50 ms + 余量)
|
||
f = FTP()
|
||
f.connect(args.host, args.port, timeout=args.timeout)
|
||
try:
|
||
f.sendcmd("NLST")
|
||
expect("未登录 NLST → 530", False, "居然放行")
|
||
except error_perm as e:
|
||
expect("未登录 NLST → 530", str(e).startswith("530"), str(e))
|
||
except Exception as e: # noqa: BLE001
|
||
expect("未登录 NLST → 530", False, repr(e))
|
||
# ⚠ 必须 close:单连接模型下不关 = 占住 socket,后续步骤全被拒
|
||
# (2026-10-11 台架实测:QUIT 后不 close → 芯片视角旧连接仍 ESTABLISHED)
|
||
try:
|
||
f.quit()
|
||
except Exception: # noqa: BLE001
|
||
try:
|
||
f.close()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
time.sleep(0.5)
|
||
|
||
# ---- 场景「错口令 ×5 → 530 ×5」----
|
||
for i in range(1, 6):
|
||
ok, code, conn = _try_login(args.host, args.port, args.timeout,
|
||
args.user, "wrong-pass-%d" % i)
|
||
expect("错口令 %d → 530" % i, (ok is False) and code.startswith("530"), code)
|
||
if conn:
|
||
conn.quit()
|
||
|
||
# ---- T4 锁定窗内正确口令 → 530 ----
|
||
ok, code, conn = _try_login(args.host, args.port, args.timeout, args.user, args.pwd)
|
||
expect("T4 锁定窗内对口令 → 530", (ok is False) and code.startswith("530"), code)
|
||
if conn:
|
||
conn.quit()
|
||
|
||
if args.skip_unlock:
|
||
print("[skip] 未验 60s 解锁(--skip-unlock)")
|
||
else:
|
||
# ---- T5 期满正确口令 → 230 ----
|
||
print(" [wait] 等锁定期满 %d s ..." % LOCK_WAIT_S)
|
||
time.sleep(LOCK_WAIT_S)
|
||
ok, code, conn = _try_login(args.host, args.port, args.timeout,
|
||
args.user, args.pwd)
|
||
expect("T5 期满对口令 → 230", ok is True, code)
|
||
if conn:
|
||
# ---- T6 登录后读写全通(出厂 write=1)----
|
||
try:
|
||
names = conn.nlst()
|
||
expect("T6a NLST", len(names) >= 0, "%d 项" % len(names))
|
||
payload = b"N19-auth-bench-" + b"X" * 500
|
||
conn.storbinary("STOR n19t.tmp", io.BytesIO(payload))
|
||
got = io.BytesIO()
|
||
conn.retrbinary("RETR n19t.tmp", got.write)
|
||
expect("T6b STOR/RETR 往返一致", got.getvalue() == payload)
|
||
conn.delete("n19t.tmp")
|
||
expect("T6c DELE", True)
|
||
except Exception as e: # noqa: BLE001
|
||
expect("T6 登录后读写", False, repr(e))
|
||
# ---- T7 成功已清零计数:本会话退出后错一次只算 1(不锁)----
|
||
conn.quit()
|
||
ok, code, conn = _try_login(args.host, args.port, args.timeout,
|
||
args.user, "one-wrong")
|
||
expect("T7 成功后错 1 次 → 530 且未锁(下次对口令可登)",
|
||
(ok is False) and code.startswith("530"), code)
|
||
if conn:
|
||
conn.quit()
|
||
ok, code, conn = _try_login(args.host, args.port, args.timeout,
|
||
args.user, args.pwd)
|
||
expect("T7b 紧接对口令 → 230", ok is True, code)
|
||
if conn:
|
||
conn.quit()
|
||
|
||
print("\nVERDICT: %s (%d fail)" % ("PASS" if not fails else "FAIL", len(fails)))
|
||
return 0 if not fails else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|