feat(m53): 自写 timeout(1) 并接入 devkit 本地源码构建路径

设备没有 timeout,busybox 1.31 也无该 applet;2026-11-11 18:44 一条挂在
硬件上的 bluetoothctl 累积 55 个残留进程耗尽进程表,dropbear 无法 fork,
整机在线却不可登录。本单给设备一个可用的 timeout。

- scripts/build/devkit-src/timeout.c(新,Apache-2.0,489 行)
  独立实现,未拷贝 busybox/GPL 代码;fork+setpgid(0,0) 后 kill(-pgid),
  孙进程一并终止;waitpid 回收直接子进程不留僵尸;-s/-k 两阶段终止;
  退出码 124/137/128+N/125/126/127 对齐 coreutils 语义。
  计时不用 timerfd(musl 的 timerfd_settime 在 4.4 上静默失效,实测
  timeout 2 sleep 30 等满 30s 才返回),改用 setitimer(ITIMER_REAL)+SIGALRM,
  与 SIGCHLD 一起进 signalfd,由一次 ppoll 等待,零轮询、不读时钟。
- build_devkit.sh:新增 build_local(B 路径,不碰上游 procps/lsof/ncurses
  流程);TOOLS/TOOL_NAMES/MANIFEST/licenses/stamp/--with-sources 接入;
  产物同时标注 Apache-2.0 并随附许可证原文。
- test_build_devkit.sh:T9(产物静态/架构/零 NEEDED/MANIFEST/license)、
  T10(源码纪律:独立实现、不调 clock_gettime、不用 timerfd、进程组模型)。
- DEVKIT.md/README.md:产物表、PATH 实测(ssh 非登录 shell 无 opt/bin,
  且设备无 bash → 用 sh -lc 或绝对路径)、禁裸跑纪律与 18:44 事故时间线、
  timerfd 静默失效的第二类 time64 坑、2026-11-11 真机验收原始输出。
This commit is contained in:
AskaEth committed 2026-11-11 21:23:52 +08:00
1 parent a41b61198e
commit 80c0bd4eb4
5 files changed
+855 -22

No files matched your search

+179 -5
View File
@@ -19,6 +19,51 @@ busybox 1.31.1:`free / ps / top / lsof` 是 busybox 的软链(实测 `/bin/p
所以本工具集只围绕「这台机器的真实调试痛点」,不做大而全。
## 1.1 ⚠ 设备侧「禁裸跑」纪律 —— 2026-11-11 整机崩塌的直接教训
**给下一个会话的救命提示,不是官腔。先读完这一节再敲命令。**
### 发生了什么(已取证的硬事实)
| 时刻(2026-11-11) | 事件 |
|---|---|
| 15:45:40 | 设备开机,一切正常 |
| 18:44:13 | `meow_launcherd` 最后一行日志 |
| 18:44:14 | sshd 最后一个 child 正常退出 |
| 18:44:14 之后 **11 分钟** | `ssh.log` **一行都没有**(连 `Child connection from` 都没记)→ dropbear 连 `fork()` 都做不到 |
| 同一时段 | ICMP 仍应答、RTT 163ms → 网络栈活着,**是进程表被耗尽**,不是掉网 |
| 崩溃前累积 | **55 个挂死的 `bluetoothctl` 客户端进程** —— 历次对着一台**已关机**的耳机裸跑 `bluetoothctl connect` 的残留 |
因果链:**裸跑会挂在硬件上的外部命令 → 每个都永久阻塞并持有资源 → 55 个之后
内核无法再 fork → dropbear 无法派生会话 → 整机"在线但不可登录" → 只能重启。**
### 纪律(没有例外)
1. **设备侧禁止裸跑任何可能挂在硬件上的外部命令**:`bluetoothctl`、`hciconfig`、
`hcitool`、`wpa_cli`、`rfkill`,以及一切由 `popen`/`system()` 起的子进程。
一律加超时:
```sh
timeout 8 bluetoothctl connect <MAC> # 首选:连它 fork 的孙进程一起杀
bluetoothctl --timeout 8 connect <MAC> # 备选:bluetoothctl 5.54 自带 --timeout
```
2. **循环重试必须封顶次数**,并且每次迭代都用 `timeout` 包住。
`while true; do bluetoothctl connect ...; done` 是**事故配方**,不是重试。
3. 收工前必须核对残留:`ps w | grep -c bluetoothctl` 回到基线(正常 0),
`ps` 里不应有自己留下的 `sleep`/`bluetoothctl`。
4. 设备是**单核 1GHz / 56MB**:并发 3 个以上挂死进程就足以让整机不可用。
这不是"性能慢",是**进程表耗尽**。
### 为什么是 `timeout`(而不是别的)
设备原本**没有** `timeout(1)`:`command -v timeout` 为空,busybox 1.31.1 也无此 applet
(`busybox --list | grep -x timeout` 无匹配)。所以本 devkit 自带一个(见 §2、§3.2),
它就是这条纪律的可执行形态:
```sh
timeout 5 sh -c 'sleep 100 & sleep 100 & wait' # 孙进程也必须死,ps 里不留 sleep
```
## 2. 产物清单
全部:`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
@@ -33,11 +78,17 @@ busybox 1.31.1:`free / ps / top / lsof` 是 busybox 的软链(实测 `/bin/p
| `sysctl` | procps-ng 4.0.4 | 同上 | 148 184 | 读写 `/proc/sys`(`-a` 列出 428 个键);busybox 1.31 无此 applet |
| `free` | procps-ng 4.0.4 | 同上 | 94 312 | 拆 buff/cache、available |
| `lsof` | lsof 4.99.4 | Purdue/BSD-style | 789 496 | SIZE/OFF、TCP 状态、NAME、`-i` 网络视图 |
| | | **合计** | **2 141 764 ≈ 2.1 MB** | |
| `timeout` | 本仓 `devkit-src/timeout.c` | **Apache-2.0**(自有独立实现,非 busybox/GPL 代码) | 47 708 | 给任何外部命令加"最多等 N 秒、然后杀掉整个进程组";busybox 1.31 无此 applet |
| | | **合计** | **2 189 472 ≈ 2.1 MB** | |
`timeout` 走的是**本地源码编译路径**(`scripts/build/devkit-src/*.c` → `build_local`),
与上游包(procps/lsof/ncurses)的「下载 + sha256 + configure/make」流程完全隔离:
不联网、不 patch、不打乱上游构建参数。新增本地工具只需往该目录丢一个 `.c`。
同目录还有 `MANIFEST.txt`(每个二进制的 sha256 + 版本 + 许可证 + 体积)、
`licenses/`(procps COPYING + COPYING.LIB、ncurses COPYING、lsof COPYING、**musl COPYRIGHT**)、
`README.txt`(上卡说明)。`--with-sources` 会额外生成 `src/`(三份源码包 + `build_devkit.sh` + 本文件)。
`licenses/`(procps COPYING + COPYING.LIB、ncurses COPYING、lsof COPYING、**musl COPYRIGHT**、
**meowos-timeout-LICENSE**=Apache-2.0 原文)、
`README.txt`(上卡说明)。`--with-sources` 会额外生成 `src/`(三份源码包 + `build_devkit.sh` + 本文件 + `timeout.c`)。
## 3. 上卡后怎么用
@@ -59,10 +110,58 @@ fi
| `free` `lsof` `top` | `/usr/bin/x -> ../../bin/busybox` | `free`/`lsof` 解析到 devkit 版;`top` 仍是 busybox(本工具集不提供) |
| `ps` | `/bin/ps -> busybox` | 解析到 devkit 版 |
| `vmstat` `slabtop` `pmap` `sysctl` | 不存在(busybox 无对应 applet) | 新增 |
| `timeout` | 不存在(busybox 1.31 无对应 applet;`command -v timeout` 为空) | 新增 |
要临时用回 busybox 版:`busybox free`、`busybox ps`、`busybox lsof`。
不要覆盖 `/usr/bin`、`/bin` 里的软链:rootfs 是 ubifs,随固件回刷即被还原,而且这些软链是基线的一部分。
### 3.1 ⚠ SSH 是非登录 shell:PATH 里没有 `MeowOS/opt/bin`
这是后续会话反复踩的坑,实测(2026-11-11):
```sh
ssh root@<ip> 'echo $PATH'
# /usr/sbin:/usr/bin:/sbin:/bin ← 没有 MeowOS/opt/bin,也没有 devkit/bin
ssh root@<ip> 'command -v timeout' # 空
ssh root@<ip> 'timeout 1 sleep 5' # sh: timeout: not found (rc=127)
```
`/etc/profile.d/meow_card_path.sh` 只对**登录 shell** 生效;`ssh host 'cmd'` 走的是
非登录 shell。所以卡上的工具必须用下面两种方式之一调用:
```sh
sh -lc 'timeout 1 sleep 5' # 登录 shell → PATH 带 opt/bin
/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout 1 sleep 5 # 绝对路径(最稳)
```
**设备上没有 `bash`**(实测 `command -v bash` → MISSING,只有 busybox sh),
所以写 `bash -lc` 会得到 `sh: bash: not found`;要用 `sh -lc`。
实测 `sh -lc 'echo $PATH'` → `/usr/data/mnt/sd_0/MeowOS/opt/bin:/bin:/sbin:/usr/bin:/usr/sbin`,
`command -v timeout` 解析到 `/usr/data/mnt/sd_0/MeowOS/opt/bin/timeout`(能跑)。
`MeowOS/opt/bin/timeout` 也放了同一份(launcher 的 PATH 会 prepend 它),
但**不要指望 ssh 一行命令里能直接命中**。
### 3.2 `timeout` 用法(本仓 Apache-2.0 实现)
```sh
timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARGS...]
timeout 2 sleep 30 # ~2s 返回,rc=124
timeout -s KILL 2 sleep 30 # rc=137
timeout -k 1 5 sh -c 'sleep 100 & sleep 100 & wait' # TERM 后 1s 仍在 → KILL,孙进程一起死
timeout 0.2 sleep 5 # 支持小数秒(~200ms 返回)
timeout 8 bluetoothctl connect XX:XX:XX:XX:XX:XX # 挂死的外部命令的默认包装
```
- 超时信号发给 COMMAND 的**整个进程组**(`setpgid(0,0)` + `kill(-pgid, SIG)`),
命令自己 fork 的孙进程一并终止;直接子进程一定被 `waitpid` 回收,不留僵尸。
- 两阶段:先 `-s`(默认 TERM),`-k` 到期再 KILL。不是忙等,计时全在内核。
- 退出码:`124` 超时;`137` 超时且最终发了 KILL;`128+N` 命令自己死于信号 N;
`125` timeout 自身参数/系统调用错;`126` 命令无法执行;`127` 命令不存在。
- 不支持 `--preserve-status` / `-v` / `d` 后缀,给了会明确报 125(不静默忽略)。
- 不跟踪 `setsid()` 逃出进程组的守护进程(coreutils 也不跟踪)。
## 4. exFAT 语义(权限 / 软链)
设备挂载:`/dev/mmcblk0 on /usr/data/mnt/sd_0 type exfat (rw, fmask=0022, dmask=0022, symlink=0, noexec 未设置)`。
@@ -100,11 +199,19 @@ fi
| ncurses 6.5 | MIT 风格(X11) | 保留版权与许可声明;只静态链入 slabtop,不单独分发 |
| lsof 4.99.4 | Purdue Research Foundation 许可(BSD 风格 4 条) | 文档与源码中保留作者/Purdue 署名;改过的版本要标注 |
| **musl**(zig 0.17 自带) | MIT | 七个产物都静态链入了它 → 随副本保留版权与许可声明(`licenses/musl-COPYRIGHT`) |
| **timeout** | **Apache-2.0**(本仓自有) | 源码 `scripts/build/devkit-src/timeout.c`,独立实现,**未拷贝 busybox/GPL 代码**;分发时附 `licenses/meowos-timeout-LICENSE`(Apache-2.0 原文) |
**GPLv2 §3**:`build_devkit.sh` 里的固定版本 + 下载 URL **不构成合规**。URL 作者可能撤、
镜像可能消失,而且 GPL 要求的是*分发者*提供的源码或书面要约。因此 `--with-sources` 会把
**三份源码包 + `build_devkit.sh` + 本文件**一起放进 `src/`(合计约 5.6 MB)。少一样都不算完整:
没有构建脚本,收卡人拿不到本轮用的 `configure`/`-D` 参数(这正是 "corresponding source" 明确包含的)。
**三份源码包 + `build_devkit.sh` + 本文件 + `timeout.c`** 一起放进 `src/`(合计约 5.6 MB)。
少一样都不算完整:没有构建脚本,收卡人拿不到本轮用的 `configure`/`-D` 参数
(这正是 "corresponding source" 明确包含的)。
**timeout(Apache-2.0,本仓自有)**:它与上面这些 GPL/LGPL 组件**没有任何源码混合**,
不是从 busybox 改的(busybox 的 `coreutils/timeout.c` 是 GPL-2.0,搬进来会造成许可污染)。
`timeout.c` 是照着 fork/setpgid/kill/setitimer/signalfd 的系统调用语义从零写的。
Apache-2.0 §4 要求分发时附许可证原文 → `licenses/meowos-timeout-LICENSE`;
`src/timeout.c` 随 `--with-sources` 一起上卡(可复现性,不是 GPL 义务)。
**LGPL-2.1 §6(静态链接)**:六个 procps 工具静态链入 libproc2,除 GPL 义务外还必须
「显著提示使用了该库 + 附许可证副本 + 三选一」:
@@ -208,6 +315,14 @@ fi
| `sleep(1)`(整秒) | 正常:`vmstat 1 3` 墙钟实测 2 秒、`vmstat 1 2` 实测 1 秒 | 整秒采样周期不受影响 |
| `ps -eo lstart` | 正常 | 时间来自 `/proc/stat` 的 `btime` + jiffies,不走 libc 取时 |
| `slabtop`/`pmap`/`sysctl`/`lsof` | 不受影响 | 不显示墙钟、也不做亚秒等待 |
| `timerfd_settime()`(2026-11-11 新测) | **静默失效**:返回 0、`errno=0`,但计时器根本没启动(`timerfd_gettime` 显示 `0.000000000`,`poll` 3 秒等不到)。用 32 位 timespec 结构的原始 syscall(MIPS o32 `4323`)或 `setitimer` 都正常 | 任何用 `timerfd` 做超时的实现都会变成"永不超时"——`timeout` 第一版就栽在这里:`timeout 2 sleep 30` 等满 30 秒、`rc=0` |
| `setitimer(ITIMER_REAL)` + `SIGALRM`(2026-11-11 新测) | **正常**:1s / 200ms 都按点触发(musl 对 `setitimer` 做了 32 位 timeval 转换) | `timeout` 最终用它计时,配上 `signalfd`(SIGALRM + SIGCHLD)与 `ppoll`,零轮询 |
> **教训**:这台机器上"time64 家族"的坏法不止一种。`clock_gettime` 是返回垃圾值,
> `timerfd_settime` 是**返回成功但什么都不做**——后者更阴险,因为没有错误码可查。
> 新写任何计时/睡眠代码,先在设备上跑一个最小探针验证行为,不要假设 libc 正常。
> 仓里 M16 的 `patch_v2/sdk/meow_ui_time.c` 正是接管了 `__timerfd_*` 一族,
> 侧面印证了这条路径需要修。
**能否复用仓里已有的修复?** 有。M16 的 `patch_v2/sdk/meow_ui_time.c`(文档 `patch_v2/sdk/CLOCK_ABI.md`)
用「链接期强定义」接管了 `__clock_gettime` / `__clock_gettime64` / `__nanosleep_time64` /
@@ -297,6 +412,65 @@ meowpanel 1395 root 3u inet 2587 0t0 TCP *:8080 (LISTEN)
- 设备上 `sha256sum` 与 `MANIFEST.txt` 逐条一致(测的就是交付的那份字节)。
- 宿主侧静态/架构自检、MANIFEST 对账、licenses 完整性:`bash scripts/build/build_devkit.sh --verify-only`。
## 11.1 `timeout` 真机验收(2026-11-11,root@10.58.215.50,内核 4.4.94+)
部署位置(两份同一字节):
`/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout`(主位置,和 slabtop 那批一起)
与 `/usr/data/mnt/sd_0/MeowOS/opt/bin/timeout`(launcher 的 PATH 会 prepend 它)。
原始输出(`T=.../devkit/bin/timeout`,`cnt()` 读 `/proc/<pid>/comm` 精确计数,
不会像 `grep` 那样把命令行文本算进去):
```
== [1] timeout 2 sleep 30
rc=124 elapsed=2.0s bt=0 sleep=0
== [2] timeout -s KILL 2 sleep 30
rc=137 elapsed=2.0s
== [3] timeout 2 sleep 1
rc=0 elapsed=1.0s
== [4] timeout 5 sh -c "sleep 100 & sleep 100 & wait"
rc=124 elapsed=5.0s bt=0 sleep=0 ps w | grep "sleep 100" = 0
== [prec] timeout 0.2 sleep 5
rc=124 elapsed=0.2s
== [6] timeout(无参数)
用法: timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...
noargs_rc=125
```
第 4 条是重点:`sh` 自己 fork 的两个 `sleep 100` 随进程组一起被杀,
`/proc` 里 `comm=sleep` 的进程数回到 **0**(不是"看起来没了",是逐 pid 核对)。
### PATH 实测(非登录 shell vs 绝对路径 vs 登录 shell)
```
ssh root@<ip> 'echo $PATH'
→ /usr/sbin:/usr/bin:/sbin:/bin
ssh root@<ip> 'timeout 1 sleep 5; echo rc=$?'
→ sh: timeout: not found / rc=127
绝对路径 /usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout 1 sleep 5
→ rc=124 elapsed=1.0s
sh -lc 'command -v timeout'
→ /usr/data/mnt/sd_0/MeowOS/opt/bin/timeout
command -v bash
→ MISSING(设备只有 busybox sh,没有 bash)
```
结论:**ssh 默认非登录 shell,PATH 不含 opt/bin;请用绝对路径或 `sh -lc`**(不是 `bash -lc`)。
### 真实场景:`timeout 8 bluetoothctl connect`
```
before: bt=0 sleep=0
$T 8 bluetoothctl connect 33:DE:2E:21:AD:30
Attempting to connect to 33:DE:2E:21:AD:30
rc=0 elapsed=0.3s
after: bt=0 sleep=0 (ps w | grep bluetoothctl 无残留)
```
耳机当时是关机状态,`bluetoothctl` 5.54 在尝试后自己退出了(0.3s,rc=0),
所以这条没跑到 8s 上限——但它证明了**调用方式可用且不留残留**;
"到点必杀"的能力由上面第 1–4 条的 `sleep` 用例证明(同一套 kill 路径)。
## 12. 其它已知限制
- `pmap -x` 无 PSS(无 smaps),只有 maps 级别的段/文件信息。