fix(m53): r1 评审 P2 — 小数后缀时长、文档数字、信号失败告警

P2-1 时长解析:后缀乘数只作用在整数部分,nsec 没跟着乘。
  0.05m 被算成 50ms(应 3s)、1.5m 算成 60.5s(应 90s)、0.5h 算成 0.5s。
  修法:sec 与 nsec 都乘 mult(各自带溢出检查,mult==1 行为不变)。
  --help 与头注释的示例改为逐个标出等价值(2.5s=2.5s、1.5m=90s、0.5h=1800s),
  不再留文档写着、实际算错的例子。
  新增 T11:宿主上编译 parser 单测(27 例,含 0.01m/0.05m/1.5m/0.5h)真跑,
  并单独点名评审抓到的三条,防止回归。

P2-2 文档数字:§2 产物表 47 708 → 49 676,合计 2 189 472 → 2 191 440;
  全表按 MANIFEST 重核(其余七项未漂移),§11.1 补上新的体积/md5/sha256。

P2-3 信号失败不静默:新增 kill_child()(kill(-pgid) 失败时 ESRCH 视为组已消失,
  退化为 kill(pid),EPERM 等明确告警)、verify_process_group()(发信号前用
  getpgid 核对建组是否生效)、父进程 setpgid 的异常 errno 告警;连兜底 kill
  都失败时退出码 125 而不是谎报 124。新增 -DTIMEOUT_TEST_SETPGID_FAIL 故障注入
  与 T12(a-d)验证告警路径可达且兜底真的杀掉子进程。

真机复验(md5 aded39e0,49 676 B):124/137/0/孙进程清零/保证挂住的命令 3.01s
被 124 结束/0.01m=0.61s、0.05m=3.01s、2.5s=2.51s/无参 125;收工 bt=0 sleep=0,
Music 199164544。回归 44 通过 0 失败。
This commit is contained in:
AskaEth committed 2026-11-11 21:23:52 +08:00
1 parent 24f1d8a682
commit c93814a78b
4 files changed
+314 -27

No files matched your search

+59 -15
View File
@@ -78,8 +78,8 @@ timeout 5 sh -c 'sleep 100 & sleep 100 & wait' # 孙进程也必须死,ps
| `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` 网络视图 |
| `timeout` | 本仓 `devkit-src/timeout.c` | **Apache-2.0**(自有独立实现,非 busybox/GPL 代码) | 47 708 | 给任何外部命令加"最多等 N 秒、然后杀掉整个进程组";busybox 1.31 无此 applet |
| | | **合计** | **2 189 472 ≈ 2.1 MB** | |
| `timeout` | 本仓 `devkit-src/timeout.c` | **Apache-2.0**(自有独立实现,非 busybox/GPL 代码) | 49 676 | 给任何外部命令加"最多等 N 秒、然后杀掉整个进程组";busybox 1.31 无此 applet |
| | | **合计** | **2 191 440 ≈ 2.1 MB** | |
`timeout` 走的是**本地源码编译路径**(`scripts/build/devkit-src/*.c` → `build_local`),
与上游包(procps/lsof/ncurses)的「下载 + sha256 + configure/make」流程完全隔离:
@@ -151,14 +151,23 @@ 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 0.5m sleep 60 # 小数 + 后缀是叠加的:0.5m = 30 秒
timeout 8 bluetoothctl connect XX:XX:XX:XX:XX:XX # 挂死的外部命令的默认包装
```
- **时长解析**:`纯数字=秒`,可带小数,可带 `s/m/h`,**小数与后缀叠加**:
`2.5s`=2.5s、`0.05m`=3s、`1.5m`=90s、`0.5h`=1800s、`0.01m`=0.6s。
(r1 评审抓到的 P2:旧实现只把后缀乘到整数部分,`0.05m` 会变成 50ms——
「比要求更早被杀」,静默给错答案。现有 `test_build_devkit.sh` T11 真跑 parser 回归。)
- 超时信号发给 COMMAND 的**整个进程组**(`setpgid(0,0)` + `kill(-pgid, SIG)`),
命令自己 fork 的孙进程一并终止;直接子进程一定被 `waitpid` 回收,不留僵尸。
- 两阶段:先 `-s`(默认 TERM),`-k` 到期再 KILL。不是忙等,计时全在内核。
- **杀不动时不装作成功**(18:44 事故的形态就是"进程没被杀掉、越积越多"):
发信号前用 `getpgid()` 核对进程组;`kill(-pgid)` 返回 `ESRCH` 视为"组已不存在",
退化为 `kill(pid, sig)` 只杀直接子进程;`EPERM` 等真错误会打一行明确告警;
连兜底 kill 都失败时退出码是 **125**(不是 124)——不谎报"已超时处理"。
- 退出码:`124` 超时;`137` 超时且最终发了 KILL;`128+N` 命令自己死于信号 N;
`125` timeout 自身参数/系统调用错;`126` 命令无法执行;`127` 命令不存在。
`125` timeout 自身参数/系统调用错或信号完全发不出去;`126` 命令无法执行;`127` 命令不存在。
- 不支持 `--preserve-status` / `-v` / `d` 后缀,给了会明确报 125(不静默忽略)。
- 不跟踪 `setsid()` 逃出进程组的守护进程(coreutils 也不跟踪)。
@@ -172,6 +181,12 @@ $T 5 sh -c 'sleep 100 & sleep 100 & wait'; echo "t4=$? (want 124)"
for p in /proc/[0-9]*; do [ "$(cat $p/comm 2>/dev/null)" = sleep ] && echo "LEAK $p"; done
```
带后缀小数的自检(秒数用 `date` 前后的墙钟差核对,见 §11.1):
```sh
$T 0.01m sleep 5; echo "u1=$? (want 124, ~0.6s)"; $T 0.05m sleep 5; echo "u2=$? (want 124, ~3s)"
```
## 4. exFAT 语义(权限 / 软链)
设备挂载:`/dev/mmcblk0 on /usr/data/mnt/sd_0 type exfat (rw, fmask=0022, dmask=0022, symlink=0, noexec 未设置)`。
@@ -428,27 +443,41 @@ meowpanel 1395 root 3u inet 2587 0t0 TCP *:8080 (LISTEN)
`/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout`(主位置,和 slabtop 那批一起)
与 `/usr/data/mnt/sd_0/MeowOS/opt/bin/timeout`(launcher 的 PATH 会 prepend 它)。
本轮交付的字节:**49 676 B**,md5 `aded39e0f47012fb5827b7cdb2b3b2e1`,
sha256 `5c4d35c63f246aab6bfa70a1fbfd182ac16e668831f69fff6104515a99795a22`
(`MANIFEST.txt` 里的就是这份;两份卡上副本 md5 与构建产物一致)。
原始输出(`T=.../devkit/bin/timeout`,`cnt()` 读 `/proc/<pid>/comm` 精确计数,
不会像 `grep` 那样把命令行文本算进去):
```
== [1] timeout 2 sleep 30
rc=124 elapsed=2.0s bt=0 sleep=0
rc=124 elapsed=2.02s
== [2] timeout -s KILL 2 sleep 30
rc=137 elapsed=2.0s
rc=137 elapsed=2.01s
== [3] timeout 2 sleep 1
rc=0 elapsed=1.0s
rc=0 elapsed=1.01s
== [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
rc=124 elapsed=5.01s after4: bt=0 sleep=0 sleep100=0
== [5] 保证会挂住的命令:timeout 3 sh -c "sleep 30"
rc=124 elapsed=3.01s
== [6] 小数 + 后缀(r2 修复的重点)
0.01m (want ~0.6s): rc=124 elapsed=0.61s
0.05m (want ~3.0s): rc=124 elapsed=3.01s
2.5s (want ~2.5s): rc=124 elapsed=2.51s
1m vs sleep 1 (want rc=0 ~1s): rc=0 elapsed=1.02s
== [7] timeout(无参数)
rc=125(stderr 打印用法)
== 收工: bt=0 sleep=0
```
第 4 条是重点:`sh` 自己 fork 的两个 `sleep 100` 随进程组一起被杀,
`/proc` 里 `comm=sleep` 的进程数回到 **0**(不是"看起来没了",是逐 pid 核对)。
第 5 条是对评审建议的补充:用一个**确定会挂住**的命令(`sh -c 'sleep 30'`)
证明"到点必杀",不再依赖 `bluetoothctl` 是否恰好秒退。
> r1 的旧数字(47 708 B / 48 012 B)与旧的 `0.05m≈0.06s` 输出已被本轮取代;
> 本文档、`MANIFEST.txt`、`README.txt` 与卡上两份副本均对齐到 49 676 B。
### PATH 实测(非登录 shell vs 绝对路径 vs 登录 shell)
@@ -467,7 +496,7 @@ command -v bash
结论:**ssh 默认非登录 shell,PATH 不含 opt/bin;请用绝对路径或 `sh -lc`**(不是 `bash -lc`)。
### 真实场景:`timeout 8 bluetoothctl connect`
### 真实场景:`timeout 8 bluetoothctl connect`(r1 记录,保留作对照)
```
before: bt=0 sleep=0
@@ -478,8 +507,23 @@ after: bt=0 sleep=0 (ps w | grep bluetoothctl 无残留)
```
耳机当时是关机状态,`bluetoothctl` 5.54 在尝试后自己退出了(0.3s,rc=0),
所以这条没跑到 8s 上限——但它证明了**调用方式可用且不留残留**;
"到点必杀"的能力由上面第 1–4 条的 `sleep` 用例证明(同一套 kill 路径)。
所以这条**没跑到 8s 上限**。评审指出这没验证到关键点——已改用第 5 条
`timeout 3 sh -c "sleep 30"`(确定挂住)作为"到点必杀"的证据。
### 故障注入自证(setpgid 失败告警路径)
`timeout.c` 带一个只给测试用的编译开关 `-DTIMEOUT_TEST_SETPGID_FAIL`(生产构建不定义)。
宿主上编译后运行 `timeout-pgfail 1 sleep 30`,实测 stderr:
```
timeout: 警告: setpgid(16333) 失败: Operation not permitted —— 超时信号可能只能发给该进程本身
timeout: 警告: 子进程 16333 未建立独立进程组(实际 pgid=16322)——setpgid 未生效,超时信号将尽量只发给该进程本身,它 fork 的孙进程可能残留
timeout: 警告: 超时: 进程组 16333 不存在,已退化为只给直接子进程 16333 发信号 15
rc=124 elapsed=1084ms
```
三条告警都出现、兜底 `kill(pid)` 真的杀掉了 `sleep 30`、退出码仍是 124。
这条自证已固化进 `test_build_devkit.sh` 的 T12(T12a–T12d),每次跑回归都会执行。
## 12. 其它已知限制