Files
MeowOS-For-R3P2/scripts/build/devkit-src/timeout.c
T
AskaEth c93814a78b 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 失败。
2026-11-11 21:23:52 +08:00

682 lines
26 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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