Merge branch 'feat/m53-devkit-timeout-utility'
This commit is contained in:
commit
26cab3a0ed
5 files changed
+1152
-22
No files matched your search
+233
-5
@@ -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. 产物清单
|
## 2. 产物清单
|
||||||
|
|
||||||
全部:`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
|
全部:`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 |
|
| `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 |
|
| `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` 网络视图 |
|
| `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 代码) | 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」流程完全隔离:
|
||||||
|
不联网、不 patch、不打乱上游构建参数。新增本地工具只需往该目录丢一个 `.c`。
|
||||||
|
|
||||||
同目录还有 `MANIFEST.txt`(每个二进制的 sha256 + 版本 + 许可证 + 体积)、
|
同目录还有 `MANIFEST.txt`(每个二进制的 sha256 + 版本 + 许可证 + 体积)、
|
||||||
`licenses/`(procps COPYING + COPYING.LIB、ncurses COPYING、lsof COPYING、**musl COPYRIGHT**)、
|
`licenses/`(procps COPYING + COPYING.LIB、ncurses COPYING、lsof COPYING、**musl COPYRIGHT**、
|
||||||
`README.txt`(上卡说明)。`--with-sources` 会额外生成 `src/`(三份源码包 + `build_devkit.sh` + 本文件)。
|
**meowos-timeout-LICENSE**=Apache-2.0 原文)、
|
||||||
|
`README.txt`(上卡说明)。`--with-sources` 会额外生成 `src/`(三份源码包 + `build_devkit.sh` + 本文件 + `timeout.c`)。
|
||||||
|
|
||||||
## 3. 上卡后怎么用
|
## 3. 上卡后怎么用
|
||||||
|
|
||||||
@@ -59,10 +110,83 @@ fi
|
|||||||
| `free` `lsof` `top` | `/usr/bin/x -> ../../bin/busybox` | `free`/`lsof` 解析到 devkit 版;`top` 仍是 busybox(本工具集不提供) |
|
| `free` `lsof` `top` | `/usr/bin/x -> ../../bin/busybox` | `free`/`lsof` 解析到 devkit 版;`top` 仍是 busybox(本工具集不提供) |
|
||||||
| `ps` | `/bin/ps -> busybox` | 解析到 devkit 版 |
|
| `ps` | `/bin/ps -> busybox` | 解析到 devkit 版 |
|
||||||
| `vmstat` `slabtop` `pmap` `sysctl` | 不存在(busybox 无对应 applet) | 新增 |
|
| `vmstat` `slabtop` `pmap` `sysctl` | 不存在(busybox 无对应 applet) | 新增 |
|
||||||
|
| `timeout` | 不存在(busybox 1.31 无对应 applet;`command -v timeout` 为空) | 新增 |
|
||||||
|
|
||||||
要临时用回 busybox 版:`busybox free`、`busybox ps`、`busybox lsof`。
|
要临时用回 busybox 版:`busybox free`、`busybox ps`、`busybox lsof`。
|
||||||
不要覆盖 `/usr/bin`、`/bin` 里的软链:rootfs 是 ubifs,随固件回刷即被还原,而且这些软链是基线的一部分。
|
不要覆盖 `/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 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` 命令不存在。
|
||||||
|
- 不支持 `--preserve-status` / `-v` / `d` 后缀,给了会明确报 125(不静默忽略)。
|
||||||
|
- 不跟踪 `setsid()` 逃出进程组的守护进程(coreutils 也不跟踪)。
|
||||||
|
|
||||||
|
**5 行自检(每次上卡后复跑,全绿才算 timeout 可用)**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
T=/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout
|
||||||
|
$T 2 sleep 30; echo "t1=$? (want 124)"; $T -s KILL 2 sleep 30; echo "t2=$? (want 137)"
|
||||||
|
$T 2 sleep 1; echo "t3=$? (want 0)"
|
||||||
|
$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 语义(权限 / 软链)
|
## 4. exFAT 语义(权限 / 软链)
|
||||||
|
|
||||||
设备挂载:`/dev/mmcblk0 on /usr/data/mnt/sd_0 type exfat (rw, fmask=0022, dmask=0022, symlink=0, noexec 未设置)`。
|
设备挂载:`/dev/mmcblk0 on /usr/data/mnt/sd_0 type exfat (rw, fmask=0022, dmask=0022, symlink=0, noexec 未设置)`。
|
||||||
@@ -100,11 +224,19 @@ fi
|
|||||||
| ncurses 6.5 | MIT 风格(X11) | 保留版权与许可声明;只静态链入 slabtop,不单独分发 |
|
| ncurses 6.5 | MIT 风格(X11) | 保留版权与许可声明;只静态链入 slabtop,不单独分发 |
|
||||||
| lsof 4.99.4 | Purdue Research Foundation 许可(BSD 风格 4 条) | 文档与源码中保留作者/Purdue 署名;改过的版本要标注 |
|
| lsof 4.99.4 | Purdue Research Foundation 许可(BSD 风格 4 条) | 文档与源码中保留作者/Purdue 署名;改过的版本要标注 |
|
||||||
| **musl**(zig 0.17 自带) | MIT | 七个产物都静态链入了它 → 随副本保留版权与许可声明(`licenses/musl-COPYRIGHT`) |
|
| **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 作者可能撤、
|
**GPLv2 §3**:`build_devkit.sh` 里的固定版本 + 下载 URL **不构成合规**。URL 作者可能撤、
|
||||||
镜像可能消失,而且 GPL 要求的是*分发者*提供的源码或书面要约。因此 `--with-sources` 会把
|
镜像可能消失,而且 GPL 要求的是*分发者*提供的源码或书面要约。因此 `--with-sources` 会把
|
||||||
**三份源码包 + `build_devkit.sh` + 本文件**一起放进 `src/`(合计约 5.6 MB)。少一样都不算完整:
|
**三份源码包 + `build_devkit.sh` + 本文件 + `timeout.c`** 一起放进 `src/`(合计约 5.6 MB)。
|
||||||
没有构建脚本,收卡人拿不到本轮用的 `configure`/`-D` 参数(这正是 "corresponding source" 明确包含的)。
|
少一样都不算完整:没有构建脚本,收卡人拿不到本轮用的 `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 义务外还必须
|
**LGPL-2.1 §6(静态链接)**:六个 procps 工具静态链入 libproc2,除 GPL 义务外还必须
|
||||||
「显著提示使用了该库 + 附许可证副本 + 三选一」:
|
「显著提示使用了该库 + 附许可证副本 + 三选一」:
|
||||||
@@ -208,6 +340,14 @@ fi
|
|||||||
| `sleep(1)`(整秒) | 正常:`vmstat 1 3` 墙钟实测 2 秒、`vmstat 1 2` 实测 1 秒 | 整秒采样周期不受影响 |
|
| `sleep(1)`(整秒) | 正常:`vmstat 1 3` 墙钟实测 2 秒、`vmstat 1 2` 实测 1 秒 | 整秒采样周期不受影响 |
|
||||||
| `ps -eo lstart` | 正常 | 时间来自 `/proc/stat` 的 `btime` + jiffies,不走 libc 取时 |
|
| `ps -eo lstart` | 正常 | 时间来自 `/proc/stat` 的 `btime` + jiffies,不走 libc 取时 |
|
||||||
| `slabtop`/`pmap`/`sysctl`/`lsof` | 不受影响 | 不显示墙钟、也不做亚秒等待 |
|
| `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`)
|
**能否复用仓里已有的修复?** 有。M16 的 `patch_v2/sdk/meow_ui_time.c`(文档 `patch_v2/sdk/CLOCK_ABI.md`)
|
||||||
用「链接期强定义」接管了 `__clock_gettime` / `__clock_gettime64` / `__nanosleep_time64` /
|
用「链接期强定义」接管了 `__clock_gettime` / `__clock_gettime64` / `__nanosleep_time64` /
|
||||||
@@ -297,6 +437,94 @@ meowpanel 1395 root 3u inet 2587 0t0 TCP *:8080 (LISTEN)
|
|||||||
- 设备上 `sha256sum` 与 `MANIFEST.txt` 逐条一致(测的就是交付的那份字节)。
|
- 设备上 `sha256sum` 与 `MANIFEST.txt` 逐条一致(测的就是交付的那份字节)。
|
||||||
- 宿主侧静态/架构自检、MANIFEST 对账、licenses 完整性:`bash scripts/build/build_devkit.sh --verify-only`。
|
- 宿主侧静态/架构自检、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 它)。
|
||||||
|
|
||||||
|
本轮交付的字节:**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.02s
|
||||||
|
== [2] timeout -s KILL 2 sleep 30
|
||||||
|
rc=137 elapsed=2.01s
|
||||||
|
== [3] timeout 2 sleep 1
|
||||||
|
rc=0 elapsed=1.01s
|
||||||
|
== [4] timeout 5 sh -c "sleep 100 & sleep 100 & wait"
|
||||||
|
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)
|
||||||
|
|
||||||
|
```
|
||||||
|
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`(r1 记录,保留作对照)
|
||||||
|
|
||||||
|
```
|
||||||
|
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 上限**。评审指出这没验证到关键点——已改用第 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. 其它已知限制
|
## 12. 其它已知限制
|
||||||
|
|
||||||
- `pmap -x` 无 PSS(无 smaps),只有 maps 级别的段/文件信息。
|
- `pmap -x` 无 PSS(无 smaps),只有 maps 级别的段/文件信息。
|
||||||
|
|||||||
+32
-2
@@ -12,7 +12,8 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `build_fw_apps.sh` | **通用打包脚本**(配置驱动)。读清单 → 组树 → repack → 校验 → 出固件。**不要改它的逻辑**,除非打包流程本身要变。 |
|
| `build_fw_apps.sh` | **通用打包脚本**(配置驱动)。读清单 → 组树 → repack → 校验 → 出固件。**不要改它的逻辑**,除非打包流程本身要变。 |
|
||||||
| `fw_content.conf` | **固件内容清单**(默认配置)。每行一条 `<src>|<dst>|<md5>|<desc>`,声明"哪个本地文件装到固件哪里"。**改固件内容只改这里。** |
|
| `fw_content.conf` | **固件内容清单**(默认配置)。每行一条 `<src>|<dst>|<md5>|<desc>`,声明"哪个本地文件装到固件哪里"。**改固件内容只改这里。** |
|
||||||
| `build_devkit.sh` | **TF 卡调试工具集**交叉编译脚本(zig → 静态 MIPS32r2):`vmstat/slabtop/free/ps/pmap/sysctl/lsof` → `fw_apps/devkit/`。与固件打包无关,独立跑。详见 `DEVKIT.md`。 |
|
| `build_devkit.sh` | **TF 卡调试工具集**交叉编译脚本(zig → 静态 MIPS32r2):`vmstat/slabtop/free/ps/pmap/sysctl/lsof`(上游包)+ `timeout`(本仓 `devkit-src/` 本地源码)→ `fw_apps/devkit/`。与固件打包无关,独立跑。详见 `DEVKIT.md`。 |
|
||||||
|
| `devkit-src/` | devkit 的**本地源码**(Apache-2.0,本仓自有):`timeout.c`。`build_devkit.sh` 的 `build_local` 路径编译它,不下载、不 patch、不混入上游流程。 |
|
||||||
| `rebuild_binaries.sh` | **进固件的那批组件重编驱动**:`bash scripts/build/rebuild_binaries.sh [bootmenu netmgrd audiod audio launcherd]`。在干净 checkout / git worktree 里**开箱可跑**:兼容 HOME 里补 `.tools`(zig 工具链)与 `patch_v2`,`meow_ui.a` 自动从主 checkout 取(取不到会报错列出试过的路径),产物就地写回 `patch_v2/<组件>/`。 |
|
| `rebuild_binaries.sh` | **进固件的那批组件重编驱动**:`bash scripts/build/rebuild_binaries.sh [bootmenu netmgrd audiod audio launcherd]`。在干净 checkout / git worktree 里**开箱可跑**:兼容 HOME 里补 `.tools`(zig 工具链)与 `patch_v2`,`meow_ui.a` 自动从主 checkout 取(取不到会报错列出试过的路径),产物就地写回 `patch_v2/<组件>/`。 |
|
||||||
| `verify_fingerprints.sh` | **产物语义指纹自证**:`bash scripts/build/verify_fingerprints.sh`(`--self-test` 自检判据非空)。重编后 md5 必变,能证明「产物含哪些源码改动」的是字符串/动态段/4263 反汇编判据;time32 门禁的退出码计入 FAIL,不会假绿。 |
|
| `verify_fingerprints.sh` | **产物语义指纹自证**:`bash scripts/build/verify_fingerprints.sh`(`--self-test` 自检判据非空)。重编后 md5 必变,能证明「产物含哪些源码改动」的是字符串/动态段/4263 反汇编判据;time32 门禁的退出码计入 FAIL,不会假绿。 |
|
||||||
| `test_build_devkit.sh` | `build_devkit.sh` 的回归测试(不重编;热缓存约 15s,冷启动需先重建基线 1–2 分钟):stamp 命中时 `--with-sources` 必须产出完整 `src/`;`licenses`/`README` 被删要能恢复、`--verify-only` 必须拒绝;MANIFEST 漂移检测。 |
|
| `test_build_devkit.sh` | `build_devkit.sh` 的回归测试(不重编;热缓存约 15s,冷启动需先重建基线 1–2 分钟):stamp 命中时 `--with-sources` 必须产出完整 `src/`;`licenses`/`README` 被删要能恢复、`--verify-only` 必须拒绝;MANIFEST 漂移检测。 |
|
||||||
@@ -124,13 +125,42 @@ md5sum patch_v2/meow_launcherd/meow_launcherd
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 4.1 devkit 本地源码(`timeout`)与「设备禁裸跑」纪律
|
||||||
|
|
||||||
|
`build_devkit.sh` 有两条互不干扰的编译路径:
|
||||||
|
|
||||||
|
| 路径 | 输入 | 流程 |
|
||||||
|
|---|---|---|
|
||||||
|
| A. 上游包 | ncurses / procps-ng / lsof 的 tarball | 固定版本 + sha256 → configure/make(缓存命中不联网) |
|
||||||
|
| B. 本地源码 | `scripts/build/devkit-src/*.c` | 一次 `zigcc -O2 -static`(无下载、无 patch、不进上游流程) |
|
||||||
|
|
||||||
|
`timeout` 走 B 路径:**本仓 Apache-2.0 独立实现**(`devkit-src/timeout.c`),
|
||||||
|
不是 busybox 的 GPL 代码。产物 `fw_apps/devkit/bin/timeout` 与其它工具一起自检
|
||||||
|
(static / ELF32 / MIPS32r2 / 0 NEEDED)、写进 `MANIFEST.txt`(Apache-2.0 行)、
|
||||||
|
`licenses/meowos-timeout-LICENSE` 附 Apache-2.0 原文;`--with-sources` 会把
|
||||||
|
`timeout.c` 一起放进 `src/`。新增本地小工具只需往 `devkit-src/` 丢一个 `.c`。
|
||||||
|
|
||||||
|
**为什么要有它(别跳过这段)**:2026-11-11 18:44,设备因为**裸跑会挂在硬件上的外部命令**
|
||||||
|
(`bluetoothctl`,累积 55 个挂死客户端)耗尽进程表:18:44:13 launcher 最后日志、
|
||||||
|
18:44:14 sshd 最后 child 退出,之后 11 分钟 `ssh.log` 一行都没有(fork 失效),
|
||||||
|
只能重启。完整时间线与纪律见 **`DEVKIT.md` §1.1**。最短版本:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
timeout 8 bluetoothctl connect <MAC> # 设备侧禁止裸跑,一律加超时并封顶重试次数
|
||||||
|
timeout 5 sh -c 'sleep 100 & sleep 100 & wait' # 孙进程也必须死
|
||||||
|
```
|
||||||
|
|
||||||
|
设备上没有 `timeout(1)`(busybox 1.31 也无此 applet),所以这个工具是那条纪律的可执行形态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 5. 踩坑记录(重要)
|
## 5. 踩坑记录(重要)
|
||||||
|
|
||||||
- **脚本共享目录互相清空**:早期每个版本一个脚本、都 `rm -rf fw_apps`,并行跑会删掉彼此的 `.upt` 产物。→ 已改为 `fw_apps/build/`(工作区)+ `fw_apps/out/`(归档)分离,且删光独立脚本。
|
- **脚本共享目录互相清空**:早期每个版本一个脚本、都 `rm -rf fw_apps`,并行跑会删掉彼此的 `.upt` 产物。→ 已改为 `fw_apps/build/`(工作区)+ `fw_apps/out/`(归档)分离,且删光独立脚本。
|
||||||
- **改源码忘更新 md5**:清单里 md5 是强校验,源变了但 md5 没改 → 打包直接失败。**凡是动了清单里列的源文件,先 `md5sum` 更新对应行。**
|
- **改源码忘更新 md5**:清单里 md5 是强校验,源变了但 md5 没改 → 打包直接失败。**凡是动了清单里列的源文件,先 `md5sum` 更新对应行。**
|
||||||
- **SDK 库(`meow_ui.a`)不入固件**:它由 APP(如 hello)链接,走 TF 卡 `MeowAPPs/` 热更新,**不要**加进清单。
|
- **SDK 库(`meow_ui.a`)不入固件**:它由 APP(如 hello)链接,走 TF 卡 `MeowAPPs/` 热更新,**不要**加进清单。
|
||||||
- **`fw_apps/` 被 gitignore**:固件产物与组装树都是构建产物,勿提交。
|
- **`fw_apps/` 被 gitignore**:固件产物与组装树都是构建产物,勿提交。
|
||||||
- **`scripts/build/` 根目录 gitignore**:构建产物与组装树勿提交。但本目录的 **`README.md`/`build_fw_apps.sh`/`fw_content.conf`/`build_devkit.sh`/`test_build_devkit.sh`/`DEVKIT.md` 已 `git add -f` 入库**(这几个是仓库侧应保留的脚本/配置/说明);其余(`fw_apps/` 产物、devkit 编译树等)不入库。
|
- **`scripts/build/` 根目录 gitignore**:构建产物与组装树勿提交。但本目录的 **`README.md`/`build_fw_apps.sh`/`fw_content.conf`/`build_devkit.sh`/`test_build_devkit.sh`/`DEVKIT.md`/`devkit-src/*.c` 已 `git add -f` 入库**(这几个是仓库侧应保留的脚本/配置/说明/本地源码);其余(`fw_apps/` 产物、devkit 编译树等)不入库。新增 `devkit-src/*.c` 后记得同样 `git add -f`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+106
-13
@@ -8,12 +8,19 @@
|
|||||||
# musl 交叉编出完全静态的二进制,放进 TF 卡供开发调试。
|
# musl 交叉编出完全静态的二进制,放进 TF 卡供开发调试。
|
||||||
#
|
#
|
||||||
# 产物(全部静态链接、strip 过、无 NEEDED):
|
# 产物(全部静态链接、strip 过、无 NEEDED):
|
||||||
# fw_apps/devkit/bin/{vmstat,slabtop,free,ps,pmap,sysctl,lsof}
|
# fw_apps/devkit/bin/{vmstat,slabtop,free,ps,pmap,sysctl,lsof,timeout}
|
||||||
# fw_apps/devkit/MANIFEST.txt 版本/许可证/体积/校验摘要
|
# fw_apps/devkit/MANIFEST.txt 版本/许可证/体积/校验摘要
|
||||||
# fw_apps/devkit/licenses/ 各上游 COPYING(分发留痕)
|
# fw_apps/devkit/licenses/ 各上游 COPYING(分发留痕)
|
||||||
# fw_apps/devkit/README.txt 上卡后怎么加 PATH、和 busybox 的同名关系
|
# fw_apps/devkit/README.txt 上卡后怎么加 PATH、和 busybox 的同名关系
|
||||||
# (--with-sources 时另有 fw_apps/devkit/src/:三份源码包 + build_devkit.sh +
|
# (--with-sources 时另有 fw_apps/devkit/src/:三份源码包 + 本脚本 +
|
||||||
# DEVKIT.md,GPLv2 §3 的“对应源码”含构建控制脚本)
|
# DEVKIT.md + timeout.c,GPLv2 §3 的“对应源码”含构建控制脚本)
|
||||||
|
#
|
||||||
|
# 两条编译路径(互不干扰):
|
||||||
|
# A. 上游包(procps-ng / lsof / ncurses)—— 固定版本 + sha256 + 缓存
|
||||||
|
# B. 本地源码(scripts/build/devkit-src/*.c)—— 本仓 Apache-2.0 独立实现
|
||||||
|
# timeout.c 就是 B 路径:设备没有 timeout(1),busybox 1.31 也无该 applet,
|
||||||
|
# 而设备曾因一条挂死的外部命令(bluetoothctl)耗尽进程表、fork 失效、
|
||||||
|
# SSH 完全不可达(2026-11-11 18:44)。B 路径不联网、不打补丁、不改上游流程。
|
||||||
#
|
#
|
||||||
# 源码固定为「版本 + sha256 + 固定 URL」,全部校验后才编译;缓存命中则零联网。
|
# 源码固定为「版本 + sha256 + 固定 URL」,全部校验后才编译;缓存命中则零联网。
|
||||||
#
|
#
|
||||||
@@ -22,6 +29,9 @@
|
|||||||
# 不需要内核头(musl 头足够;只有 lsof 缺 11 个 TCP 状态宏,本脚本 -D 补上)。
|
# 不需要内核头(musl 头足够;只有 lsof 缺 11 个 TCP 状态宏,本脚本 -D 补上)。
|
||||||
# ⚠ 该工具链在 4.4 内核上是 time64 ABI 错配:`vmstat -t` 时间戳列会是空,
|
# ⚠ 该工具链在 4.4 内核上是 time64 ABI 错配:`vmstat -t` 时间戳列会是空,
|
||||||
# 原因/证据/后续修法见 DEVKIT.md「已知限制:工具链 time64」。不是 -D 能修的。
|
# 原因/证据/后续修法见 DEVKIT.md「已知限制:工具链 time64」。不是 -D 能修的。
|
||||||
|
# 同一坑的第二个表现:musl 的 timerfd_settime() 静默失效(返回 0 但不计时)。
|
||||||
|
# timeout 因此既不读时钟也不用 timerfd:超时用 setitimer(ITIMER_REAL)+SIGALRM,
|
||||||
|
# 子进程退出用 SIGCHLD,两者进同一个 signalfd,由一次 ppoll 等(零轮询)。
|
||||||
#
|
#
|
||||||
# 许可证(二进制将来随卡分发时请保留此节):
|
# 许可证(二进制将来随卡分发时请保留此节):
|
||||||
# procps-ng 4.0.4 GPL-2.0-or-later(COPYING=GPLv2)
|
# procps-ng 4.0.4 GPL-2.0-or-later(COPYING=GPLv2)
|
||||||
@@ -31,6 +41,8 @@
|
|||||||
# ncurses 6.5 MIT 风格(X11 许可),保留版权声明即可
|
# ncurses 6.5 MIT 风格(X11 许可),保留版权声明即可
|
||||||
# lsof 4.99.4 Purdue Research Foundation 许可(BSD 风格 4 条),
|
# lsof 4.99.4 Purdue Research Foundation 许可(BSD 风格 4 条),
|
||||||
# 要求文档与源码中保留作者与 Purdue 的署名(COPYING 第 2 条)
|
# 要求文档与源码中保留作者与 Purdue 的署名(COPYING 第 2 条)
|
||||||
|
# timeout Apache-2.0(本仓自有,scripts/build/devkit-src/timeout.c)
|
||||||
|
# 独立实现,未拷贝 busybox/GPL 代码;许可证原文随产物分发
|
||||||
# 本脚本链接的 ncurses 只作为 slabtop 的静态依赖进入产物,不单独分发。
|
# 本脚本链接的 ncurses 只作为 slabtop 的静态依赖进入产物,不单独分发。
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
@@ -69,6 +81,11 @@ NCURSES_VER="6.5"
|
|||||||
PROCPS_VER="4.0.4"
|
PROCPS_VER="4.0.4"
|
||||||
LSOF_VER="4.99.4"
|
LSOF_VER="4.99.4"
|
||||||
|
|
||||||
|
# 本地源码(B 路径):本仓 Apache-2.0 独立实现,不下载、不 patch、不进上游流程。
|
||||||
|
# 每个 *.c 编一个同名可执行文件;新增工具只需丢一个 .c 进这个目录。
|
||||||
|
LOCAL_SRC_DIR="$ROOT/scripts/build/devkit-src"
|
||||||
|
LOCAL_TOOLS="timeout"
|
||||||
|
|
||||||
# 设备内核版本:写进 lsof 的 LINUXV,让它按目标内核(4.4)选代码路径
|
# 设备内核版本:写进 lsof 的 LINUXV,让它按目标内核(4.4)选代码路径
|
||||||
TARGET_KERNEL_VSTR="4.4.94"
|
TARGET_KERNEL_VSTR="4.4.94"
|
||||||
|
|
||||||
@@ -77,18 +94,20 @@ SRC_NCURSES="ncurses-${NCURSES_VER}.tar.gz|https://mirrors.tuna.tsinghua.edu.cn/
|
|||||||
SRC_PROCPS="procps-ng-${PROCPS_VER}.tar.xz|https://downloads.sourceforge.net/project/procps-ng/Production/procps-ng-${PROCPS_VER}.tar.xz|22870d6feb2478adb617ce4f09a787addaf2d260c5a8aa7b17d889a962c5e42e"
|
SRC_PROCPS="procps-ng-${PROCPS_VER}.tar.xz|https://downloads.sourceforge.net/project/procps-ng/Production/procps-ng-${PROCPS_VER}.tar.xz|22870d6feb2478adb617ce4f09a787addaf2d260c5a8aa7b17d889a962c5e42e"
|
||||||
SRC_LSOF="lsof-${LSOF_VER}.tar.xz|https://deb.debian.org/debian/pool/main/l/lsof/lsof_${LSOF_VER}+dfsg.orig.tar.xz|dc3e7f51cf03bc7fbcdd9e62d4a52825682092031bd1ae08b15fe7f4f582aebf"
|
SRC_LSOF="lsof-${LSOF_VER}.tar.xz|https://deb.debian.org/debian/pool/main/l/lsof/lsof_${LSOF_VER}+dfsg.orig.tar.xz|dc3e7f51cf03bc7fbcdd9e62d4a52825682092031bd1ae08b15fe7f4f582aebf"
|
||||||
|
|
||||||
# 卡上 src/ 必须包含的文件:三份源码包 + 构建脚本 + 本文档。
|
# 卡上 src/ 必须包含的文件:三份源码包 + 构建脚本 + 本文档 + 本地源码。
|
||||||
# GPLv2 §3 的“对应源码”明确包含 "scripts used to control compilation and
|
# GPLv2 §3 的“对应源码”明确包含 "scripts used to control compilation and
|
||||||
# installation",缺了脚本,收卡人拿不到本轮用的 -D/configure 参数。
|
# installation",缺了脚本,收卡人拿不到本轮用的 -D/configure 参数。
|
||||||
SRC_STAGE_LIST="ncurses-${NCURSES_VER}.tar.gz procps-ng-${PROCPS_VER}.tar.xz lsof-${LSOF_VER}.tar.xz build_devkit.sh DEVKIT.md"
|
# timeout.c 不在 GPL 义务范围内(Apache-2.0 自有),但带上才能复现整包。
|
||||||
|
SRC_STAGE_LIST="ncurses-${NCURSES_VER}.tar.gz procps-ng-${PROCPS_VER}.tar.xz lsof-${LSOF_VER}.tar.xz build_devkit.sh DEVKIT.md timeout.c"
|
||||||
|
|
||||||
# 交付的工具——验证、MANIFEST 比对、stamp 自检都从这里取,避免多处手写漂移
|
# 交付的工具——验证、MANIFEST 比对、stamp 自检都从这里取,避免多处手写漂移
|
||||||
TOOL_NAMES="vmstat slabtop free ps pmap sysctl lsof"
|
TOOL_NAMES="vmstat slabtop free ps pmap sysctl lsof $LOCAL_TOOLS"
|
||||||
|
|
||||||
# licenses/ 下必须存在的许可证副本(缺一个都算交付不完整):
|
# licenses/ 下必须存在的许可证副本(缺一个都算交付不完整):
|
||||||
# procps 是 GPL-2.0+(COPYING)且 library/ 是 LGPL-2.1+(COPYING.LIB),
|
# procps 是 GPL-2.0+(COPYING)且 library/ 是 LGPL-2.1+(COPYING.LIB),
|
||||||
# ncurses/lsof 是 MIT 风格 / Purdue,musl 是 MIT 且七个产物都静态链入了它。
|
# ncurses/lsof 是 MIT 风格 / Purdue,musl 是 MIT 且七个产物都静态链入了它。
|
||||||
LICENSE_FILES="procps-ng-${PROCPS_VER}-COPYING procps-ng-${PROCPS_VER}-COPYING.LIB ncurses-${NCURSES_VER}-COPYING lsof-${LSOF_VER}-COPYING musl-COPYRIGHT"
|
# timeout 是本仓 Apache-2.0 自有实现,分发二进制时按 Apache-2.0 §4 附许可证原文。
|
||||||
|
LICENSE_FILES="procps-ng-${PROCPS_VER}-COPYING procps-ng-${PROCPS_VER}-COPYING.LIB ncurses-${NCURSES_VER}-COPYING lsof-${LSOF_VER}-COPYING musl-COPYRIGHT meowos-timeout-LICENSE"
|
||||||
|
|
||||||
FORCE=0
|
FORCE=0
|
||||||
CLEAN=0
|
CLEAN=0
|
||||||
@@ -221,12 +240,14 @@ sources_complete() {
|
|||||||
done
|
done
|
||||||
cmp -s "$SCRIPT_PATH" "$OUT_DIR/src/build_devkit.sh" || return 1
|
cmp -s "$SCRIPT_PATH" "$OUT_DIR/src/build_devkit.sh" || return 1
|
||||||
cmp -s "$ROOT/scripts/build/DEVKIT.md" "$OUT_DIR/src/DEVKIT.md" || return 1
|
cmp -s "$ROOT/scripts/build/DEVKIT.md" "$OUT_DIR/src/DEVKIT.md" || return 1
|
||||||
|
# 本地源码也要与仓库逐字节一致:src/ 里的 timeout.c 必须能编出同一个二进制
|
||||||
|
cmp -s "$LOCAL_SRC_DIR/timeout.c" "$OUT_DIR/src/timeout.c" || return 1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
stage_sources() {
|
stage_sources() {
|
||||||
local f
|
local f
|
||||||
log "整理 $OUT_DIR/src(三份源码包 + 构建脚本 + 文档)"
|
log "整理 $OUT_DIR/src(三份源码包 + 本地源码 + 构建脚本 + 文档)"
|
||||||
fetch_src "$SRC_NCURSES"
|
fetch_src "$SRC_NCURSES"
|
||||||
fetch_src "$SRC_PROCPS"
|
fetch_src "$SRC_PROCPS"
|
||||||
fetch_src "$SRC_LSOF"
|
fetch_src "$SRC_LSOF"
|
||||||
@@ -239,6 +260,8 @@ stage_sources() {
|
|||||||
|| die "复制构建脚本失败: $SCRIPT_PATH(GPLv2 §3 要求带构建脚本)"
|
|| die "复制构建脚本失败: $SCRIPT_PATH(GPLv2 §3 要求带构建脚本)"
|
||||||
cp "$ROOT/scripts/build/DEVKIT.md" "$OUT_DIR/src/DEVKIT.md" \
|
cp "$ROOT/scripts/build/DEVKIT.md" "$OUT_DIR/src/DEVKIT.md" \
|
||||||
|| die "复制 DEVKIT.md 失败: $ROOT/scripts/build/DEVKIT.md"
|
|| die "复制 DEVKIT.md 失败: $ROOT/scripts/build/DEVKIT.md"
|
||||||
|
cp "$LOCAL_SRC_DIR/timeout.c" "$OUT_DIR/src/timeout.c" \
|
||||||
|
|| die "复制本地源码失败: $LOCAL_SRC_DIR/timeout.c(Apache-2.0,复现 timeout 需要)"
|
||||||
sources_complete || die "src/ 整理后仍不完整(sha256 或文件缺失)"
|
sources_complete || die "src/ 整理后仍不完整(sha256 或文件缺失)"
|
||||||
info "共 $(du -sh "$OUT_DIR/src" | cut -f1):$(ls "$OUT_DIR/src" | tr '\n' ' ')"
|
info "共 $(du -sh "$OUT_DIR/src" | cut -f1):$(ls "$OUT_DIR/src" | tr '\n' ' ')"
|
||||||
}
|
}
|
||||||
@@ -341,13 +364,41 @@ build_lsof() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4b) 本地源码(B 路径):scripts/build/devkit-src/*.c
|
||||||
|
# 与上游包完全隔离:不下载、不 configure、不 make、不改 TOOLS 里上游的
|
||||||
|
# 源码/构建参数。只做一次 zig cc,产物落在 $BUILD_DIR/local/<name>,
|
||||||
|
# 之后由 collect_and_verify 统一 strip + 静态/架构/NEEDED 自检。
|
||||||
|
# -ffile-prefix-map 抹掉宿主路径(与上游同样的可复现性要求,见 DEVKIT.md §10);
|
||||||
|
# -static 显式给,不依赖工具链默认值。
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
build_local() {
|
||||||
|
local f name
|
||||||
|
log "编译本地源码($LOCAL_SRC_DIR,Apache-2.0 自有实现)"
|
||||||
|
rm -rf "$BUILD_DIR/local"
|
||||||
|
mkdir -p "$BUILD_DIR/local"
|
||||||
|
for f in "$LOCAL_SRC_DIR"/*.c; do
|
||||||
|
[ -f "$f" ] || die "本地源码目录为空: $LOCAL_SRC_DIR"
|
||||||
|
name="$(basename "$f" .c)"
|
||||||
|
info "zigcc → $name"
|
||||||
|
"$ZIGCC" -O2 -Wall -Wextra -static \
|
||||||
|
-ffunction-sections -fdata-sections \
|
||||||
|
-ffile-prefix-map="$LOCAL_SRC_DIR"=/devkit-src \
|
||||||
|
-Wl,--gc-sections \
|
||||||
|
-o "$BUILD_DIR/local/$name" "$f" \
|
||||||
|
> "$BUILD_DIR/local-$name.log" 2>&1 \
|
||||||
|
|| { tail -30 "$BUILD_DIR/local-$name.log"; die "$name 编译失败"; }
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 5) 收集 + strip + 逐项验证
|
# 5) 收集 + strip + 逐项验证
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 产出清单:<源路径>|<目标名>|<出处>|<许可证>
|
# 产出清单:<源路径>|<目标名>|<出处>|<许可证>
|
||||||
# 只放「真机实测能跑且真有价值」的:vmstat/slabtop(busybox 没有)、
|
# 只放「真机实测能跑且真有价值」的:vmstat/slabtop(busybox 没有)、
|
||||||
# free/ps/pmap(busybox 版字段残缺)、sysctl(busybox 1.31 没有此 applet)、
|
# free/ps/pmap(busybox 版字段残缺)、sysctl(busybox 1.31 没有此 applet)、
|
||||||
# lsof(busybox 版只有 pid/fd)。
|
# lsof(busybox 版只有 pid/fd)、timeout(busybox 1.31 没有此 applet,2026-11-11
|
||||||
|
# 整机进程耗尽事故后成为设备侧刚需)。
|
||||||
# 不交付 top:表头时钟被工具链 time64 打坏(不是 procps 的 bug,见 DEVKIT.md §9),
|
# 不交付 top:表头时钟被工具链 time64 打坏(不是 procps 的 bug,见 DEVKIT.md §9),
|
||||||
# 且卡上已有 htop 3.5.3(673KB)→ 属重复且不可信。
|
# 且卡上已有 htop 3.5.3(673KB)→ 属重复且不可信。
|
||||||
TOOLS="
|
TOOLS="
|
||||||
@@ -358,6 +409,7 @@ $BUILD_DIR/procps/src/ps/pscommand|ps|procps-ng $PROCPS_VER|GPL-2.0-or-later
|
|||||||
$BUILD_DIR/procps/src/pmap|pmap|procps-ng $PROCPS_VER|GPL-2.0-or-later
|
$BUILD_DIR/procps/src/pmap|pmap|procps-ng $PROCPS_VER|GPL-2.0-or-later
|
||||||
$BUILD_DIR/procps/src/sysctl|sysctl|procps-ng $PROCPS_VER|GPL-2.0-or-later
|
$BUILD_DIR/procps/src/sysctl|sysctl|procps-ng $PROCPS_VER|GPL-2.0-or-later
|
||||||
$BUILD_DIR/lsof/lsof|lsof|lsof $LSOF_VER|Purdue/BSD-style
|
$BUILD_DIR/lsof/lsof|lsof|lsof $LSOF_VER|Purdue/BSD-style
|
||||||
|
$BUILD_DIR/local/timeout|timeout|devkit-src (local)|Apache-2.0
|
||||||
"
|
"
|
||||||
|
|
||||||
FAIL=0
|
FAIL=0
|
||||||
@@ -420,6 +472,9 @@ write_meta() {
|
|||||||
# musl:七个 procps/lsof 产物都静态链入了 zig 自带 musl(MIT),必须随副本留声明
|
# musl:七个 procps/lsof 产物都静态链入了 zig 自带 musl(MIT),必须随副本留声明
|
||||||
cp "$MUSL_COPYRIGHT" "$OUT_DIR/licenses/musl-COPYRIGHT" \
|
cp "$MUSL_COPYRIGHT" "$OUT_DIR/licenses/musl-COPYRIGHT" \
|
||||||
|| die "复制许可证失败: musl COPYRIGHT($MUSL_COPYRIGHT)"
|
|| die "复制许可证失败: musl COPYRIGHT($MUSL_COPYRIGHT)"
|
||||||
|
# timeout 是本仓自有 Apache-2.0 实现:Apache-2.0 §4 要求分发时附许可证原文
|
||||||
|
cp "$ROOT/LICENSE" "$OUT_DIR/licenses/meowos-timeout-LICENSE" \
|
||||||
|
|| die "复制许可证失败: $ROOT/LICENSE(timeout 的 Apache-2.0 原文)"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "MeowOS devkit — 静态 MIPS32r2 调试工具集"
|
echo "MeowOS devkit — 静态 MIPS32r2 调试工具集"
|
||||||
@@ -439,16 +494,20 @@ EOF
|
|||||||
echo " ncurses $NCURSES_VER MIT-style sha256 $(printf '%s' "$SRC_NCURSES" | awk -F'|' '{print $3}')"
|
echo " ncurses $NCURSES_VER MIT-style sha256 $(printf '%s' "$SRC_NCURSES" | awk -F'|' '{print $3}')"
|
||||||
echo " procps-ng $PROCPS_VER GPL-2.0+ / library=LGPL-2.1+ sha256 $(printf '%s' "$SRC_PROCPS" | awk -F'|' '{print $3}')"
|
echo " procps-ng $PROCPS_VER GPL-2.0+ / library=LGPL-2.1+ sha256 $(printf '%s' "$SRC_PROCPS" | awk -F'|' '{print $3}')"
|
||||||
echo " lsof $LSOF_VER Purdue/BSD sha256 $(printf '%s' "$SRC_LSOF" | awk -F'|' '{print $3}')"
|
echo " lsof $LSOF_VER Purdue/BSD sha256 $(printf '%s' "$SRC_LSOF" | awk -F'|' '{print $3}')"
|
||||||
|
echo " timeout Apache-2.0 本仓 scripts/build/devkit-src/timeout.c(独立实现,未拷贝 GPL 代码)"
|
||||||
|
echo " sha256 $(sha256sum "$LOCAL_SRC_DIR/timeout.c" | cut -d' ' -f1)"
|
||||||
echo " musl zig $ZIG_VERSION 自带(MIT) 许可证原文见 licenses/musl-COPYRIGHT"
|
echo " musl zig $ZIG_VERSION 自带(MIT) 许可证原文见 licenses/musl-COPYRIGHT"
|
||||||
echo " (--with-sources 时)src/ 内还有 build_devkit.sh + DEVKIT.md(构建控制脚本,GPLv2 §3 要求)"
|
echo " (--with-sources 时)src/ 内还有 build_devkit.sh + DEVKIT.md + timeout.c(构建控制脚本与本地源码)"
|
||||||
echo " 重编方法: src/build_devkit.sh(卡上路径;仓库里是 scripts/build/build_devkit.sh)"
|
echo " 重编方法: src/build_devkit.sh(卡上路径;仓库里是 scripts/build/build_devkit.sh)"
|
||||||
echo " 许可证原文: licenses/*-COPYING / licenses/*-COPYRIGHT;说明见 src/DEVKIT.md"
|
echo " 许可证原文: licenses/*-COPYING / licenses/*-COPYRIGHT / licenses/meowos-timeout-LICENSE"
|
||||||
|
echo " 说明见 src/DEVKIT.md"
|
||||||
echo
|
echo
|
||||||
echo "注意: procps-ng 是 GPL-2.0-or-later。把本目录随卡交付给第三方时,"
|
echo "注意: procps-ng 是 GPL-2.0-or-later。把本目录随卡交付给第三方时,"
|
||||||
echo " GPLv2 §3 要求提供对应源码(含构建脚本)—— 请用 --with-sources 带上 src/,"
|
echo " GPLv2 §3 要求提供对应源码(含构建脚本)—— 请用 --with-sources 带上 src/,"
|
||||||
echo " 或随卡附书面要约(有效期 ≥3 年)。仅给出下载 URL 不构成合规。"
|
echo " 或随卡附书面要约(有效期 ≥3 年)。仅给出下载 URL 不构成合规。"
|
||||||
echo " 另外 ps/vmstat/slabtop/free/pmap/sysctl 静态链接了 LGPL-2.1+ 的 libproc2:"
|
echo " 另外 ps/vmstat/slabtop/free/pmap/sysctl 静态链接了 LGPL-2.1+ 的 libproc2:"
|
||||||
echo " LGPL-2.1 §6 要求「源码(§6a) 或 共享库(§6b) 或 书面要约(§6c)」三选一,详见 DEVKIT.md §6。"
|
echo " LGPL-2.1 §6 要求「源码(§6a) 或 共享库(§6b) 或 书面要约(§6c)」三选一,详见 DEVKIT.md §6。"
|
||||||
|
echo " timeout 是 Apache-2.0(本仓自有),与上述 GPL 组件无源码混合。"
|
||||||
} > "$OUT_DIR/MANIFEST.txt"
|
} > "$OUT_DIR/MANIFEST.txt"
|
||||||
|
|
||||||
cat > "$OUT_DIR/README.txt" <<'EOS'
|
cat > "$OUT_DIR/README.txt" <<'EOS'
|
||||||
@@ -464,12 +523,21 @@ MeowOS devkit — 设备端调试工具(静态 MIPS32r2)
|
|||||||
export PATH="/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin:$PATH"
|
export PATH="/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin:$PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
⚠ SSH 默认是**非登录 shell**:`ssh root@设备 'cmd'` 的 PATH 只有
|
||||||
|
/usr/sbin:/usr/bin:/sbin:/bin,**不含 MeowOS/opt/bin,也不含本 devkit 目录**。
|
||||||
|
所以卡上直接 `timeout 1 sleep 5` 会报 not found。两种正确写法:
|
||||||
|
sh -lc 'timeout 1 sleep 5' # 走登录 shell → 带上 opt/bin
|
||||||
|
/usr/data/mnt/sd_0/MeowOS/opt/devkit/bin/timeout 1 sleep 5 # 绝对路径
|
||||||
|
(2026-11-11 实测:ssh 非登录 shell 下 `command -v timeout` 为空。)
|
||||||
|
|
||||||
目录内容(本文件所在目录就是 devkit 根):
|
目录内容(本文件所在目录就是 devkit 根):
|
||||||
bin/ 可执行文件(实体文件,非软链)
|
bin/ 可执行文件(实体文件,非软链)
|
||||||
MANIFEST.txt 版本 / 许可证 / 体积 / sha256
|
MANIFEST.txt 版本 / 许可证 / 体积 / sha256
|
||||||
licenses/ 上游许可证原文(procps GPLv2 + LGPL-2.1、ncurses、lsof、musl)
|
licenses/ 许可证原文(procps GPLv2 + LGPL-2.1、ncurses、lsof、musl、
|
||||||
|
meowos-timeout-LICENSE = Apache-2.0)
|
||||||
src/ 仅当随卡附带了源码(--with-sources)时存在:三份源码包 +
|
src/ 仅当随卡附带了源码(--with-sources)时存在:三份源码包 +
|
||||||
build_devkit.sh + DEVKIT.md。**src/ 是生成目录,请勿手放文件。**
|
build_devkit.sh + DEVKIT.md + timeout.c。
|
||||||
|
**src/ 是生成目录,请勿手放文件。**
|
||||||
说明文档:src/DEVKIT.md(若本源码未随附,见仓库 scripts/build/DEVKIT.md)
|
说明文档:src/DEVKIT.md(若本源码未随附,见仓库 scripts/build/DEVKIT.md)
|
||||||
|
|
||||||
exFAT 没有 Unix 权限位(挂载 fmask=0022 → 新文件一律 0755),也没有符号链接。
|
exFAT 没有 Unix 权限位(挂载 fmask=0022 → 新文件一律 0755),也没有符号链接。
|
||||||
@@ -480,11 +548,30 @@ exFAT 没有 Unix 权限位(挂载 fmask=0022 → 新文件一律 0755),
|
|||||||
与 busybox 同名命令的关系(devkit/bin 前插 PATH 时生效):
|
与 busybox 同名命令的关系(devkit/bin 前插 PATH 时生效):
|
||||||
free ps lsof → 会被本目录版本覆盖(procps/lsof 字段更全)
|
free ps lsof → 会被本目录版本覆盖(procps/lsof 字段更全)
|
||||||
vmstat slabtop pmap sysctl → busybox 1.31 没有这些 applet,直接新增
|
vmstat slabtop pmap sysctl → busybox 1.31 没有这些 applet,直接新增
|
||||||
|
timeout → busybox 1.31 同样没有,直接新增
|
||||||
要临时用回 busybox 版:busybox free / busybox ps / busybox lsof
|
要临时用回 busybox 版:busybox free / busybox ps / busybox lsof
|
||||||
注意:/bin/ps -> busybox,/usr/bin/{free,lsof,top} -> ../../bin/busybox,
|
注意:/bin/ps -> busybox,/usr/bin/{free,lsof,top} -> ../../bin/busybox,
|
||||||
所以“前插 devkit/bin”是唯一让新版本生效的方式(不要覆盖这些软链:
|
所以“前插 devkit/bin”是唯一让新版本生效的方式(不要覆盖这些软链:
|
||||||
rootfs 是 ubifs,随固件回刷会被还原,而且 busybox 软链是基线的一部分)。
|
rootfs 是 ubifs,随固件回刷会被还原,而且 busybox 软链是基线的一部分)。
|
||||||
|
|
||||||
|
timeout 用法(本仓 Apache-2.0 独立实现,非 busybox 代码):
|
||||||
|
timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARGS...]
|
||||||
|
timeout 2 sleep 30 # 2 秒后返回,退出码 124
|
||||||
|
timeout -s KILL 2 sleep 30 # 退出码 137
|
||||||
|
timeout -k 1 5 bluetoothctl connect XX:XX... # TERM 后 1 秒仍在 → KILL
|
||||||
|
timeout 0.2 sleep 5 # 支持小数秒(约 200ms 返回)
|
||||||
|
timeout 0.5m sleep 60 # 小数+后缀叠加:0.5m = 30 秒(1.5m = 90 秒)
|
||||||
|
超时信号发给 COMMAND 的**整个进程组**:命令自己 fork 的孙进程一起死
|
||||||
|
(`timeout 5 sh -c 'a & b & wait'` 不会留下 a/b)。退出码:
|
||||||
|
124=超时 137=超时且最终发了 KILL 128+N=命令死于信号 N
|
||||||
|
125=timeout 自身参数/系统调用错 126=命令无法执行 127=命令不存在
|
||||||
|
杀不动时不装作成功:进程组不可用会告警并退化为只杀直接子进程;
|
||||||
|
连兜底 kill 都失败时退出码 125(不是 124),不会谎报"已处理"。
|
||||||
|
不支持 --preserve-status / -v / d(天)后缀,给了会明确报 125(不静默忽略)。
|
||||||
|
不跟踪 setsid() 逃出进程组的守护进程(coreutils 也不跟踪)。
|
||||||
|
精度是毫秒级够用:计时交给内核 setitimer(ITIMER_REAL)+SIGALRM,不读时钟,
|
||||||
|
误差主要来自调度延迟(单核负载高时数十毫秒)。
|
||||||
|
|
||||||
已知限制:
|
已知限制:
|
||||||
- pmap 只能读 /proc/<pid>/maps;本机 CONFIG_PROC_PAGE_MONITOR=n,
|
- pmap 只能读 /proc/<pid>/maps;本机 CONFIG_PROC_PAGE_MONITOR=n,
|
||||||
/proc/<pid>/smaps 不存在,所以 pmap -x 拿不到 PSS/私有脏页。
|
/proc/<pid>/smaps 不存在,所以 pmap -x 拿不到 PSS/私有脏页。
|
||||||
@@ -496,6 +583,9 @@ rootfs 是 ubifs,随固件回刷会被还原,而且 busybox 软链是基线
|
|||||||
`free -s 1` 与 `vmstat 1 N` 的整秒间隔正常
|
`free -s 1` 与 `vmstat 1 N` 的整秒间隔正常
|
||||||
* ps 的 lstart、slabtop/pmap/sysctl 不受影响
|
* ps 的 lstart、slabtop/pmap/sysctl 不受影响
|
||||||
原因与后续修法见 src/DEVKIT.md「已知限制:工具链 time64」。
|
原因与后续修法见 src/DEVKIT.md「已知限制:工具链 time64」。
|
||||||
|
timeout 不受影响:它不调用 clock_gettime/time,也不用 timerfd
|
||||||
|
(musl 的 timerfd_settime 在这台机上是静默失效的),超时由内核
|
||||||
|
setitimer(ITIMER_REAL)+SIGALRM 计时。
|
||||||
- lsof 不带 -n 时会尝试反向解析(编译进 musl 的 resolv.conf 路径是构建机的,
|
- lsof 不带 -n 时会尝试反向解析(编译进 musl 的 resolv.conf 路径是构建机的,
|
||||||
设备上不存在)→ 实测 `lsof -i` 约 3s 超时;请用 `lsof -nP`。
|
设备上不存在)→ 实测 `lsof -i` 约 3s 超时;请用 `lsof -nP`。
|
||||||
- 不提供 top:见 src/DEVKIT.md(同样是 time64 问题:表头时钟不可信)。
|
- 不提供 top:见 src/DEVKIT.md(同样是 time64 问题:表头时钟不可信)。
|
||||||
@@ -510,6 +600,8 @@ stamp_value() {
|
|||||||
# 带 zig 的完整版本(0.17.0 → 0.17.1 也要触发重建:工具链 patch 会改产物)
|
# 带 zig 的完整版本(0.17.0 → 0.17.1 也要触发重建:工具链 patch 会改产物)
|
||||||
echo "zig=$ZIG_VERSION target=$ZIG_TARGET kernel=$TARGET_KERNEL_VSTR"
|
echo "zig=$ZIG_VERSION target=$ZIG_TARGET kernel=$TARGET_KERNEL_VSTR"
|
||||||
echo "ncurses=$NCURSES_VER procps=$PROCPS_VER lsof=$LSOF_VER"
|
echo "ncurses=$NCURSES_VER procps=$PROCPS_VER lsof=$LSOF_VER"
|
||||||
|
# 本地源码(timeout 等):内容变就必须重编,否则 stamp 命中会漏掉新代码
|
||||||
|
( cd "$LOCAL_SRC_DIR" && sha256sum *.c ) 2>/dev/null || true
|
||||||
} | sha256sum | cut -d' ' -f1
|
} | sha256sum | cut -d' ' -f1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +682,7 @@ main() {
|
|||||||
build_ncurses
|
build_ncurses
|
||||||
build_procps
|
build_procps
|
||||||
build_lsof
|
build_lsof
|
||||||
|
build_local
|
||||||
|
|
||||||
collect_and_verify
|
collect_and_verify
|
||||||
[ "$FAIL" = 0 ] || die "$FAIL 个产物未通过静态/架构自检(见上)"
|
[ "$FAIL" = 0 ] || die "$FAIL 个产物未通过静态/架构自检(见上)"
|
||||||
|
|||||||
@@ -0,0 +1,681 @@
|
|||||||
|
/*
|
||||||
|
* timeout.c — MeowOS devkit: a small, self-contained timeout(1)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
* Copyright (c) 2026 MeowOS contributors
|
||||||
|
*
|
||||||
|
* 独立实现,未拷贝任何 GPL 代码。
|
||||||
|
* busybox 的 coreutils/timeout.c 与本仓 Apache-2.0 不兼容,因此本文件从零编写,
|
||||||
|
* 只依据 POSIX/Linux 系统调用语义(fork/setpgid/kill/setitimer/signalfd/ppoll)。
|
||||||
|
*
|
||||||
|
* 为什么设备需要它(2026-11-11 18:44 整机进程耗尽事故):设备没有 timeout(1),
|
||||||
|
* busybox 1.31.1 也无该 applet,一条挂在硬件上的外部命令(bluetoothctl 等)
|
||||||
|
* 会永久阻塞并持续 fork,最终 dropbear 连 fork 都做不到。本工具让"最多等 N 秒
|
||||||
|
* 然后杀掉整个进程组"成为设备侧可用的默认动作。
|
||||||
|
*
|
||||||
|
* 支持的子集(coreutils timeout 的最小可用子集):
|
||||||
|
* timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...
|
||||||
|
* timeout --help
|
||||||
|
*
|
||||||
|
* DURATION 纯数字=秒;支持小数(0.2、2.5s)与 s/m/h 后缀(大小写均可),
|
||||||
|
* 小数与后缀可叠加:1.5m = 90 秒、0.5h = 1800 秒、0.01m = 0.6 秒。
|
||||||
|
* 不接受 coreutils 的 d(天)后缀,也不接受 0(与本工具用途矛盾)。
|
||||||
|
* -s SIGNAL 超时后发给整个进程组的信号,默认 TERM。可用 SIGTERM/TERM/15。
|
||||||
|
* -k DURATION 发完 -s 信号后再等 DURATION,仍未退出则 SIGKILL(两阶段)。
|
||||||
|
*
|
||||||
|
* 退出码(对齐 coreutils 语义):
|
||||||
|
* 124 超时(本工具发出的信号不是 KILL,或 -k 未到期)
|
||||||
|
* 137 超时且本工具最终发了 SIGKILL(-s KILL,或 -k 到期)
|
||||||
|
* 128+N 命令自己死于信号 N(未超时;不提供 --preserve-status)
|
||||||
|
* 125 timeout 自身失败(参数错、signalfd/setitimer/fork 失败、
|
||||||
|
* 或超时信号完全发不出去——后者会先在 stderr 明确告警)
|
||||||
|
* 126 命令存在但无法执行(EACCES 等)
|
||||||
|
* 127 命令不存在(ENOENT)
|
||||||
|
* 其它 透传命令自身的退出码
|
||||||
|
*
|
||||||
|
* 进程模型(本工具最容易做错的部分):
|
||||||
|
* fork 后子进程 setpgid(0,0) 自成一个进程组,父进程也 setpgid(pid,pid)
|
||||||
|
* 关掉 fork/setpgid 之间的竞态。超时后 kill(-pgid, SIG),因此命令自己 fork
|
||||||
|
* 出来的孙进程(bluetoothctl 的形态、sh -c 'a & b & wait')一并收到信号。
|
||||||
|
* 直接子进程一定被 waitpid 回收(不留僵尸);孙进程在组内被信号终止后
|
||||||
|
* 由 init 收养并回收。
|
||||||
|
*
|
||||||
|
* 杀不动时绝不装作成功(18:44 事故的形态就是"进程没被杀掉、越积越多"):
|
||||||
|
* - 父进程 setpgid 失败(除 EACCES/ESRCH 这些正常情形)→ stderr 告警;
|
||||||
|
* - 发信号前用 getpgid() 核对"子进程确实自成一个组",不成立就告警;
|
||||||
|
* - kill(-pgid) 返回 ESRCH(组已不存在)不算错,退化为 kill(pid, sig)
|
||||||
|
* 只杀直接子进程;返回 EPERM 等 → 明确告警 + 兜底 kill(pid);
|
||||||
|
* - 连兜底 kill 都失败(非 ESRCH)→ 退出码 125,而不是谎报 124。
|
||||||
|
* `-DTIMEOUT_TEST_SETPGID_FAIL` 可注入 setpgid 失败,用于验证上述告警路径。
|
||||||
|
*
|
||||||
|
* 计时(为什么不读时钟、也不用 timerfd):
|
||||||
|
* zig/musl 的 time64 头在 4.4 内核上 ABI 错配,clock_gettime/time 会返回垃圾值
|
||||||
|
* (本仓 M11/M16/M40/M43 的既有结论)。实测还有第二个陷阱:musl 的
|
||||||
|
* timerfd_settime() 在这台机上是**静默失效**的——返回 0、不报错,但计时器
|
||||||
|
* 根本不启动(/proc 侧 gettime 为 0,poll 永远等不到)。用普通 timerfd 写法的
|
||||||
|
* 第一版因此表现为「timeout 2 sleep 30 等满 30 秒才返回、退出码 0」。
|
||||||
|
* 本工具改用 setitimer(ITIMER_REAL) + SIGALRM:musl 对 setitimer 做了
|
||||||
|
* 32 位 timeval 转换(实测 1s/200ms 都能按点触发),不经过坏掉的 timerfd 路径。
|
||||||
|
* 计时完全交给内核,进程只做两件事:等 SIGALRM(超时)与等 SIGCHLD(子进程退出),
|
||||||
|
* 两者都进同一个 signalfd,由一次 ppoll() 等待。没有轮询循环,没有忙等,
|
||||||
|
* 也没有任何"读时钟算差值"的代码。
|
||||||
|
*
|
||||||
|
* 已知限制:
|
||||||
|
* - 不跟踪 setsid()/双重 fork 逃出进程组的守护进程(coreutils 也不跟踪)。
|
||||||
|
* - 不提供 --preserve-status / -v;给出这些参数会明确报 125 而不是静默忽略。
|
||||||
|
* - 精度是"毫秒级够用":单核 1GHz 上真实误差来自调度延迟(负载高时数十毫秒),
|
||||||
|
* 本工具不谎称微秒级精度。
|
||||||
|
* - 若直接子进程在超时前自行退出,其留在组内的孙进程不会被本工具清理
|
||||||
|
* (与 coreutils 一致;想要组清理请让子进程活到超时)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/signalfd.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define EXIT_TIMEOUT 124
|
||||||
|
#define EXIT_FAILURE_125 125
|
||||||
|
#define EXIT_CANNOT_EXEC 126
|
||||||
|
#define EXIT_NOT_FOUND 127
|
||||||
|
|
||||||
|
#define NSEC_PER_SEC 1000000000ULL
|
||||||
|
|
||||||
|
/* 被转发给子进程组的信号:父进程收到后原样转给组,避免 Ctrl-C 只打断
|
||||||
|
* timeout 自己、把子进程留成孤儿。 */
|
||||||
|
static const int forward_sigs[] = {
|
||||||
|
SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2
|
||||||
|
};
|
||||||
|
|
||||||
|
static volatile sig_atomic_t g_child_pgid = 0;
|
||||||
|
static volatile sig_atomic_t g_pending_sig = 0;
|
||||||
|
|
||||||
|
static void forward_signal(int sig)
|
||||||
|
{
|
||||||
|
pid_t pgid = (pid_t)g_child_pgid;
|
||||||
|
|
||||||
|
g_pending_sig = sig;
|
||||||
|
if (pgid > 0) {
|
||||||
|
(void)kill(-pgid, sig);
|
||||||
|
g_pending_sig = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* setpgid 的可注入版本:-DTIMEOUT_TEST_SETPGID_FAIL 用来验证"建组失败"的告警与兜底。
|
||||||
|
* 生产构建不会定义它,走的就是 libc 的 setpgid。 */
|
||||||
|
#ifdef TIMEOUT_TEST_SETPGID_FAIL
|
||||||
|
static int fake_setpgid(pid_t pid, pid_t pgid)
|
||||||
|
{
|
||||||
|
(void)pid;
|
||||||
|
(void)pgid;
|
||||||
|
errno = EPERM;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#define call_setpgid fake_setpgid
|
||||||
|
#else
|
||||||
|
#define call_setpgid setpgid
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 发信号前核对"子进程确实自成一个进程组"。
|
||||||
|
* setpgid 的失败在 fork 竞态下很难直接观测(父进程侧对已 exec 的子进程返回 EACCES
|
||||||
|
* 是正常的),所以用 getpgid 在真正要杀的那一刻做权威判定。
|
||||||
|
* 返回 1=组存在,0=组不存在/已消失(已告警或无需告警)。 */
|
||||||
|
static int verify_process_group(pid_t pid)
|
||||||
|
{
|
||||||
|
pid_t g;
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
g = getpgid(pid);
|
||||||
|
if (g < 0) {
|
||||||
|
if (errno != ESRCH)
|
||||||
|
fprintf(stderr, "timeout: 警告: getpgid(%ld) 失败: %s\n",
|
||||||
|
(long)pid, strerror(errno));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (g != pid) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"timeout: 警告: 子进程 %ld 未建立独立进程组(实际 pgid=%ld)——"
|
||||||
|
"setpgid 未生效,超时信号将尽量只发给该进程本身,它 fork 的孙进程可能残留\n",
|
||||||
|
(long)pid, (long)g);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 给"子进程组"发信号,失败时告警并退化为只杀直接子进程。
|
||||||
|
* 返回值:0=组信号已发出;1=组不存在,退化为直接子进程信号;2=目标已消失(无残留);
|
||||||
|
* -1=组与进程都没能收到信号(应让 timeout 自身以 125 退出,不能谎报 124)。 */
|
||||||
|
static int kill_child(pid_t pid, int sig, const char *phase)
|
||||||
|
{
|
||||||
|
if (kill(-pid, sig) == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
int e = errno;
|
||||||
|
|
||||||
|
if (e == ESRCH) {
|
||||||
|
/* 组已不存在:子进程可能已退出,也可能 setpgid 没成功。不是错误。 */
|
||||||
|
if (kill(pid, sig) == 0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"timeout: 警告: %s: 进程组 %ld 不存在,已退化为只给直接子进程 %ld 发信号 %d\n",
|
||||||
|
phase, (long)pid, (long)pid, sig);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (errno == ESRCH)
|
||||||
|
return 2;
|
||||||
|
fprintf(stderr, "timeout: 警告: %s: 信号 %d 发不出去: %s(组与进程都不可达)\n",
|
||||||
|
phase, sig, strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "timeout: 警告: %s: kill(-%ld, %d) 失败: %s\n",
|
||||||
|
phase, (long)pid, sig, strerror(e));
|
||||||
|
if (kill(pid, sig) == 0)
|
||||||
|
return 1;
|
||||||
|
if (errno == ESRCH)
|
||||||
|
return 2;
|
||||||
|
fprintf(stderr, "timeout: 警告: %s: 连直接子进程 %ld 也杀不掉: %s\n",
|
||||||
|
phase, (long)pid, strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sig_name {
|
||||||
|
const char *name;
|
||||||
|
int num;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct sig_name sig_names[] = {
|
||||||
|
{ "HUP", SIGHUP }, { "INT", SIGINT }, { "QUIT", SIGQUIT },
|
||||||
|
{ "ILL", SIGILL }, { "TRAP", SIGTRAP }, { "ABRT", SIGABRT },
|
||||||
|
{ "BUS", SIGBUS }, { "FPE", SIGFPE }, { "KILL", SIGKILL },
|
||||||
|
{ "USR1", SIGUSR1 }, { "SEGV", SIGSEGV }, { "USR2", SIGUSR2 },
|
||||||
|
{ "PIPE", SIGPIPE }, { "ALRM", SIGALRM }, { "TERM", SIGTERM },
|
||||||
|
{ "CHLD", SIGCHLD }, { "CONT", SIGCONT }, { "STOP", SIGSTOP },
|
||||||
|
{ "TSTP", SIGTSTP }, { "TTIN", SIGTTIN }, { "TTOU", SIGTTOU },
|
||||||
|
{ "URG", SIGURG }, { "XCPU", SIGXCPU }, { "XFSZ", SIGXFSZ },
|
||||||
|
{ "VTALRM", SIGVTALRM }, { "PROF", SIGPROF }, { "WINCH", SIGWINCH },
|
||||||
|
{ "IO", SIGIO }, { "SYS", SIGSYS }, { "PWR", SIGPWR },
|
||||||
|
};
|
||||||
|
|
||||||
|
static void usage(FILE *fp)
|
||||||
|
{
|
||||||
|
fputs(
|
||||||
|
"用法: timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...\n"
|
||||||
|
"\n"
|
||||||
|
" DURATION 等待时长:纯数字为秒,可带小数,后缀 s/m/h\n"
|
||||||
|
" (2=2s、0.2=200ms、2.5s=2.5s、1.5m=90s、0.5h=1800s)\n"
|
||||||
|
" -s SIGNAL 超时后发给整个进程组的信号,默认 TERM(可写 SIGTERM/TERM/15)\n"
|
||||||
|
" -k DURATION 发出 -s 信号后再等这么久,仍未退出则发 KILL(两阶段终止)\n"
|
||||||
|
" -h, --help 显示本帮助\n"
|
||||||
|
"\n"
|
||||||
|
"退出码: 124=超时 137=超时且发了 KILL 128+N=命令死于信号 N\n"
|
||||||
|
" 125=timeout 自身出错 126=命令无法执行 127=命令不存在\n"
|
||||||
|
"\n"
|
||||||
|
"超时信号发给 COMMAND 所在的整个进程组,命令自己 fork 的孙进程也会被终止。\n",
|
||||||
|
fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解析信号:数字(1..64)、NAME、SIGNAME(大小写不敏感) */
|
||||||
|
static int parse_signal(const char *s)
|
||||||
|
{
|
||||||
|
const char *name = s;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if (s == NULL || *s == '\0')
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (isdigit((unsigned char)*s)) {
|
||||||
|
char *end = NULL;
|
||||||
|
long v = strtol(s, &end, 10);
|
||||||
|
if (end == s || *end != '\0' || v < 1 || v > 64)
|
||||||
|
return -1;
|
||||||
|
return (int)v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strncasecmp(name, "SIG", 3) == 0)
|
||||||
|
name += 3;
|
||||||
|
|
||||||
|
for (i = 0; i < sizeof(sig_names) / sizeof(sig_names[0]); i++) {
|
||||||
|
if (strcasecmp(name, sig_names[i].name) == 0)
|
||||||
|
return sig_names[i].num;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解析时长 → 纳秒。整数秒 + 最多 9 位小数 + s/m/h 后缀;0 视为非法。 */
|
||||||
|
static int parse_duration(const char *s, uint64_t *out_ns)
|
||||||
|
{
|
||||||
|
uint64_t sec = 0, nsec = 0, mult = 1;
|
||||||
|
size_t i = 0;
|
||||||
|
int int_digits = 0, frac_digits = 0;
|
||||||
|
|
||||||
|
if (s == NULL || *s == '\0')
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
while (isdigit((unsigned char)s[i])) {
|
||||||
|
if (sec > (UINT64_MAX - 9) / 10)
|
||||||
|
return -1;
|
||||||
|
sec = sec * 10 + (uint64_t)(s[i] - '0');
|
||||||
|
i++;
|
||||||
|
int_digits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s[i] == '.') {
|
||||||
|
i++;
|
||||||
|
while (isdigit((unsigned char)s[i])) {
|
||||||
|
if (frac_digits < 9) {
|
||||||
|
nsec = nsec * 10 + (uint64_t)(s[i] - '0');
|
||||||
|
frac_digits++;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
/* 小数位补足 9 位;"5." 这类也接受 */
|
||||||
|
while (frac_digits < 9) {
|
||||||
|
nsec *= 10;
|
||||||
|
frac_digits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int_digits == 0 && frac_digits == 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
switch (s[i]) {
|
||||||
|
case '\0': break;
|
||||||
|
case 's': case 'S': mult = 1; i++; break;
|
||||||
|
case 'm': case 'M': mult = 60; i++; break;
|
||||||
|
case 'h': case 'H': mult = 3600; i++; break;
|
||||||
|
default: return -1;
|
||||||
|
}
|
||||||
|
if (s[i] != '\0')
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* 后缀乘数是"整个时长"的,小数部分必须一起乘:
|
||||||
|
* 0.05m = 0.05 分钟 = 3 秒,1.5m = 90 秒,0.5h = 1800 秒。
|
||||||
|
* (只乘整数部分会让 0.05m 变成 50 毫秒——比要求更早杀掉,静默给错答案。) */
|
||||||
|
if (mult != 1) {
|
||||||
|
if (sec > UINT64_MAX / mult)
|
||||||
|
return -1;
|
||||||
|
sec *= mult;
|
||||||
|
if (nsec > UINT64_MAX / mult)
|
||||||
|
return -1;
|
||||||
|
nsec *= mult;
|
||||||
|
}
|
||||||
|
if (sec > (UINT64_MAX - nsec) / NSEC_PER_SEC)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
*out_ns = sec * NSEC_PER_SEC + nsec;
|
||||||
|
return (*out_ns == 0) ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void arm_timer(uint64_t ns)
|
||||||
|
{
|
||||||
|
struct itimerval it;
|
||||||
|
|
||||||
|
memset(&it, 0, sizeof(it));
|
||||||
|
it.it_value.tv_sec = (time_t)(ns / NSEC_PER_SEC);
|
||||||
|
it.it_value.tv_usec = (suseconds_t)((ns % NSEC_PER_SEC) / 1000);
|
||||||
|
/* 亚毫秒向上取整到 1µs:setitimer 的 0 表示"不启动",会把小超时变成永不超时 */
|
||||||
|
if (ns != 0 && it.it_value.tv_sec == 0 && it.it_value.tv_usec == 0)
|
||||||
|
it.it_value.tv_usec = 1;
|
||||||
|
/* 失败也无从补救:下面的 ppoll 会一直等,用户看得见卡住 */
|
||||||
|
(void)setitimer(ITIMER_REAL, &it, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disarm_timer(void)
|
||||||
|
{
|
||||||
|
struct itimerval it;
|
||||||
|
|
||||||
|
memset(&it, 0, sizeof(it));
|
||||||
|
(void)setitimer(ITIMER_REAL, &it, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int duration_arg(const char *opt, const char *val, uint64_t *ns)
|
||||||
|
{
|
||||||
|
if (parse_duration(val, ns) != 0) {
|
||||||
|
fprintf(stderr, "timeout: 无效时长 '%s'(%s):支持纯数字秒、小数与 s/m/h 后缀\n",
|
||||||
|
val, opt);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单元测试模式:把真正的 main 改名,文件末尾注入一个只测 parse_duration 的 main。
|
||||||
|
* 生产构建不定义 TIMEOUT_UNIT_TEST,二进制里没有这些测试代码。 */
|
||||||
|
#ifdef TIMEOUT_UNIT_TEST
|
||||||
|
#define main timeout_main
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int kill_signal = SIGTERM;
|
||||||
|
uint64_t kill_after_ns = 0;
|
||||||
|
int have_kill_after = 0;
|
||||||
|
int i = 1;
|
||||||
|
uint64_t timeout_ns = 0;
|
||||||
|
char **cmd;
|
||||||
|
pid_t pid;
|
||||||
|
int sfd = -1;
|
||||||
|
struct sigaction sa;
|
||||||
|
sigset_t wait_mask, orig_mask;
|
||||||
|
struct pollfd pfd;
|
||||||
|
int status = 0;
|
||||||
|
int timed_out = 0, killed_by_us = 0, reaped = 0, done = 0, signal_failed = 0;
|
||||||
|
|
||||||
|
while (i < argc) {
|
||||||
|
const char *a = argv[i];
|
||||||
|
|
||||||
|
if (strcmp(a, "--") == 0) {
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (a[0] != '-' || strcmp(a, "-") == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
|
||||||
|
usage(stdout);
|
||||||
|
return 0;
|
||||||
|
} else if (strcmp(a, "-s") == 0 || strcmp(a, "--signal") == 0) {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
fprintf(stderr, "timeout: %s 缺少信号参数\n", a);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
kill_signal = parse_signal(argv[++i]);
|
||||||
|
if (kill_signal < 0) {
|
||||||
|
fprintf(stderr, "timeout: 无效信号 '%s'\n", argv[i]);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
} else if (strncmp(a, "--signal=", 9) == 0) {
|
||||||
|
kill_signal = parse_signal(a + 9);
|
||||||
|
if (kill_signal < 0) {
|
||||||
|
fprintf(stderr, "timeout: 无效信号 '%s'\n", a + 9);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
} else if (strcmp(a, "-k") == 0 || strcmp(a, "--kill-after") == 0) {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
fprintf(stderr, "timeout: %s 缺少时长参数\n", a);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
if (duration_arg(a, argv[++i], &kill_after_ns) != 0)
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
have_kill_after = 1;
|
||||||
|
} else if (strncmp(a, "--kill-after=", 13) == 0) {
|
||||||
|
if (duration_arg("--kill-after", a + 13, &kill_after_ns) != 0)
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
have_kill_after = 1;
|
||||||
|
} else if (strcmp(a, "--preserve-status") == 0 ||
|
||||||
|
strcmp(a, "-v") == 0 || strcmp(a, "--verbose") == 0) {
|
||||||
|
fprintf(stderr, "timeout: 不支持 %s(本工具是 coreutils 的最小可用子集)\n", a);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "timeout: 未知选项 '%s'\n", a);
|
||||||
|
usage(stderr);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= argc) {
|
||||||
|
usage(stderr);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
if (parse_duration(argv[i], &timeout_ns) != 0) {
|
||||||
|
fprintf(stderr, "timeout: 无效时长 '%s':支持纯数字秒、小数与 s/m/h 后缀\n",
|
||||||
|
argv[i]);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
if (i >= argc) {
|
||||||
|
fprintf(stderr, "timeout: 缺少要执行的命令\n");
|
||||||
|
usage(stderr);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
cmd = &argv[i];
|
||||||
|
|
||||||
|
/* SIGCHLD(子进程退出)与 SIGALRM(超时)都屏蔽后交给同一个 signalfd;
|
||||||
|
* 这样一次 ppoll 就能同时等两件事,不需要轮询,也不需要 timerfd。 */
|
||||||
|
sigemptyset(&wait_mask);
|
||||||
|
sigaddset(&wait_mask, SIGCHLD);
|
||||||
|
sigaddset(&wait_mask, SIGALRM);
|
||||||
|
if (sigprocmask(SIG_BLOCK, &wait_mask, &orig_mask) != 0) {
|
||||||
|
perror("timeout: sigprocmask");
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
sfd = signalfd(-1, &wait_mask, SFD_CLOEXEC | SFD_NONBLOCK);
|
||||||
|
if (sfd < 0) {
|
||||||
|
perror("timeout: signalfd");
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_handler = forward_signal;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0; /* 不 SA_RESTART:要让 ppoll 返回 EINTR */
|
||||||
|
for (size_t k = 0; k < sizeof(forward_sigs) / sizeof(forward_sigs[0]); k++)
|
||||||
|
(void)sigaction(forward_sigs[k], &sa, NULL);
|
||||||
|
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
perror("timeout: fork");
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
/* 自成进程组:超时信号 kill(-pgid) 才能覆盖孙进程。
|
||||||
|
* 这里失败不直接报错(父进程会再做一次;发信号前还有 getpgid 核对),
|
||||||
|
* 因为子进程此时还没 exec,直接写 stderr 会和命令自身的输出混在一起。 */
|
||||||
|
(void)call_setpgid(0, 0);
|
||||||
|
for (size_t k = 0; k < sizeof(forward_sigs) / sizeof(forward_sigs[0]); k++)
|
||||||
|
(void)signal(forward_sigs[k], SIG_DFL);
|
||||||
|
(void)sigprocmask(SIG_SETMASK, &orig_mask, NULL);
|
||||||
|
|
||||||
|
execvp(cmd[0], cmd);
|
||||||
|
|
||||||
|
/* exec 失败:说清原因并用 coreutils 的 127/126 区分 */
|
||||||
|
{
|
||||||
|
int saved = errno;
|
||||||
|
char msg[512];
|
||||||
|
int n = snprintf(msg, sizeof(msg),
|
||||||
|
"timeout: 无法执行 '%s': %s\n", cmd[0], strerror(saved));
|
||||||
|
if (n > 0)
|
||||||
|
(void)write(STDERR_FILENO, msg, (size_t)n);
|
||||||
|
_exit(saved == ENOENT ? EXIT_NOT_FOUND : EXIT_CANNOT_EXEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_child_pgid = pid;
|
||||||
|
/* fork 与子进程 setpgid 之间的竞态由父进程这一侧兜住(任一侧成功即可)。
|
||||||
|
* EACCES = 子进程已经 exec(它自己那次 setpgid 已经跑过),ESRCH = 已退出,
|
||||||
|
* 这两种是正常竞态,不当错误;其它 errno 说明建组可能失败,要告警。 */
|
||||||
|
if (call_setpgid(pid, pid) != 0) {
|
||||||
|
int e = errno;
|
||||||
|
if (e != EACCES && e != ESRCH)
|
||||||
|
fprintf(stderr,
|
||||||
|
"timeout: 警告: setpgid(%ld) 失败: %s —— 超时信号可能只能发给该进程本身\n",
|
||||||
|
(long)pid, strerror(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 若在 fork 与 g_child_pgid 赋值之间收到过转发信号,这里补发一次 */
|
||||||
|
if (g_pending_sig != 0) {
|
||||||
|
int sig = (int)g_pending_sig;
|
||||||
|
if (kill_child(pid, sig, "转发信号") < 0)
|
||||||
|
signal_failed = 1;
|
||||||
|
g_pending_sig = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
arm_timer(timeout_ns);
|
||||||
|
|
||||||
|
pfd.fd = sfd;
|
||||||
|
pfd.events = POLLIN;
|
||||||
|
pfd.revents = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int r = ppoll(&pfd, 1, NULL, NULL);
|
||||||
|
|
||||||
|
if (r < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue; /* 转发信号的处理函数已完成转发 */
|
||||||
|
perror("timeout: ppoll");
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
if (!(pfd.revents & POLLIN))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
struct signalfd_siginfo si;
|
||||||
|
ssize_t got = read(sfd, &si, sizeof(si));
|
||||||
|
pid_t w;
|
||||||
|
|
||||||
|
if (got != (ssize_t)sizeof(si))
|
||||||
|
break; /* EAGAIN:这一轮通知读完了 */
|
||||||
|
|
||||||
|
if (si.ssi_signo == SIGALRM) {
|
||||||
|
if (!timed_out) {
|
||||||
|
timed_out = 1;
|
||||||
|
/* 真正要杀之前核对一次进程组:setpgid 若没生效,这里必须告警,
|
||||||
|
* 不能让用户看到 124 却以为孙进程都被杀了。 */
|
||||||
|
(void)verify_process_group(pid);
|
||||||
|
/* 先发温和信号(默认 TERM),发给整个进程组 */
|
||||||
|
if (kill_child(pid, kill_signal, "超时") < 0)
|
||||||
|
signal_failed = 1;
|
||||||
|
if (kill_signal == SIGKILL) {
|
||||||
|
killed_by_us = 1;
|
||||||
|
disarm_timer();
|
||||||
|
} else if (have_kill_after) {
|
||||||
|
arm_timer(kill_after_ns); /* 第二阶段:-k 到期 */
|
||||||
|
} else {
|
||||||
|
disarm_timer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* -k 到期:KILL 整个组,仍继续 waitpid 回收直接子进程 */
|
||||||
|
if (kill_child(pid, SIGKILL, "-k 到期") < 0)
|
||||||
|
signal_failed = 1;
|
||||||
|
killed_by_us = 1;
|
||||||
|
disarm_timer();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (si.ssi_signo != SIGCHLD)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
do {
|
||||||
|
w = waitpid(pid, &status, WNOHANG);
|
||||||
|
} while (w < 0 && errno == EINTR);
|
||||||
|
|
||||||
|
if (w == pid) {
|
||||||
|
reaped = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (w < 0 && errno == ECHILD) {
|
||||||
|
/* 子进程被别人回收(极不寻常),状态不可得 */
|
||||||
|
reaped = 0;
|
||||||
|
done = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/* w == 0:还有其它子进程状态变化(本工具没有),继续等 */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reaped || done)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reaped)
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
|
||||||
|
if (timed_out) {
|
||||||
|
/* 信号完全发不出去时不能谎报 124:那是"以为杀了、其实没杀"的形态,
|
||||||
|
* 正是 2026-11-11 事故的样子。已在 stderr 告警,退出码用 125。 */
|
||||||
|
if (signal_failed)
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
/* 超时:最终发了 KILL 才报 137,否则 124(coreutils 语义) */
|
||||||
|
return killed_by_us ? 128 + SIGKILL : EXIT_TIMEOUT;
|
||||||
|
}
|
||||||
|
if (WIFEXITED(status))
|
||||||
|
return WEXITSTATUS(status);
|
||||||
|
if (WIFSIGNALED(status))
|
||||||
|
return 128 + WTERMSIG(status);
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef TIMEOUT_UNIT_TEST
|
||||||
|
#undef main
|
||||||
|
|
||||||
|
/* 时长解析的单元测试。在宿主上用 `cc -DTIMEOUT_UNIT_TEST` 编译后直接跑,
|
||||||
|
* 不需要 sleep、不依赖目标设备——所以 test_build_devkit.sh 能把它当回归用例。
|
||||||
|
* 覆盖重点:小数 + s/m/h 后缀叠加(0.05m=3s、1.5m=90s、0.5h=1800s),
|
||||||
|
* 这正是 r1 评审抓到的 P2(只乘了整数部分,0.05m 被当成 50ms)。 */
|
||||||
|
struct dur_case {
|
||||||
|
const char *text;
|
||||||
|
uint64_t want_ns;
|
||||||
|
int want_ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const struct dur_case dur_cases[] = {
|
||||||
|
{ "2", 2ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "2s", 2ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "2S", 2ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "10", 10ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ ".5", 500000000ULL, 1 },
|
||||||
|
{ "0.2", 200000000ULL, 1 },
|
||||||
|
{ "2.5s", 2500000000ULL, 1 },
|
||||||
|
{ "1.5", 1500000000ULL, 1 },
|
||||||
|
{ "0.000000001", 1ULL, 1 },
|
||||||
|
{ "1m", 60ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "1.5m", 90ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "0.01m", 600000000ULL, 1 },
|
||||||
|
{ "0.05m", 3000000000ULL, 1 },
|
||||||
|
{ "0.5M", 30ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "0.5h", 1800ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "1h", 3600ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "0.25h", 900ULL * NSEC_PER_SEC, 1 },
|
||||||
|
{ "0", 0, 0 },
|
||||||
|
{ "", 0, 0 },
|
||||||
|
{ "1d", 0, 0 },
|
||||||
|
{ "abc", 0, 0 },
|
||||||
|
{ "1x", 0, 0 },
|
||||||
|
{ "1.2.3", 0, 0 },
|
||||||
|
{ "-1", 0, 0 },
|
||||||
|
{ " 1", 0, 0 },
|
||||||
|
{ "18446744074", 0, 0 }, /* sec*1e9 溢出 */
|
||||||
|
{ "99999999999999999999h", 0, 0 }, /* 整数解析溢出 */
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
int bad = 0;
|
||||||
|
const size_t n = sizeof(dur_cases) / sizeof(dur_cases[0]);
|
||||||
|
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
uint64_t got = 0;
|
||||||
|
int ok = (parse_duration(dur_cases[i].text, &got) == 0);
|
||||||
|
|
||||||
|
if (ok != dur_cases[i].want_ok ||
|
||||||
|
(ok && got != dur_cases[i].want_ns)) {
|
||||||
|
printf("FAIL DURATION '%s': rc=%d ns=%llu, want rc=%d ns=%llu\n",
|
||||||
|
dur_cases[i].text, ok, (unsigned long long)got,
|
||||||
|
dur_cases[i].want_ok, (unsigned long long)dur_cases[i].want_ns);
|
||||||
|
bad++;
|
||||||
|
} else {
|
||||||
|
printf("ok DURATION '%-24s' -> %s%llu ns\n",
|
||||||
|
dur_cases[i].text, ok ? "" : "INVALID ",
|
||||||
|
ok ? (unsigned long long)got : 0ULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("DURATION: %zu cases, %d failed\n", n, bad);
|
||||||
|
return bad ? 1 : 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
# r2 P2-1 —— licenses/README 被删后 --with-sources 必须恢复,
|
# r2 P2-1 —— licenses/README 被删后 --with-sources 必须恢复,
|
||||||
# --verify-only 必须拒绝缺许可证的交付
|
# --verify-only 必须拒绝缺许可证的交付
|
||||||
# MANIFEST 内容漂移检测、src/ 缺文件与内容漂移自愈
|
# MANIFEST 内容漂移检测、src/ 缺文件与内容漂移自愈
|
||||||
|
# M53 —— 本地源码路径:timeout 产物存在/静态/MIPS32r2/零 NEEDED、
|
||||||
|
# MANIFEST 有 Apache-2.0 行、license 原文随附、
|
||||||
|
# 源码纪律(独立实现、不调 clock_gettime、不用 timerfd、setpgid+kill(-pgid))
|
||||||
|
# M53 r2 —— 时长解析真跑回归(宿主编译 parser,覆盖 0.01m/0.05m/1.5m/0.5h)
|
||||||
|
# 与信号失败告警路径(注入 setpgid 失败,必须告警且兜底 kill(pid))
|
||||||
#
|
#
|
||||||
# 前置:MEOW_DEVKIT_WORK 里已有一次构建(脚本会先跑一次增量构建,命中则秒回;
|
# 前置:MEOW_DEVKIT_WORK 里已有一次构建(脚本会先跑一次增量构建,命中则秒回;
|
||||||
# 缓存缺失时会联网下载/编译,第一次运行约 1 分钟)。
|
# 缓存缺失时会联网下载/编译,第一次运行约 1 分钟)。
|
||||||
@@ -60,7 +65,7 @@ rm -rf "$OUT/src"
|
|||||||
run "普通重跑(应命中 stamp 且不建 src/)" bash "$SCRIPT" || fail "T1a 普通重跑"
|
run "普通重跑(应命中 stamp 且不建 src/)" bash "$SCRIPT" || fail "T1a 普通重跑"
|
||||||
[ ! -d "$OUT/src" ] && pass "T1a 普通重跑不创建 src/(符合预期)" || fail "T1a 普通重跑意外创建了 src/"
|
[ ! -d "$OUT/src" ] && pass "T1a 普通重跑不创建 src/(符合预期)" || fail "T1a 普通重跑意外创建了 src/"
|
||||||
run "--with-sources(stamp 命中路径)" bash "$SCRIPT" --with-sources || fail "T1b --with-sources 退出码非 0"
|
run "--with-sources(stamp 命中路径)" bash "$SCRIPT" --with-sources || fail "T1b --with-sources 退出码非 0"
|
||||||
for f in ncurses-6.5.tar.gz procps-ng-4.0.4.tar.xz lsof-4.99.4.tar.xz build_devkit.sh DEVKIT.md; do
|
for f in ncurses-6.5.tar.gz procps-ng-4.0.4.tar.xz lsof-4.99.4.tar.xz build_devkit.sh DEVKIT.md timeout.c; do
|
||||||
[ -s "$OUT/src/$f" ] && pass "T1b src/$f 存在且非空" || fail "T1b 缺 src/$f(P1 未修好)"
|
[ -s "$OUT/src/$f" ] && pass "T1b src/$f 存在且非空" || fail "T1b 缺 src/$f(P1 未修好)"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -94,7 +99,7 @@ cp "$TMP/MANIFEST.txt.bak" "$OUT/MANIFEST.txt"
|
|||||||
run "--verify-only(恢复后)" bash "$SCRIPT" --verify-only && pass "T5b 恢复后重新通过" || fail "T5b 恢复后仍失败"
|
run "--verify-only(恢复后)" bash "$SCRIPT" --verify-only && pass "T5b 恢复后重新通过" || fail "T5b 恢复后仍失败"
|
||||||
|
|
||||||
echo "== 6) 交付完整性:删 licenses/ 后 --with-sources 必须恢复"
|
echo "== 6) 交付完整性:删 licenses/ 后 --with-sources 必须恢复"
|
||||||
LICENSES="procps-ng-4.0.4-COPYING procps-ng-4.0.4-COPYING.LIB ncurses-6.5-COPYING lsof-4.99.4-COPYING musl-COPYRIGHT"
|
LICENSES="procps-ng-4.0.4-COPYING procps-ng-4.0.4-COPYING.LIB ncurses-6.5-COPYING lsof-4.99.4-COPYING musl-COPYRIGHT meowos-timeout-LICENSE"
|
||||||
rm -rf "$OUT/licenses"
|
rm -rf "$OUT/licenses"
|
||||||
run "--with-sources 补 licenses" bash "$SCRIPT" --with-sources || fail "T6 --with-sources 退出码非 0"
|
run "--with-sources 补 licenses" bash "$SCRIPT" --with-sources || fail "T6 --with-sources 退出码非 0"
|
||||||
for f in $LICENSES; do
|
for f in $LICENSES; do
|
||||||
@@ -116,6 +121,99 @@ rm -f "$OUT/README.txt"
|
|||||||
run "--with-sources 补 README" bash "$SCRIPT" --with-sources || fail "T8 --with-sources 退出码非 0"
|
run "--with-sources 补 README" bash "$SCRIPT" --with-sources || fail "T8 --with-sources 退出码非 0"
|
||||||
[ -s "$OUT/README.txt" ] && pass "T8 README.txt 已恢复" || fail "T8 README.txt 未恢复"
|
[ -s "$OUT/README.txt" ] && pass "T8 README.txt 已恢复" || fail "T8 README.txt 未恢复"
|
||||||
|
|
||||||
|
echo "== 9) timeout(本地源码路径):产物存在 + 静态/架构/零 NEEDED"
|
||||||
|
if [ -f "$OUT/bin/timeout" ]; then
|
||||||
|
pass "T9a bin/timeout 存在"
|
||||||
|
DESC="$(file -b "$OUT/bin/timeout")"
|
||||||
|
case "$DESC" in *"statically linked"*) pass "T9b 静态链接" ;; *) fail "T9b 非静态: $DESC" ;; esac
|
||||||
|
case "$DESC" in *"MIPS32 rel2"*) pass "T9c MIPS32 rel2" ;; *) fail "T9c 架构不符: $DESC" ;; esac
|
||||||
|
[ "$(readelf -d "$OUT/bin/timeout" 2>/dev/null | grep -c NEEDED)" = 0 ] \
|
||||||
|
&& pass "T9d readelf -d 零 NEEDED" || fail "T9d 有 NEEDED 依赖"
|
||||||
|
readelf -h "$OUT/bin/timeout" | grep -q mips32r2 \
|
||||||
|
&& pass "T9e ELF Flags 含 mips32r2" || fail "T9e ELF Flags 无 mips32r2"
|
||||||
|
else
|
||||||
|
fail "T9a 缺 $OUT/bin/timeout(本地源码没编进来)"
|
||||||
|
fi
|
||||||
|
ROW="$(awk '$1=="timeout" && $(NF-1) ~ /^[0-9]+$/ && length($NF)==64' "$OUT/MANIFEST.txt")"
|
||||||
|
if [ -n "$ROW" ]; then
|
||||||
|
pass "T9f MANIFEST 有 timeout 表格行(sha256 + 体积)"
|
||||||
|
case "$ROW" in *Apache-2.0*) pass "T9g MANIFEST 标注 Apache-2.0" ;; *) fail "T9g MANIFEST 未标 Apache-2.0: $ROW" ;; esac
|
||||||
|
else
|
||||||
|
fail "T9f MANIFEST 缺 timeout 表格行"
|
||||||
|
fi
|
||||||
|
head -2 "$OUT/licenses/meowos-timeout-LICENSE" 2>/dev/null | grep -q 'Apache License' \
|
||||||
|
&& pass "T9h licenses/meowos-timeout-LICENSE 是 Apache-2.0 原文" \
|
||||||
|
|| fail "T9h 缺 Apache-2.0 许可证原文"
|
||||||
|
|
||||||
|
echo "== 10) timeout 源码纪律:本仓独立实现,且不碰坏掉的 clock_gettime"
|
||||||
|
SRC_T="$ROOT/scripts/build/devkit-src/timeout.c"
|
||||||
|
grep -q 'SPDX-License-Identifier: Apache-2.0' "$SRC_T" \
|
||||||
|
&& pass "T10a 源码带 Apache-2.0 SPDX 头" || fail "T10a 源码缺 Apache-2.0 SPDX 头"
|
||||||
|
grep -q '独立实现,未拷贝' "$SRC_T" \
|
||||||
|
&& pass "T10b 源码声明独立实现、未拷贝 GPL 代码" || fail "T10b 缺独立实现声明"
|
||||||
|
if grep -nE 'clock_gettime|gettimeofday' "$SRC_T" | grep -qvE '^[0-9]+:[[:space:]]*(/\*|\*|//)'; then
|
||||||
|
fail "T10c 源码调用了 clock_gettime/gettimeofday(4.4 内核上 time64 ABI 错配)"
|
||||||
|
else
|
||||||
|
pass "T10c 源码不调用 clock_gettime/gettimeofday(只在注释里提及)"
|
||||||
|
fi
|
||||||
|
if grep -nE 'timerfd_settime|timerfd_create' "$SRC_T" | grep -qvE '^[0-9]+:[[:space:]]*(/\*|\*|//)'; then
|
||||||
|
fail "T10e 源码用了 timerfd(musl 的 timerfd_settime 在 4.4 上静默失效,必须用 setitimer)"
|
||||||
|
else
|
||||||
|
pass "T10e 源码不用 timerfd(超时走 setitimer+SIGALRM)"
|
||||||
|
fi
|
||||||
|
grep -q 'setitimer(ITIMER_REAL' "$SRC_T" && grep -q 'SIGALRM' "$SRC_T" \
|
||||||
|
&& pass "T10f 超时机制是 setitimer(ITIMER_REAL)+SIGALRM" \
|
||||||
|
|| fail "T10f 缺 setitimer/SIGALRM 超时机制"
|
||||||
|
grep -q 'setpgid' "$SRC_T" && grep -q 'kill(-' "$SRC_T" \
|
||||||
|
&& pass "T10d 进程组模型到位(setpgid + kill(-pgid))" \
|
||||||
|
|| fail "T10d 缺进程组模型(孙进程会漏杀)"
|
||||||
|
|
||||||
|
echo "== 11) 时长解析回归(宿主编译,真跑 parser,不做 sleep)"
|
||||||
|
if command -v cc >/dev/null 2>&1; then HOST_CC="cc"
|
||||||
|
elif command -v gcc >/dev/null 2>&1; then HOST_CC="gcc"
|
||||||
|
elif command -v clang >/dev/null 2>&1; then HOST_CC="clang"
|
||||||
|
elif command -v zig >/dev/null 2>&1; then HOST_CC="zig cc"
|
||||||
|
else HOST_CC=""; fi
|
||||||
|
|
||||||
|
if [ -z "$HOST_CC" ]; then
|
||||||
|
echo " SKIP: 没有宿主 C 编译器(cc/gcc/clang/zig),时长解析回归未运行"
|
||||||
|
elif $HOST_CC -O2 -Wno-unused-function -DTIMEOUT_UNIT_TEST -o "$TMP/dur-test" "$SRC_T" >"$TMP/hostcc.log" 2>&1; then
|
||||||
|
if "$TMP/dur-test" > "$TMP/dur.log" 2>&1; then
|
||||||
|
pass "T11a 时长解析 $(grep -c '^ok' "$TMP/dur.log") 例全过(含 0.01m/0.05m/1.5m/0.5h)"
|
||||||
|
else
|
||||||
|
fail "T11a 时长解析有用例失败"
|
||||||
|
grep '^FAIL' "$TMP/dur.log" | sed 's/^/ | /'
|
||||||
|
fi
|
||||||
|
# 单独点名 r1 评审抓到的三条(防止将来又被改回"只乘整数部分")
|
||||||
|
for c in "0.05m" "1.5m" "0.5h"; do
|
||||||
|
grep -q "DURATION '$c" "$TMP/dur.log" && ! grep -q "^FAIL.*'$c" "$TMP/dur.log" \
|
||||||
|
&& pass "T11b '$c' 用例存在且通过" || fail "T11b '$c' 用例缺失或失败"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
fail "T11 宿主编译失败($HOST_CC)"; sed 's/^/ | /' "$TMP/hostcc.log" | tail -5
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== 12) 信号失败告警路径(注入 setpgid 失败,验证不静默)"
|
||||||
|
if [ -n "$HOST_CC" ] && \
|
||||||
|
$HOST_CC -O2 -DTIMEOUT_TEST_SETPGID_FAIL -o "$TMP/timeout-pgfail" "$SRC_T" >"$TMP/hostcc2.log" 2>&1; then
|
||||||
|
"$TMP/timeout-pgfail" 1 sleep 30 > "$TMP/pgfail.out" 2>&1
|
||||||
|
PGRC=$?
|
||||||
|
[ "$PGRC" = 124 ] && pass "T12a 建组失败时仍杀掉直接子进程(rc=124)" \
|
||||||
|
|| fail "T12a rc=$PGRC(期望 124)"
|
||||||
|
grep -q '未建立独立进程组' "$TMP/pgfail.out" \
|
||||||
|
&& pass "T12b 建组失败有明确告警(不静默)" || fail "T12b 缺'未建立独立进程组'告警"
|
||||||
|
grep -q '退化为只给直接子进程' "$TMP/pgfail.out" \
|
||||||
|
&& pass "T12c 组信号失败后走 kill(pid) 兜底" || fail "T12c 缺兜底路径告警"
|
||||||
|
# 直接子进程必须真死:注入场景下它没有进程组保护,只能靠 kill(pid) 兜底
|
||||||
|
sleep 1
|
||||||
|
ps -A -o args= 2>/dev/null | grep -q '^sleep 30$' \
|
||||||
|
&& { fail "T12d 注入场景下 sleep 30 仍存活(兜底没生效)"
|
||||||
|
command -v pkill >/dev/null 2>&1 && pkill -f '^sleep 30$' 2>/dev/null; } \
|
||||||
|
|| pass "T12d 注入场景下没有残留 sleep 30"
|
||||||
|
else
|
||||||
|
echo " SKIP: 没有宿主 C 编译器,T12 未运行"
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
printf '== 结果: %d 通过, %d 失败\n' "$PASS" "$FAILN"
|
printf '== 结果: %d 通过, %d 失败\n' "$PASS" "$FAILN"
|
||||||
[ "$FAILN" = 0 ]
|
[ "$FAILN" = 0 ]
|
||||||
Reference in new issue
Block a user