4c21cff753
现象:抓自检台账基线那次 FTP 会话一结束,设备就打 `[ERR] [FTP] error reading next command`, 本次 `@test` 直接 FAIL(Forbidden 含 `[ERR]`)。查下去发现 lftpd 把**全部**异常路径一律按 error 级打,其中九处其实由外部输入决定:客户端关窗口、连上就走、PASV 后不建数据连接、 运维改 IP 触发监听重建。两条代价:① 任何人"连 21 端口一下就断开"就能把一次启动测试打成 FAIL;② 同样的动作可以**无限制造固件级 ERROR 日志**——日志面与门禁面都可被外部驱动, 工业现场过不了审。 **做法**:`private/lftpd_log.h` 新增 `lftpd_log_peer()`(info 级,带 `[FTP]` 前缀),而不是 就地改成 `DBG_INFO`——让"谁才算真故障"在读代码时一目了然,也防止后来者顺手把对端行为升回 error。降级的九处:控制通道被对端关闭(原 `error reading next command`,改为 `control channel closed by peer (ret=%d)`,措辞原先把正常收尾写成故障、误导过排查方向)、 发送 220 前对端已走、`listener rebuilt`(网络重建,原因由 accept 层自己记)、四处 `data accept failed`(PASV 后没来数据连接)、`accept timeout`。 **刻意留在 error 的**:create/bind/listen/取端口失败、写通道失败、文件打开失败、 `DOWNLOAD/UPLOAD FAILED`(对端中途退出确实让一次传输没走完 = 数据完整性事件, `../问题记录/CH395F_Trap_Records.md` 的中止家族要靠这些锚点定位)、 `data listener disarmed`(控制连接还活着而我方数据监听已不在服役 = 本侧异常,就地注明)。 **实测划清的边界**:客户端**干净收尾**(发 `QUIT` 或 FIN 半关)现在整条链路没有任何 `[ERR]`; 但 **RST 硬拆**(`SO_LINGER` 清零 / 进程被杀 / 拔线)仍会留一条 `[ERR][NET] sockN TIMEOUT`—— 那是 net 层的套接字超时锚点,网络自愈架构判据要读它,不在本次降级范围。所以"采集窗口内别跑 `data listener disarmed`(控制连接还活着而我方数据监听已不在服役 = 本侧异常,就地注明)。 **真机验证**:`@build` 0 错 0 警告;`@test --require-selftest-pass --device-ip` → **PASS** (基线 29 条 → 本次无新增;`net init: OK` 726 ms);专门写了 RST 复现脚本(登录后 `SO_LINGER` 清零直接关)→ 串口得到 `[FTP] control channel closed by peer (ret=-1)`,**不再是 `[ERR]` 行**; 同一份日志里 QUIT 收尾的那次会话完全没有错误行。 体量:`Code` 213,236 → **213,264 B(+28)**、`RO` 18,580 → **18,652 B(+72)**(措辞变长), 同一份日志里 QUIT 收尾的那次会话完全没有错误行。 `ZI` 131,500、`RW_IRAM1` 75,264 B 57.4% / `RW_CCM` 57,376 B 87.9% 全不变)。数字联动照做: 两份 `data/` 快照、`ram_baseline --update/--write`、`check_mem_numbers --write`、FLASH 配图 常量重算重生成(桶合计 = 231,916 与 `Code+RO` 相符;越界复查 0)、报告 §1.3/§5.1/§5.4 与 部署文档 §3.3 同步。 口径落文:新增 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.3「FTP 日志级别划分:对端行为 vs 固件故障」(含两类的判据表与边界),并更新 `docs/测试说明/自动化测试流程.md` 的采集窗口警示。 顺手把 `private/lftpd_log.h` 工作区的 CRLF 混用归一回 LF(仓库里 blob 本来就是 LF,diff 无影响)。 门禁:`doc-links --all --sections-strict` 146 文档 0 问题、`id-check --strict` 增量拦截通过 (可判定 82.82%)、`mem-numbers` / `mem-budget` / `layer-deps` 全绿;改动文件字节卫生 BOM 无 / CR 0 / U+FFFD 0 / 无行尾空格。未推送。
249 lines
8.8 KiB
C
249 lines
8.8 KiB
C
#define DBG_TAG "[LFT_INET]"
|
||
|
||
#include "private/lftpd_inet.h"
|
||
#include "dbg_log.h"
|
||
#include "net_socket.h"
|
||
#include <string.h>
|
||
|
||
/*
|
||
* E-02 头阻塞修复 + recv pending 化(M3 台架实证):
|
||
* netTask 是全部 socket 操作的唯一执行者;lftpd 若用**阻塞** net_recv 等待对端
|
||
* 下一行/下一块数据 → 只要客户端连上却不发完整行,netTask 就被长期占用,
|
||
* 其余任务(含远端显示)的网络操作整体冻结(实测一次空闲 FTP 登录让 LCDMEM
|
||
* 整帧等待 17.7s)。
|
||
* 现形态:每次等待 = 一次挂起登记(netTask 零占用,完成按主循环节拍检查),
|
||
* 调用方单次超时 = 剩余总预算(总等待 ≤ LFTPD_TOTAL_BUDGET_MS)。
|
||
*/
|
||
#define LFTPD_TOTAL_BUDGET_MS 30000U
|
||
|
||
/*
|
||
* 看门狗心跳钩子弱默认实现:空(未接应用层的构建——如模拟器——不喂心跳,
|
||
* 看门狗为 MCU 构建专属概念)。MCU 构建下 App/util/task_watchdog.c 提供
|
||
* 同名强符号(路由到 task_wd_beat(TASK_WD_FTP)),链接器取强符号
|
||
* (与 wd_platform_* 钩子同机制)。
|
||
*/
|
||
#if defined(__CC_ARM)
|
||
__weak
|
||
#endif
|
||
void lftpd_task_wd_beat(void)
|
||
{
|
||
}
|
||
|
||
int lftpd_inet_listen(int port)
|
||
{
|
||
/* 应用层阻塞重试退役(原 5 轮 500ms/2000ms,最坏 10s 阻塞
|
||
* ftpTask)——TRAP-NET-09 的启动形态已由就绪屏障消除(FTP_READY 等 NET_READY,
|
||
* lftpd_start 外层本就有监听失败重建循环);芯片忙窗失败属罕见,PASV 数据
|
||
* 监听交给 FTP 客户端自身的重试语义(主流 FTP 服务器 passive listen 失败
|
||
* 亦直接 425,不在服务端叠应用层阻塞重试) */
|
||
lftpd_task_wd_beat(); /* 看门狗:入口喂心跳 */
|
||
int s = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||
if (s < 0) {
|
||
DBG_ERROR("create socket");
|
||
return -1;
|
||
}
|
||
|
||
struct net_sockaddr_in addr;
|
||
addr.sin_family = NET_AF_INET;
|
||
addr.sin_port = net_htons((uint16_t)port);
|
||
addr.sin_addr.s_addr = 0;
|
||
|
||
if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) {
|
||
DBG_ERROR("bind port %d", port);
|
||
net_close(s);
|
||
return -1;
|
||
}
|
||
|
||
if (net_listen(s) >= 0) {
|
||
return s;
|
||
}
|
||
|
||
DBG_ERROR("listen port %d failed", port);
|
||
net_close(s);
|
||
return -1;
|
||
}
|
||
|
||
int lftpd_inet_get_socket_port(int socket)
|
||
{
|
||
struct net_sockaddr_in addr;
|
||
int addrlen = sizeof(addr);
|
||
|
||
if (net_getsockname(socket, (struct net_sockaddr *)&addr, &addrlen) < 0) {
|
||
DBG_ERROR("error getting socket port number");
|
||
return -1;
|
||
}
|
||
|
||
return (int)net_ntohs(addr.sin_port);
|
||
}
|
||
|
||
int lftpd_inet_accept(int socket)
|
||
{
|
||
/* 网络重建(net_init)后旧 Socket 已失效:检测到代次变化即返回 -1,
|
||
* 由 lftpd_start 外层循环重建监听(网络未就绪时其 listen 会重试) */
|
||
uint32_t gen = net_get_generation();
|
||
|
||
while (1) {
|
||
int s = net_accept(socket, NULL, NULL);
|
||
if (s >= 0) {
|
||
return s;
|
||
}
|
||
if (net_get_generation() != gen) {
|
||
/* 运维改 IP 的正常后果,不是缺陷 → info(级别划分见存储架构说明 §10.3) */
|
||
DBG_INFO("network re-init: abort accept, relisten");
|
||
return -1;
|
||
}
|
||
lftpd_task_wd_beat(); /* 看门狗:自旋轮次喂心跳(无限等待,可能长时间无客户端) */
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
}
|
||
}
|
||
|
||
int lftpd_inet_accept_timeout(int socket, int timeout_ms)
|
||
{
|
||
int elapsed = 0;
|
||
|
||
while (elapsed < timeout_ms) {
|
||
int s = net_accept(socket, NULL, NULL);
|
||
if (s >= 0) {
|
||
return s;
|
||
}
|
||
/* F-NET-15 M2(自愈专项实验 2026-10-12):镜像门控退役——原直读 p_sock->state 在
|
||
* relisten 忙期(CLOSED 瞬态)误判"已关闭",扔掉 ~4.9s 等待预算;armed 门控
|
||
* 认"监听器在服役(LISTENING)或待重建且 RX 有残留(closed-with-data 救回窗)",
|
||
* 与 :7003 的 N-32 P1 同型修复 */
|
||
if (net_listener_armed(socket) == 0) {
|
||
/* **故意留 error**:控制连接还活着、我方数据监听却已不在服役 →
|
||
* 这是本侧异常,不是对端行为 */
|
||
DBG_ERROR("data listener disarmed, accept aborted");
|
||
return -1;
|
||
}
|
||
lftpd_task_wd_beat(); /* 看门狗:每轮喂心跳(数据连接等待最长 5s) */
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
elapsed += 100;
|
||
}
|
||
|
||
/* 客户端连上控制通道却没来建数据连接(中止/被防火墙拦)= 对端行为 → info */
|
||
DBG_INFO("accept timeout (%dms) - peer never opened data channel", timeout_ms);
|
||
return -1;
|
||
}
|
||
|
||
int lftpd_inet_read_line(int socket, char *buffer, size_t buffer_len)
|
||
{
|
||
TickType_t t0 = xTaskGetTickCount();
|
||
|
||
memset(buffer, 0, buffer_len);
|
||
int total_read_len = 0;
|
||
|
||
while (total_read_len < (int)buffer_len) {
|
||
/* 单次预算 = 剩余总预算(pending:等待期 netTask 零占用,调用方
|
||
* 在自己上下文睡任务通知) */
|
||
TickType_t elapsed = xTaskGetTickCount() - t0;
|
||
uint32_t rem = (elapsed >= (TickType_t)pdMS_TO_TICKS(LFTPD_TOTAL_BUDGET_MS)) ? 0U
|
||
: (uint32_t)((TickType_t)LFTPD_TOTAL_BUDGET_MS - elapsed);
|
||
if (rem == 0U) {
|
||
return -1; /* 总预算耗尽(语义同旧 30s 超时) */
|
||
}
|
||
lftpd_task_wd_beat(); /* 看门狗:每片前喂心跳(长等待无数据期保持节奏) */
|
||
if (rem > LFTPD_WAIT_SLICE_MS) {
|
||
rem = LFTPD_WAIT_SLICE_MS; /* 分片:单次 30s 挂起拆为 ≤200ms 片 */
|
||
}
|
||
int read_len = net_recv_to(socket,
|
||
buffer + total_read_len,
|
||
(int)(buffer_len - total_read_len - 1),
|
||
rem);
|
||
if (read_len == 0) {
|
||
return -1; /* 对端关闭 */
|
||
}
|
||
if (read_len == -2) {
|
||
continue; /* 本次预算等满无数据 → 下轮 rem=0 退出 */
|
||
}
|
||
if (read_len < 0) {
|
||
return read_len; /* 真错误 */
|
||
}
|
||
|
||
total_read_len += read_len;
|
||
|
||
char *p = strstr(buffer, "\r\n");
|
||
if (p != NULL) {
|
||
*p = '\0';
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
int lftpd_inet_write_string(int socket, const char *message)
|
||
{
|
||
const char *p = message;
|
||
int length = (int)strlen(message);
|
||
|
||
while (length > 0) {
|
||
int write_len = net_send(socket, p, length, 0);
|
||
/* write_len == 0:发送超时且零进展(对端未消费数据);不判为致命则
|
||
* length 永不减少,将在此空转(任务死锁)。返回错误交会话层拆除
|
||
* 连接(lftpd.c handle_control_channel 的 cleanup 路径)。 */
|
||
if (write_len <= 0) {
|
||
DBG_ERROR("write error, ret=%d", write_len);
|
||
return -1;
|
||
}
|
||
p += write_len;
|
||
length -= write_len;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
int lftpd_inet_write(int socket, const void *buf, int len)
|
||
{
|
||
const char *p = (const char *)buf;
|
||
int remaining = len;
|
||
|
||
while (remaining > 0) {
|
||
int n = net_send(socket, p, remaining, 0);
|
||
/* n == 0:发送超时且零进展(对端停滞/断链),判为致命:否则 remaining
|
||
* 永不减少 → 死循环。调用方(lftpd.c send_file)收到 < 0 后终止传输
|
||
* 并向上报 450 错误,会话保持可用。 */
|
||
if (n <= 0) {
|
||
DBG_ERROR("write error, ret=%d", n);
|
||
return -1;
|
||
}
|
||
p += n;
|
||
remaining -= n;
|
||
}
|
||
|
||
return len;
|
||
}
|
||
|
||
int lftpd_inet_read(int socket, void *buf, int len)
|
||
{
|
||
/* pending 读:总预算 30s 不变,等待期 netTask 零占用
|
||
* (空闲数据连接不再冻结全网) */
|
||
TickType_t t0 = xTaskGetTickCount();
|
||
|
||
for (;;) {
|
||
TickType_t elapsed = xTaskGetTickCount() - t0;
|
||
uint32_t rem = (elapsed >= (TickType_t)pdMS_TO_TICKS(LFTPD_TOTAL_BUDGET_MS)) ? 0U
|
||
: (uint32_t)((TickType_t)LFTPD_TOTAL_BUDGET_MS - elapsed);
|
||
if (rem == 0U) {
|
||
return -1; /* 总预算耗尽(语义同旧 30s 超时) */
|
||
}
|
||
lftpd_task_wd_beat(); /* 看门狗:每片前喂心跳(长等待无数据期保持节奏) */
|
||
if (rem > LFTPD_WAIT_SLICE_MS) {
|
||
rem = LFTPD_WAIT_SLICE_MS; /* 分片:单次 30s 挂起拆为 ≤200ms 片 */
|
||
}
|
||
int n = net_recv_to(socket, buf, len, rem);
|
||
if (n >= 0) {
|
||
return n; /* >0 数据 / 0 EOF */
|
||
}
|
||
if (n == -1) {
|
||
return -1; /* 真错误 */
|
||
}
|
||
/* n == -2:本次预算等满无数据 → 下轮 rem=0 退出 */
|
||
}
|
||
}
|
||
|
||
int lftpd_inet_close(int socket)
|
||
{
|
||
return net_close(socket);
|
||
}
|