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 失败。
682 lines
26 KiB
C
682 lines
26 KiB
C
/*
|
||
* 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
|