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:
1 parent
24f1d8a682
commit
c93814a78b
4 files changed
+314
-27
No files matched your search
+59
-15
@@ -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 |
|
| `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` 网络视图 |
|
||||||
| `timeout` | 本仓 `devkit-src/timeout.c` | **Apache-2.0**(自有独立实现,非 busybox/GPL 代码) | 47 708 | 给任何外部命令加"最多等 N 秒、然后杀掉整个进程组";busybox 1.31 无此 applet |
|
| `timeout` | 本仓 `devkit-src/timeout.c` | **Apache-2.0**(自有独立实现,非 busybox/GPL 代码) | 49 676 | 给任何外部命令加"最多等 N 秒、然后杀掉整个进程组";busybox 1.31 无此 applet |
|
||||||
| | | **合计** | **2 189 472 ≈ 2.1 MB** | |
|
| | | **合计** | **2 191 440 ≈ 2.1 MB** | |
|
||||||
|
|
||||||
`timeout` 走的是**本地源码编译路径**(`scripts/build/devkit-src/*.c` → `build_local`),
|
`timeout` 走的是**本地源码编译路径**(`scripts/build/devkit-src/*.c` → `build_local`),
|
||||||
与上游包(procps/lsof/ncurses)的「下载 + sha256 + configure/make」流程完全隔离:
|
与上游包(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 -s KILL 2 sleep 30 # rc=137
|
||||||
timeout -k 1 5 sh -c 'sleep 100 & sleep 100 & wait' # TERM 后 1s 仍在 → KILL,孙进程一起死
|
timeout -k 1 5 sh -c 'sleep 100 & sleep 100 & wait' # TERM 后 1s 仍在 → KILL,孙进程一起死
|
||||||
timeout 0.2 sleep 5 # 支持小数秒(~200ms 返回)
|
timeout 0.2 sleep 5 # 支持小数秒(~200ms 返回)
|
||||||
|
timeout 0.5m sleep 60 # 小数 + 后缀是叠加的:0.5m = 30 秒
|
||||||
timeout 8 bluetoothctl connect XX:XX:XX:XX:XX:XX # 挂死的外部命令的默认包装
|
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)`),
|
- 超时信号发给 COMMAND 的**整个进程组**(`setpgid(0,0)` + `kill(-pgid, SIG)`),
|
||||||
命令自己 fork 的孙进程一并终止;直接子进程一定被 `waitpid` 回收,不留僵尸。
|
命令自己 fork 的孙进程一并终止;直接子进程一定被 `waitpid` 回收,不留僵尸。
|
||||||
- 两阶段:先 `-s`(默认 TERM),`-k` 到期再 KILL。不是忙等,计时全在内核。
|
- 两阶段:先 `-s`(默认 TERM),`-k` 到期再 KILL。不是忙等,计时全在内核。
|
||||||
|
- **杀不动时不装作成功**(18:44 事故的形态就是"进程没被杀掉、越积越多"):
|
||||||
|
发信号前用 `getpgid()` 核对进程组;`kill(-pgid)` 返回 `ESRCH` 视为"组已不存在",
|
||||||
|
退化为 `kill(pid, sig)` 只杀直接子进程;`EPERM` 等真错误会打一行明确告警;
|
||||||
|
连兜底 kill 都失败时退出码是 **125**(不是 124)——不谎报"已超时处理"。
|
||||||
- 退出码:`124` 超时;`137` 超时且最终发了 KILL;`128+N` 命令自己死于信号 N;
|
- 退出码:`124` 超时;`137` 超时且最终发了 KILL;`128+N` 命令自己死于信号 N;
|
||||||
`125` timeout 自身参数/系统调用错;`126` 命令无法执行;`127` 命令不存在。
|
`125` timeout 自身参数/系统调用错或信号完全发不出去;`126` 命令无法执行;`127` 命令不存在。
|
||||||
- 不支持 `--preserve-status` / `-v` / `d` 后缀,给了会明确报 125(不静默忽略)。
|
- 不支持 `--preserve-status` / `-v` / `d` 后缀,给了会明确报 125(不静默忽略)。
|
||||||
- 不跟踪 `setsid()` 逃出进程组的守护进程(coreutils 也不跟踪)。
|
- 不跟踪 `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
|
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 未设置)`。
|
||||||
@@ -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/devkit/bin/timeout`(主位置,和 slabtop 那批一起)
|
||||||
与 `/usr/data/mnt/sd_0/MeowOS/opt/bin/timeout`(launcher 的 PATH 会 prepend 它)。
|
与 `/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` 精确计数,
|
原始输出(`T=.../devkit/bin/timeout`,`cnt()` 读 `/proc/<pid>/comm` 精确计数,
|
||||||
不会像 `grep` 那样把命令行文本算进去):
|
不会像 `grep` 那样把命令行文本算进去):
|
||||||
|
|
||||||
```
|
```
|
||||||
== [1] timeout 2 sleep 30
|
== [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
|
== [2] timeout -s KILL 2 sleep 30
|
||||||
rc=137 elapsed=2.0s
|
rc=137 elapsed=2.01s
|
||||||
== [3] timeout 2 sleep 1
|
== [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"
|
== [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
|
rc=124 elapsed=5.01s after4: bt=0 sleep=0 sleep100=0
|
||||||
== [prec] timeout 0.2 sleep 5
|
== [5] 保证会挂住的命令:timeout 3 sh -c "sleep 30"
|
||||||
rc=124 elapsed=0.2s
|
rc=124 elapsed=3.01s
|
||||||
== [6] timeout(无参数)
|
== [6] 小数 + 后缀(r2 修复的重点)
|
||||||
用法: timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...
|
0.01m (want ~0.6s): rc=124 elapsed=0.61s
|
||||||
noargs_rc=125
|
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` 随进程组一起被杀,
|
第 4 条是重点:`sh` 自己 fork 的两个 `sleep 100` 随进程组一起被杀,
|
||||||
`/proc` 里 `comm=sleep` 的进程数回到 **0**(不是"看起来没了",是逐 pid 核对)。
|
`/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)
|
### PATH 实测(非登录 shell vs 绝对路径 vs 登录 shell)
|
||||||
|
|
||||||
@@ -467,7 +496,7 @@ command -v bash
|
|||||||
|
|
||||||
结论:**ssh 默认非登录 shell,PATH 不含 opt/bin;请用绝对路径或 `sh -lc`**(不是 `bash -lc`)。
|
结论:**ssh 默认非登录 shell,PATH 不含 opt/bin;请用绝对路径或 `sh -lc`**(不是 `bash -lc`)。
|
||||||
|
|
||||||
### 真实场景:`timeout 8 bluetoothctl connect`
|
### 真实场景:`timeout 8 bluetoothctl connect`(r1 记录,保留作对照)
|
||||||
|
|
||||||
```
|
```
|
||||||
before: bt=0 sleep=0
|
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),
|
耳机当时是关机状态,`bluetoothctl` 5.54 在尝试后自己退出了(0.3s,rc=0),
|
||||||
所以这条没跑到 8s 上限——但它证明了**调用方式可用且不留残留**;
|
所以这条**没跑到 8s 上限**。评审指出这没验证到关键点——已改用第 5 条
|
||||||
"到点必杀"的能力由上面第 1–4 条的 `sleep` 用例证明(同一套 kill 路径)。
|
`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. 其它已知限制
|
||||||
|
|
||||||
|
|||||||
@@ -560,10 +560,13 @@ timeout 用法(本仓 Apache-2.0 独立实现,非 busybox 代码):
|
|||||||
timeout -s KILL 2 sleep 30 # 退出码 137
|
timeout -s KILL 2 sleep 30 # 退出码 137
|
||||||
timeout -k 1 5 bluetoothctl connect XX:XX... # TERM 后 1 秒仍在 → KILL
|
timeout -k 1 5 bluetoothctl connect XX:XX... # TERM 后 1 秒仍在 → KILL
|
||||||
timeout 0.2 sleep 5 # 支持小数秒(约 200ms 返回)
|
timeout 0.2 sleep 5 # 支持小数秒(约 200ms 返回)
|
||||||
|
timeout 0.5m sleep 60 # 小数+后缀叠加:0.5m = 30 秒(1.5m = 90 秒)
|
||||||
超时信号发给 COMMAND 的**整个进程组**:命令自己 fork 的孙进程一起死
|
超时信号发给 COMMAND 的**整个进程组**:命令自己 fork 的孙进程一起死
|
||||||
(`timeout 5 sh -c 'a & b & wait'` 不会留下 a/b)。退出码:
|
(`timeout 5 sh -c 'a & b & wait'` 不会留下 a/b)。退出码:
|
||||||
124=超时 137=超时且最终发了 KILL 128+N=命令死于信号 N
|
124=超时 137=超时且最终发了 KILL 128+N=命令死于信号 N
|
||||||
125=timeout 自身参数/系统调用错 126=命令无法执行 127=命令不存在
|
125=timeout 自身参数/系统调用错 126=命令无法执行 127=命令不存在
|
||||||
|
杀不动时不装作成功:进程组不可用会告警并退化为只杀直接子进程;
|
||||||
|
连兜底 kill 都失败时退出码 125(不是 124),不会谎报"已处理"。
|
||||||
不支持 --preserve-status / -v / d(天)后缀,给了会明确报 125(不静默忽略)。
|
不支持 --preserve-status / -v / d(天)后缀,给了会明确报 125(不静默忽略)。
|
||||||
不跟踪 setsid() 逃出进程组的守护进程(coreutils 也不跟踪)。
|
不跟踪 setsid() 逃出进程组的守护进程(coreutils 也不跟踪)。
|
||||||
精度是毫秒级够用:计时交给内核 setitimer(ITIMER_REAL)+SIGALRM,不读时钟,
|
精度是毫秒级够用:计时交给内核 setitimer(ITIMER_REAL)+SIGALRM,不读时钟,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*
|
*
|
||||||
* 独立实现,未拷贝任何 GPL 代码。
|
* 独立实现,未拷贝任何 GPL 代码。
|
||||||
* busybox 的 coreutils/timeout.c 与本仓 Apache-2.0 不兼容,因此本文件从零编写,
|
* busybox 的 coreutils/timeout.c 与本仓 Apache-2.0 不兼容,因此本文件从零编写,
|
||||||
* 只依据 POSIX/Linux 系统调用语义(fork/setpgid/kill/timerfd/ppoll/signalfd)。
|
* 只依据 POSIX/Linux 系统调用语义(fork/setpgid/kill/setitimer/signalfd/ppoll)。
|
||||||
*
|
*
|
||||||
* 为什么设备需要它(2026-11-11 18:44 整机进程耗尽事故):设备没有 timeout(1),
|
* 为什么设备需要它(2026-11-11 18:44 整机进程耗尽事故):设备没有 timeout(1),
|
||||||
* busybox 1.31.1 也无该 applet,一条挂在硬件上的外部命令(bluetoothctl 等)
|
* busybox 1.31.1 也无该 applet,一条挂在硬件上的外部命令(bluetoothctl 等)
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
* timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...
|
* timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...
|
||||||
* timeout --help
|
* timeout --help
|
||||||
*
|
*
|
||||||
* DURATION 纯数字=秒;支持小数(0.2、1.5m)与 s/m/h 后缀(大小写均可)。
|
* DURATION 纯数字=秒;支持小数(0.2、2.5s)与 s/m/h 后缀(大小写均可),
|
||||||
|
* 小数与后缀可叠加:1.5m = 90 秒、0.5h = 1800 秒、0.01m = 0.6 秒。
|
||||||
* 不接受 coreutils 的 d(天)后缀,也不接受 0(与本工具用途矛盾)。
|
* 不接受 coreutils 的 d(天)后缀,也不接受 0(与本工具用途矛盾)。
|
||||||
* -s SIGNAL 超时后发给整个进程组的信号,默认 TERM。可用 SIGTERM/TERM/15。
|
* -s SIGNAL 超时后发给整个进程组的信号,默认 TERM。可用 SIGTERM/TERM/15。
|
||||||
* -k DURATION 发完 -s 信号后再等 DURATION,仍未退出则 SIGKILL(两阶段)。
|
* -k DURATION 发完 -s 信号后再等 DURATION,仍未退出则 SIGKILL(两阶段)。
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
* 124 超时(本工具发出的信号不是 KILL,或 -k 未到期)
|
* 124 超时(本工具发出的信号不是 KILL,或 -k 未到期)
|
||||||
* 137 超时且本工具最终发了 SIGKILL(-s KILL,或 -k 到期)
|
* 137 超时且本工具最终发了 SIGKILL(-s KILL,或 -k 到期)
|
||||||
* 128+N 命令自己死于信号 N(未超时;不提供 --preserve-status)
|
* 128+N 命令自己死于信号 N(未超时;不提供 --preserve-status)
|
||||||
* 125 timeout 自身失败(参数错、timerfd/fork 失败、命令状态不可得)
|
* 125 timeout 自身失败(参数错、signalfd/setitimer/fork 失败、
|
||||||
|
* 或超时信号完全发不出去——后者会先在 stderr 明确告警)
|
||||||
* 126 命令存在但无法执行(EACCES 等)
|
* 126 命令存在但无法执行(EACCES 等)
|
||||||
* 127 命令不存在(ENOENT)
|
* 127 命令不存在(ENOENT)
|
||||||
* 其它 透传命令自身的退出码
|
* 其它 透传命令自身的退出码
|
||||||
@@ -38,6 +40,14 @@
|
|||||||
* 直接子进程一定被 waitpid 回收(不留僵尸);孙进程在组内被信号终止后
|
* 直接子进程一定被 waitpid 回收(不留僵尸);孙进程在组内被信号终止后
|
||||||
* 由 init 收养并回收。
|
* 由 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):
|
* 计时(为什么不读时钟、也不用 timerfd):
|
||||||
* zig/musl 的 time64 头在 4.4 内核上 ABI 错配,clock_gettime/time 会返回垃圾值
|
* zig/musl 的 time64 头在 4.4 内核上 ABI 错配,clock_gettime/time 会返回垃圾值
|
||||||
* (本仓 M11/M16/M40/M43 的既有结论)。实测还有第二个陷阱:musl 的
|
* (本仓 M11/M16/M40/M43 的既有结论)。实测还有第二个陷阱:musl 的
|
||||||
@@ -103,6 +113,85 @@ static void forward_signal(int sig)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
struct sig_name {
|
||||||
const char *name;
|
const char *name;
|
||||||
int num;
|
int num;
|
||||||
@@ -126,7 +215,8 @@ static void usage(FILE *fp)
|
|||||||
fputs(
|
fputs(
|
||||||
"用法: timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...\n"
|
"用法: timeout [-s SIGNAL] [-k DURATION] DURATION COMMAND [ARG]...\n"
|
||||||
"\n"
|
"\n"
|
||||||
" DURATION 等待时长:纯数字为秒,可带小数,后缀 s/m/h(如 2、0.2、1.5m)\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"
|
" -s SIGNAL 超时后发给整个进程组的信号,默认 TERM(可写 SIGTERM/TERM/15)\n"
|
||||||
" -k DURATION 发出 -s 信号后再等这么久,仍未退出则发 KILL(两阶段终止)\n"
|
" -k DURATION 发出 -s 信号后再等这么久,仍未退出则发 KILL(两阶段终止)\n"
|
||||||
" -h, --help 显示本帮助\n"
|
" -h, --help 显示本帮助\n"
|
||||||
@@ -212,10 +302,16 @@ static int parse_duration(const char *s, uint64_t *out_ns)
|
|||||||
if (s[i] != '\0')
|
if (s[i] != '\0')
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
|
/* 后缀乘数是"整个时长"的,小数部分必须一起乘:
|
||||||
|
* 0.05m = 0.05 分钟 = 3 秒,1.5m = 90 秒,0.5h = 1800 秒。
|
||||||
|
* (只乘整数部分会让 0.05m 变成 50 毫秒——比要求更早杀掉,静默给错答案。) */
|
||||||
if (mult != 1) {
|
if (mult != 1) {
|
||||||
if (sec > UINT64_MAX / mult)
|
if (sec > UINT64_MAX / mult)
|
||||||
return -1;
|
return -1;
|
||||||
sec *= mult;
|
sec *= mult;
|
||||||
|
if (nsec > UINT64_MAX / mult)
|
||||||
|
return -1;
|
||||||
|
nsec *= mult;
|
||||||
}
|
}
|
||||||
if (sec > (UINT64_MAX - nsec) / NSEC_PER_SEC)
|
if (sec > (UINT64_MAX - nsec) / NSEC_PER_SEC)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -256,6 +352,12 @@ static int duration_arg(const char *opt, const char *val, uint64_t *ns)
|
|||||||
return 0;
|
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 main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
int kill_signal = SIGTERM;
|
int kill_signal = SIGTERM;
|
||||||
@@ -270,7 +372,7 @@ int main(int argc, char **argv)
|
|||||||
sigset_t wait_mask, orig_mask;
|
sigset_t wait_mask, orig_mask;
|
||||||
struct pollfd pfd;
|
struct pollfd pfd;
|
||||||
int status = 0;
|
int status = 0;
|
||||||
int timed_out = 0, killed_by_us = 0, reaped = 0, done = 0;
|
int timed_out = 0, killed_by_us = 0, reaped = 0, done = 0, signal_failed = 0;
|
||||||
|
|
||||||
while (i < argc) {
|
while (i < argc) {
|
||||||
const char *a = argv[i];
|
const char *a = argv[i];
|
||||||
@@ -371,8 +473,10 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
/* 自成进程组:超时信号 kill(-pgid) 才能覆盖孙进程 */
|
/* 自成进程组:超时信号 kill(-pgid) 才能覆盖孙进程。
|
||||||
(void)setpgid(0, 0);
|
* 这里失败不直接报错(父进程会再做一次;发信号前还有 getpgid 核对),
|
||||||
|
* 因为子进程此时还没 exec,直接写 stderr 会和命令自身的输出混在一起。 */
|
||||||
|
(void)call_setpgid(0, 0);
|
||||||
for (size_t k = 0; k < sizeof(forward_sigs) / sizeof(forward_sigs[0]); k++)
|
for (size_t k = 0; k < sizeof(forward_sigs) / sizeof(forward_sigs[0]); k++)
|
||||||
(void)signal(forward_sigs[k], SIG_DFL);
|
(void)signal(forward_sigs[k], SIG_DFL);
|
||||||
(void)sigprocmask(SIG_SETMASK, &orig_mask, NULL);
|
(void)sigprocmask(SIG_SETMASK, &orig_mask, NULL);
|
||||||
@@ -392,13 +496,22 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
g_child_pgid = pid;
|
g_child_pgid = pid;
|
||||||
/* fork 与子进程 setpgid 之间的竞态由父进程这一侧兜住(任一侧成功即可) */
|
/* fork 与子进程 setpgid 之间的竞态由父进程这一侧兜住(任一侧成功即可)。
|
||||||
(void)setpgid(pid, pid);
|
* 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 赋值之间收到过转发信号,这里补发一次 */
|
/* 若在 fork 与 g_child_pgid 赋值之间收到过转发信号,这里补发一次 */
|
||||||
if (g_pending_sig != 0) {
|
if (g_pending_sig != 0) {
|
||||||
int sig = (int)g_pending_sig;
|
int sig = (int)g_pending_sig;
|
||||||
(void)kill(-pid, sig);
|
if (kill_child(pid, sig, "转发信号") < 0)
|
||||||
|
signal_failed = 1;
|
||||||
g_pending_sig = 0;
|
g_pending_sig = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,8 +544,12 @@ int main(int argc, char **argv)
|
|||||||
if (si.ssi_signo == SIGALRM) {
|
if (si.ssi_signo == SIGALRM) {
|
||||||
if (!timed_out) {
|
if (!timed_out) {
|
||||||
timed_out = 1;
|
timed_out = 1;
|
||||||
|
/* 真正要杀之前核对一次进程组:setpgid 若没生效,这里必须告警,
|
||||||
|
* 不能让用户看到 124 却以为孙进程都被杀了。 */
|
||||||
|
(void)verify_process_group(pid);
|
||||||
/* 先发温和信号(默认 TERM),发给整个进程组 */
|
/* 先发温和信号(默认 TERM),发给整个进程组 */
|
||||||
(void)kill(-pid, kill_signal);
|
if (kill_child(pid, kill_signal, "超时") < 0)
|
||||||
|
signal_failed = 1;
|
||||||
if (kill_signal == SIGKILL) {
|
if (kill_signal == SIGKILL) {
|
||||||
killed_by_us = 1;
|
killed_by_us = 1;
|
||||||
disarm_timer();
|
disarm_timer();
|
||||||
@@ -443,7 +560,8 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* -k 到期:KILL 整个组,仍继续 waitpid 回收直接子进程 */
|
/* -k 到期:KILL 整个组,仍继续 waitpid 回收直接子进程 */
|
||||||
(void)kill(-pid, SIGKILL);
|
if (kill_child(pid, SIGKILL, "-k 到期") < 0)
|
||||||
|
signal_failed = 1;
|
||||||
killed_by_us = 1;
|
killed_by_us = 1;
|
||||||
disarm_timer();
|
disarm_timer();
|
||||||
}
|
}
|
||||||
@@ -478,6 +596,10 @@ int main(int argc, char **argv)
|
|||||||
return EXIT_FAILURE_125;
|
return EXIT_FAILURE_125;
|
||||||
|
|
||||||
if (timed_out) {
|
if (timed_out) {
|
||||||
|
/* 信号完全发不出去时不能谎报 124:那是"以为杀了、其实没杀"的形态,
|
||||||
|
* 正是 2026-11-11 事故的样子。已在 stderr 告警,退出码用 125。 */
|
||||||
|
if (signal_failed)
|
||||||
|
return EXIT_FAILURE_125;
|
||||||
/* 超时:最终发了 KILL 才报 137,否则 124(coreutils 语义) */
|
/* 超时:最终发了 KILL 才报 137,否则 124(coreutils 语义) */
|
||||||
return killed_by_us ? 128 + SIGKILL : EXIT_TIMEOUT;
|
return killed_by_us ? 128 + SIGKILL : EXIT_TIMEOUT;
|
||||||
}
|
}
|
||||||
@@ -487,3 +609,73 @@ int main(int argc, char **argv)
|
|||||||
return 128 + WTERMSIG(status);
|
return 128 + WTERMSIG(status);
|
||||||
return EXIT_FAILURE_125;
|
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
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
# M53 —— 本地源码路径:timeout 产物存在/静态/MIPS32r2/零 NEEDED、
|
# M53 —— 本地源码路径:timeout 产物存在/静态/MIPS32r2/零 NEEDED、
|
||||||
# MANIFEST 有 Apache-2.0 行、license 原文随附、
|
# MANIFEST 有 Apache-2.0 行、license 原文随附、
|
||||||
# 源码纪律(独立实现、不调 clock_gettime、不用 timerfd、setpgid+kill(-pgid))
|
# 源码纪律(独立实现、不调 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 分钟)。
|
||||||
@@ -166,6 +168,52 @@ grep -q 'setpgid' "$SRC_T" && grep -q 'kill(-' "$SRC_T" \
|
|||||||
&& pass "T10d 进程组模型到位(setpgid + kill(-pgid))" \
|
&& pass "T10d 进程组模型到位(setpgid + kill(-pgid))" \
|
||||||
|| fail "T10d 缺进程组模型(孙进程会漏杀)"
|
|| 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