/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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