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 真机验收原始输出。
This commit is contained in:
AskaEth committed 2026-11-11 21:23:52 +08:00
1 parent a41b61198e
commit 80c0bd4eb4
5 files changed
+855 -22

No files matched your search

+489
View File
@@ -0,0 +1,489 @@
/*
* 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;
}