Files
MeowOS-For-R3P2/scripts/build/devkit-src/timeout.c
T
AskaEth 80c0bd4eb4 feat(m53): 自写 timeout(1) 并接入 devkit 本地源码构建路径
设备没有 timeout,busybox 1.31 也无该 applet;2026-11-11 18:44 一条挂在
硬件上的 bluetoothctl 累积 55 个残留进程耗尽进程表,dropbear 无法 fork,
整机在线却不可登录。本单给设备一个可用的 timeout。

- scripts/build/devkit-src/timeout.c(新,Apache-2.0,489 行)
  独立实现,未拷贝 busybox/GPL 代码;fork+setpgid(0,0) 后 kill(-pgid),
  孙进程一并终止;waitpid 回收直接子进程不留僵尸;-s/-k 两阶段终止;
  退出码 124/137/128+N/125/126/127 对齐 coreutils 语义。
  计时不用 timerfd(musl 的 timerfd_settime 在 4.4 上静默失效,实测
  timeout 2 sleep 30 等满 30s 才返回),改用 setitimer(ITIMER_REAL)+SIGALRM,
  与 SIGCHLD 一起进 signalfd,由一次 ppoll 等待,零轮询、不读时钟。
- build_devkit.sh:新增 build_local(B 路径,不碰上游 procps/lsof/ncurses
  流程);TOOLS/TOOL_NAMES/MANIFEST/licenses/stamp/--with-sources 接入;
  产物同时标注 Apache-2.0 并随附许可证原文。
- test_build_devkit.sh:T9(产物静态/架构/零 NEEDED/MANIFEST/license)、
  T10(源码纪律:独立实现、不调 clock_gettime、不用 timerfd、进程组模型)。
- DEVKIT.md/README.md:产物表、PATH 实测(ssh 非登录 shell 无 opt/bin,
  且设备无 bash → 用 sh -lc 或绝对路径)、禁裸跑纪律与 18:44 事故时间线、
  timerfd 静默失效的第二类 time64 坑、2026-11-11 真机验收原始输出。
2026-11-11 21:23:52 +08:00

490 lines
17 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/timerfd/ppoll/signalfd)。
*
* 为什么设备需要它(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、1.5m)与 s/m/h 后缀(大小写均可)。
* 不接受 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 自身失败(参数错、timerfd/fork 失败、命令状态不可得)
* 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 收养并回收。
*
* 计时(为什么不读时钟、也不用 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;
}
}
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(如 2、0.2、1.5m)\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;
if (mult != 1) {
if (sec > UINT64_MAX / mult)
return -1;
sec *= 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;
}
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;
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) 才能覆盖孙进程 */
(void)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 之间的竞态由父进程这一侧兜住(任一侧成功即可) */
(void)setpgid(pid, pid);
/* 若在 fork 与 g_child_pgid 赋值之间收到过转发信号,这里补发一次 */
if (g_pending_sig != 0) {
int sig = (int)g_pending_sig;
(void)kill(-pid, sig);
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;
/* 先发温和信号(默认 TERM),发给整个进程组 */
(void)kill(-pid, kill_signal);
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 回收直接子进程 */
(void)kill(-pid, SIGKILL);
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) {
/* 超时:最终发了 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;
}