diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:27 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:27 +0300 |
| commit | 3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7 (patch) | |
| tree | ea0b462d625dc4c5d104449d721ce29879dbedfc /corebinutils | |
| parent | 85f60af1bb558bc7248fb64528c5bba92e504adf (diff) | |
| parent | 2283ae2924693a1f4370a7dc423ace87b52600c2 (diff) | |
| download | Project-Tick-3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7.tar.gz Project-Tick-3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7.zip | |
Add 'corebinutils/sleep/' from commit '2283ae2924693a1f4370a7dc423ace87b52600c2'
git-subtree-dir: corebinutils/sleep
git-subtree-mainline: 85f60af1bb558bc7248fb64528c5bba92e504adf
git-subtree-split: 2283ae2924693a1f4370a7dc423ace87b52600c2
Diffstat (limited to 'corebinutils')
| -rw-r--r-- | corebinutils/sleep/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/sleep/GNUmakefile | 37 | ||||
| -rw-r--r-- | corebinutils/sleep/LICENSE | 32 | ||||
| -rw-r--r-- | corebinutils/sleep/README.md | 43 | ||||
| -rw-r--r-- | corebinutils/sleep/sleep.1 | 133 | ||||
| -rw-r--r-- | corebinutils/sleep/sleep.c | 269 | ||||
| -rw-r--r-- | corebinutils/sleep/tests/test.sh | 378 |
7 files changed, 917 insertions, 0 deletions
diff --git a/corebinutils/sleep/.gitignore b/corebinutils/sleep/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/sleep/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/corebinutils/sleep/GNUmakefile b/corebinutils/sleep/GNUmakefile new file mode 100644 index 0000000000..bb7338f0e2 --- /dev/null +++ b/corebinutils/sleep/GNUmakefile @@ -0,0 +1,37 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS ?= +CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror +LDFLAGS ?= +LDLIBS ?= +LDLIBS += -lm + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/sleep +OBJS := $(OBJDIR)/sleep.o + +.PHONY: all clean dirs status test + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/sleep.o: $(CURDIR)/sleep.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/sleep.c" -o "$@" + +test: $(TARGET) + CC="$(CC)" SLEEP_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/sleep/LICENSE b/corebinutils/sleep/LICENSE new file mode 100644 index 0000000000..8d4e27aede --- /dev/null +++ b/corebinutils/sleep/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 1988, 1993, 1994 + The Regents of the University of California. All rights reserved. + +Copyright (c) 2026 + Project Tick. All rights reserved. + +This code is derived from software contributed to Berkeley by +the Institute of Electrical and Electronics Engineers, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/corebinutils/sleep/README.md b/corebinutils/sleep/README.md new file mode 100644 index 0000000000..f3ba39beb5 --- /dev/null +++ b/corebinutils/sleep/README.md @@ -0,0 +1,43 @@ +# sleep + +Standalone musl-libc-based Linux port of FreeBSD `sleep` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port Strategy + +- Port structure follows the standalone sibling ports such as `bin/hostname` and `bin/date`: local `GNUmakefile`, short technical `README.md`, and shell tests under `tests/`. +- The FreeBSD source was converted directly to a Linux-native implementation instead of preserving BSD build glue, Capsicum entry, or BSD libc diagnostics. +- Interval parsing is strict and manpage-driven: operands are parsed with `strtold(3)`, unit handling is explicit, invalid trailing data is rejected, and the final summed interval is rounded up to the next nanosecond so Linux does not undersleep the requested minimum duration. + +## Linux API Mapping + +- FreeBSD `nanosleep(2)` usage remains `nanosleep(2)` on Linux; interrupted sleeps are resumed with the kernel-provided remaining interval. +- FreeBSD `SIGINFO` progress reporting maps to Linux `SIGUSR1`, because Linux does not provide `SIGINFO`. The report still prints the estimated remaining time for the current sleep request. +- FreeBSD `<err.h>` diagnostics are replaced with local `fprintf(3)`-based error handling so the port builds cleanly on musl without BSD libc helpers. +- FreeBSD Capsicum setup (`caph_limit_stdio()` / `caph_enter()`) is removed. Linux has no equivalent process-sandbox semantic that belongs in `sleep(1)`, and adding a fake compatibility path would be incorrect. + +## Supported Semantics On Linux + +- `sleep number[unit] [...]` with `s`, `m`, `h`, or `d` suffixes, fractional operands, and multiple operands added together +- Negative operands when the final sum remains positive, matching the manpage statement that zero or negative final sums exit immediately +- `--` as an operand separator for callers that want explicit disambiguation before negative numeric operands +- Progress reporting on Linux via `SIGUSR1`, written to stdout as the manpage requires for the `SIGINFO` path + +## Intentionally Unsupported Semantics + +- Non-finite `strtold(3)` forms such as `inf`, `infinity`, and `nan` are rejected with an explicit error. Linux has no finite `nanosleep(2)` representation for those values, and the port does not guess at an emulation. +- Requests that exceed Linux `time_t` / `struct timespec` range fail with an explicit error instead of truncating or wrapping. +- No GNU `sleep` extensions such as `--help` or `--version` are implemented; `sleep.1` defines no such interface. diff --git a/corebinutils/sleep/sleep.1 b/corebinutils/sleep/sleep.1 new file mode 100644 index 0000000000..c9069a0157 --- /dev/null +++ b/corebinutils/sleep/sleep.1 @@ -0,0 +1,133 @@ +.\"- +.\" Copyright (c) 1990, 1993, 1994 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" the Institute of Electrical and Electronics Engineers, Inc. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd March 22, 2024 +.Dt SLEEP 1 +.Os +.Sh NAME +.Nm sleep +.Nd suspend execution for an interval of time +.Sh SYNOPSIS +.Nm +.Ar number Ns Op Ar unit +.Op ... +.Sh DESCRIPTION +The +.Nm +command suspends execution for a minimum of +.Ar number +seconds (the default, or unit +.Li s ) , +minutes (unit +.Li m ) , +hours (unit +.Li h ) , +or days (unit +.Li d ) . +Intervals can be written in any form allowed by +.Xr strtod 3 . +If multiple intervals are given, they are added together. +If the final sum is zero or negative, +.Nm +exits immediately. +.Pp +If the +.Nm +command receives a signal, it takes the standard action. +When the +.Dv SIGINFO +signal is received, the estimate of the amount of seconds left to +sleep is printed on the standard output. +.Sh IMPLEMENTATION NOTES +The +.Dv SIGALRM +signal is not handled specially by this implementation. +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +To run a command after half an hour: +.Pp +.Dl (sleep 0.5h; sh command_file >out 2>err)& +.Pp +This incantation would wait half an hour before +running the script +.Pa command_file . +See the +.Xr at 1 +utility for another way to do this. +.Pp +To reiteratively run a command: +.Pp +.Bd -literal -offset indent -compact +while :; do + if ! [ -r zzz.rawdata ] ; then + sleep 5m + else + for i in *.rawdata ; do + sleep 70 + awk -f collapse_data "$i" + done >results + break + fi +done +.Ed +.Pp +The scenario for a script such as this might be: a program currently +running is taking longer than expected to process a series of +files, and it would be nice to have +another program start processing the files created by the first +program as soon as it is finished (when +.Pa zzz.rawdata +is created). +The script checks every five minutes for the file +.Pa zzz.rawdata , +when the file is found, then another portion processing +is done courteously by sleeping for 70 seconds in between each +.Xr awk 1 +job. +.Sh SEE ALSO +.Xr nanosleep 2 , +.Xr sleep 3 +.Sh STANDARDS +The +.Nm +command is expected to be +.St -p1003.2 +compatible. +.Pp +Support for non-integer intervals, units other than seconds, and +multiple intervals which are added together are non-portable +extensions first introduced in GNU sh-utils 2.0a (released in 2002). +.Sh HISTORY +A +.Nm +command appeared in +.At v4 . diff --git a/corebinutils/sleep/sleep.c b/corebinutils/sleep/sleep.c new file mode 100644 index 0000000000..d56845b4fe --- /dev/null +++ b/corebinutils/sleep/sleep.c @@ -0,0 +1,269 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1988, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <stdarg.h> +#include <stdbool.h> +#include <errno.h> +#include <limits.h> +#include <math.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#ifndef SIGINFO +#define SLEEP_INFO_SIGNAL SIGUSR1 +#else +#define SLEEP_INFO_SIGNAL SIGINFO +#endif + +#define NSECS_PER_SEC 1000000000L + +static volatile sig_atomic_t report_requested; + +static void usage(void) __attribute__((noreturn)); +static void die(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2))); +static void die_errno(const char *context) __attribute__((noreturn)); +static void install_info_handler(void); +static long double seconds_from_timespec(const struct timespec *ts); +static long double maximum_time_t_seconds(void); +static void report_remaining(const struct timespec *remaining, long double original); +static long double scale_interval(long double value, int multiplier, const char *arg); +static long double parse_interval(const char *arg); +static struct timespec seconds_to_timespec(long double seconds); + +static void +report_request(int signo) +{ + (void)signo; + report_requested = 1; +} + +static void +usage(void) +{ + fprintf(stderr, "usage: sleep number[unit] [...]\n" + "Unit can be 's' (seconds, the default), " + "m (minutes), h (hours), or d (days).\n"); + exit(1); +} + +static void +die(const char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "sleep: "); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} + +static void +die_errno(const char *context) +{ + die("%s: %s", context, strerror(errno)); +} + +static void +install_info_handler(void) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = report_request; + sigemptyset(&sa.sa_mask); + if (sigaction(SLEEP_INFO_SIGNAL, &sa, NULL) != 0) + die_errno("sigaction"); +} + +static long double +seconds_from_timespec(const struct timespec *ts) +{ + return ((long double)ts->tv_sec + + (long double)ts->tv_nsec / (long double)NSECS_PER_SEC); +} + +static long double +maximum_time_t_seconds(void) +{ + int bits; + + bits = (int)(sizeof(time_t) * CHAR_BIT); + if ((time_t)-1 < (time_t)0) + return (ldexpl(1.0L, bits - 1) - 1.0L); + return (ldexpl(1.0L, bits) - 1.0L); +} + +static void +report_remaining(const struct timespec *remaining, long double original) +{ + if (printf("about %.9Lf second(s) left out of the original %.9Lf\n", + seconds_from_timespec(remaining), original) < 0) + die_errno("stdout"); + if (fflush(stdout) != 0) + die_errno("stdout"); +} + +static long double +scale_interval(long double value, int multiplier, const char *arg) +{ + long double result; + + result = value * (long double)multiplier; + if (!isfinite(result)) + die("time interval out of range: %s", arg); + return (result); +} + +static long double +parse_interval(const char *arg) +{ + long double value; + char *end; + + errno = 0; + value = strtold(arg, &end); + if (end == arg) + die("invalid time interval: %s", arg); + if (errno == ERANGE) + die("time interval out of range: %s", arg); + if (!isfinite(value)) + die("non-finite time interval is not supported on Linux: %s", arg); + if (*end == '\0') + return (value); + if (end[1] != '\0') + die("invalid time interval: %s", arg); + + switch (*end) { + case 's': + return (value); + case 'm': + return (scale_interval(value, 60, arg)); + case 'h': + return (scale_interval(value, 60 * 60, arg)); + case 'd': + return (scale_interval(value, 24 * 60 * 60, arg)); + default: + die("unsupported time unit in interval '%s': '%c' (supported: s, m, h, d)", + arg, *end); + } +} + +static struct timespec +seconds_to_timespec(long double seconds) +{ + struct timespec ts; + long double whole; + long double fractional; + long double nsec; + long double max_seconds; + + if (seconds <= 0.0L) { + ts.tv_sec = 0; + ts.tv_nsec = 0; + return (ts); + } + + max_seconds = maximum_time_t_seconds(); + if (seconds > max_seconds + 1.0L) + die("requested interval is too large for Linux sleep APIs"); + + fractional = modfl(seconds, &whole); + if (whole > max_seconds) + die("requested interval is too large for Linux sleep APIs"); + + ts.tv_sec = (time_t)whole; + nsec = ceill(fractional * (long double)NSECS_PER_SEC); + if (nsec >= (long double)NSECS_PER_SEC) { + if ((long double)ts.tv_sec >= max_seconds) + die("requested interval is too large for Linux sleep APIs"); + ts.tv_sec += 1; + ts.tv_nsec = 0; + return (ts); + } + if (nsec <= 0.0L) + nsec = 1.0L; + ts.tv_nsec = (long)nsec; + return (ts); +} + +int +main(int argc, char *argv[]) +{ + struct timespec time_to_sleep; + long double original; + long double seconds; + int i; + + if (argc > 1 && strcmp(argv[1], "--") == 0) { + argv++; + argc--; + } + if (argc < 2) + usage(); + + seconds = 0.0L; + for (i = 1; i < argc; i++) { + long double interval; + long double total; + + interval = parse_interval(argv[i]); + total = seconds + interval; + if (!isfinite(total)) + die("time interval out of range after adding: %s", argv[i]); + seconds = total; + } + if (seconds <= 0.0L) + exit(0); + + time_to_sleep = seconds_to_timespec(seconds); + original = seconds_from_timespec(&time_to_sleep); + install_info_handler(); + + while (nanosleep(&time_to_sleep, &time_to_sleep) != 0) { + if (errno != EINTR) + die_errno("nanosleep"); + if (report_requested) { + report_remaining(&time_to_sleep, original); + report_requested = 0; + } + } + + exit(0); +} diff --git a/corebinutils/sleep/tests/test.sh b/corebinutils/sleep/tests/test.sh new file mode 100644 index 0000000000..5aab4b9499 --- /dev/null +++ b/corebinutils/sleep/tests/test.sh @@ -0,0 +1,378 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +SLEEP_BIN=${SLEEP_BIN:-"$ROOT/out/sleep"} +CC=${CC:-cc} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/sleep-test.XXXXXX") +HELPER_C="$WORKDIR/run_sleep_case.c" +HELPER_BIN="$WORKDIR/run_sleep_case" +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +RESULT_FILE="$WORKDIR/result" +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +export LC_ALL=C + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_eq() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" != "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected ---" >&2 + printf '%s' "$expected" >&2 + printf '\n%s\n' "--- actual ---" >&2 + printf '%s' "$actual" >&2 + printf '\n' >&2 + exit 1 + fi +} + +assert_empty() { + name=$1 + text=$2 + if [ -n "$text" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected empty ---" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s' "$text" >&2 + printf '\n' >&2 + exit 1 + fi +} + +assert_status() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" -ne "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected status: $expected" >&2 + printf '%s\n' "actual status: $actual" >&2 + exit 1 + fi +} + +assert_ge() { + name=$1 + actual=$2 + minimum=$3 + if [ "$actual" -lt "$minimum" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected >= $minimum" >&2 + printf '%s\n' "actual: $actual" >&2 + exit 1 + fi +} + +assert_match() { + name=$1 + pattern=$2 + text=$3 + printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$name" +} + +build_helper() { + cat >"$HELPER_C" <<'EOF' +#define _POSIX_C_SOURCE 200809L +#define _XOPEN_SOURCE 700 + +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +#define NSECS_PER_SEC 1000000000LL + +static int64_t +ns_since_epoch(const struct timespec *ts) +{ + return ((int64_t)ts->tv_sec * NSECS_PER_SEC + (int64_t)ts->tv_nsec); +} + +static void +sleep_ms(int milliseconds) +{ + struct timespec request; + + request.tv_sec = milliseconds / 1000; + request.tv_nsec = (long)(milliseconds % 1000) * 1000000L; + while (nanosleep(&request, &request) != 0) { + if (errno != EINTR) { + perror("nanosleep"); + exit(2); + } + } +} + +int +main(int argc, char *argv[]) +{ + struct timespec started; + struct timespec now; + pid_t child; + int stdout_fd; + int stderr_fd; + int timeout_ms; + int signal_delay_ms; + int status; + int exit_status; + int64_t elapsed_ns; + bool signal_sent; + char *end; + + if (argc < 6) { + fprintf(stderr, "usage: run_sleep_case timeout_ms signal_delay_ms stdout stderr bin [args ...]\n"); + return (2); + } + + errno = 0; + timeout_ms = (int)strtol(argv[1], &end, 10); + if (errno != 0 || *end != '\0' || timeout_ms <= 0) { + fprintf(stderr, "invalid timeout: %s\n", argv[1]); + return (2); + } + + errno = 0; + signal_delay_ms = (int)strtol(argv[2], &end, 10); + if (errno != 0 || *end != '\0' || signal_delay_ms < -1) { + fprintf(stderr, "invalid signal delay: %s\n", argv[2]); + return (2); + } + + stdout_fd = open(argv[3], O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (stdout_fd < 0) { + perror("open stdout"); + return (2); + } + + stderr_fd = open(argv[4], O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (stderr_fd < 0) { + perror("open stderr"); + return (2); + } + + if (clock_gettime(CLOCK_MONOTONIC, &started) != 0) { + perror("clock_gettime"); + return (2); + } + + child = fork(); + if (child < 0) { + perror("fork"); + return (2); + } + if (child == 0) { + if (dup2(stdout_fd, STDOUT_FILENO) < 0 || dup2(stderr_fd, STDERR_FILENO) < 0) { + perror("dup2"); + _exit(127); + } + close(stdout_fd); + close(stderr_fd); + execv(argv[5], &argv[5]); + perror("execv"); + _exit(127); + } + + close(stdout_fd); + close(stderr_fd); + + signal_sent = false; + for (;;) { + pid_t waited; + int64_t elapsed_ms; + + waited = waitpid(child, &status, WNOHANG); + if (waited < 0) { + perror("waitpid"); + return (2); + } + if (waited == child) + break; + + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + perror("clock_gettime"); + return (2); + } + + elapsed_ms = (ns_since_epoch(&now) - ns_since_epoch(&started)) / 1000000LL; + if (!signal_sent && signal_delay_ms >= 0 && elapsed_ms >= signal_delay_ms) { + if (kill(child, SIGUSR1) != 0) { + perror("kill"); + return (2); + } + signal_sent = true; + } + if (elapsed_ms >= timeout_ms) { + if (kill(child, SIGKILL) != 0 && errno != ESRCH) { + perror("kill timeout"); + return (2); + } + if (waitpid(child, &status, 0) < 0) { + perror("waitpid timeout"); + return (2); + } + break; + } + + sleep_ms(5); + } + + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) { + perror("clock_gettime"); + return (2); + } + elapsed_ns = ns_since_epoch(&now) - ns_since_epoch(&started); + + if (WIFEXITED(status)) + exit_status = WEXITSTATUS(status); + else if (WIFSIGNALED(status)) + exit_status = 128 + WTERMSIG(status); + else + exit_status = 255; + + printf("status=%d\n", exit_status); + printf("elapsed_ns=%lld\n", (long long)elapsed_ns); + return (0); +} +EOF + + "$CC" -O2 -std=c17 -Wall -Wextra -Werror "$HELPER_C" -o "$HELPER_BIN" \ + || fail "failed to build helper with $CC" +} + +run_case() { + timeout_ms=$1 + signal_delay_ms=$2 + shift 2 + + "$HELPER_BIN" "$timeout_ms" "$signal_delay_ms" "$STDOUT_FILE" "$STDERR_FILE" \ + "$SLEEP_BIN" "$@" >"$RESULT_FILE" \ + || fail "helper failed for: $*" + + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") + LAST_STATUS=$(sed -n 's/^status=//p' "$RESULT_FILE") + LAST_ELAPSED_NS=$(sed -n 's/^elapsed_ns=//p' "$RESULT_FILE") +} + +[ -x "$SLEEP_BIN" ] || fail "missing binary: $SLEEP_BIN" +build_helper + +usage_text=$(printf '%s\n%s' \ + "usage: sleep number[unit] [...]" \ + "Unit can be 's' (seconds, the default), m (minutes), h (hours), or d (days).") + +run_case 500 -1 +assert_status "no-arg usage status" 1 "$LAST_STATUS" +assert_empty "no-arg usage stdout" "$LAST_STDOUT" +assert_eq "no-arg usage stderr" "$usage_text" "$LAST_STDERR" + +run_case 500 -1 -- +assert_status "bare -- usage status" 1 "$LAST_STATUS" +assert_empty "bare -- usage stdout" "$LAST_STDOUT" +assert_eq "bare -- usage stderr" "$usage_text" "$LAST_STDERR" + +run_case 500 -1 0.03 +assert_status "seconds operand status" 0 "$LAST_STATUS" +assert_empty "seconds operand stdout" "$LAST_STDOUT" +assert_empty "seconds operand stderr" "$LAST_STDERR" +assert_ge "seconds operand elapsed" "$LAST_ELAPSED_NS" 20000000 + +run_case 500 -1 0.0005m +assert_status "minutes operand status" 0 "$LAST_STATUS" +assert_empty "minutes operand stdout" "$LAST_STDOUT" +assert_empty "minutes operand stderr" "$LAST_STDERR" +assert_ge "minutes operand elapsed" "$LAST_ELAPSED_NS" 20000000 + +run_case 500 -1 0.00001h +assert_status "hours operand status" 0 "$LAST_STATUS" +assert_empty "hours operand stdout" "$LAST_STDOUT" +assert_empty "hours operand stderr" "$LAST_STDERR" +assert_ge "hours operand elapsed" "$LAST_ELAPSED_NS" 25000000 + +run_case 500 -1 0.0000005d +assert_status "days operand status" 0 "$LAST_STATUS" +assert_empty "days operand stdout" "$LAST_STDOUT" +assert_empty "days operand stderr" "$LAST_STDERR" +assert_ge "days operand elapsed" "$LAST_ELAPSED_NS" 30000000 + +run_case 500 -1 -0.02 0.07s +assert_status "negative operand addition status" 0 "$LAST_STATUS" +assert_empty "negative operand addition stdout" "$LAST_STDOUT" +assert_empty "negative operand addition stderr" "$LAST_STDERR" +assert_ge "negative operand addition elapsed" "$LAST_ELAPSED_NS" 30000000 + +run_case 500 -1 -- -0.01 0.03s +assert_status "double-dash negative status" 0 "$LAST_STATUS" +assert_empty "double-dash negative stdout" "$LAST_STDOUT" +assert_empty "double-dash negative stderr" "$LAST_STDERR" +assert_ge "double-dash negative elapsed" "$LAST_ELAPSED_NS" 15000000 + +run_case 200 -1 1 -1 +assert_status "zero-sum immediate status" 0 "$LAST_STATUS" +assert_empty "zero-sum immediate stdout" "$LAST_STDOUT" +assert_empty "zero-sum immediate stderr" "$LAST_STDERR" + +run_case 200 -1 bogus +assert_status "bogus operand status" 1 "$LAST_STATUS" +assert_empty "bogus operand stdout" "$LAST_STDOUT" +assert_eq "bogus operand stderr" "sleep: invalid time interval: bogus" "$LAST_STDERR" + +run_case 200 -1 1ss +assert_status "trailing garbage status" 1 "$LAST_STATUS" +assert_empty "trailing garbage stdout" "$LAST_STDOUT" +assert_eq "trailing garbage stderr" "sleep: invalid time interval: 1ss" "$LAST_STDERR" + +run_case 200 -1 1w +assert_status "unsupported unit status" 1 "$LAST_STATUS" +assert_empty "unsupported unit stdout" "$LAST_STDOUT" +assert_eq "unsupported unit stderr" \ + "sleep: unsupported time unit in interval '1w': 'w' (supported: s, m, h, d)" \ + "$LAST_STDERR" + +run_case 200 -1 inf +assert_status "inf status" 1 "$LAST_STATUS" +assert_empty "inf stdout" "$LAST_STDOUT" +assert_eq "inf stderr" \ + "sleep: non-finite time interval is not supported on Linux: inf" \ + "$LAST_STDERR" + +run_case 200 -1 nan +assert_status "nan status" 1 "$LAST_STATUS" +assert_empty "nan stdout" "$LAST_STDOUT" +assert_eq "nan stderr" \ + "sleep: non-finite time interval is not supported on Linux: nan" \ + "$LAST_STDERR" + +run_case 200 -1 1e30d +assert_status "too-large status" 1 "$LAST_STATUS" +assert_empty "too-large stdout" "$LAST_STDOUT" +assert_eq "too-large stderr" \ + "sleep: requested interval is too large for Linux sleep APIs" \ + "$LAST_STDERR" + +run_case 1500 50 0.4s +assert_status "signal report status" 0 "$LAST_STATUS" +assert_empty "signal report stderr" "$LAST_STDERR" +assert_match "signal report stdout" \ + '^about [0-9]+\.[0-9]{9} second\(s\) left out of the original 0\.400000000$' \ + "$LAST_STDOUT" +assert_ge "signal report elapsed" "$LAST_ELAPSED_NS" 300000000 + +printf '%s\n' "PASS" |
