Author | Jamozed <[email protected]> |
Date | 2022-02-28 14:04:16 |
Commit | 48e316bef1bc769c698b51ec0b987caf74d5a993 |
Parent | d57b240f3cd7ad52e749855d70ee811abfeb0e48 |
timeout: Add timeout utility
Diffstat
M | CMakeLists.txt | | | 85 | ++++++++++++++++++++++++++++++++++++++++--------------------------------------- |
A | src/timeout.c | | | 257 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 300 insertions, 42 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 4603db4..9eb8a66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,47 +1,48 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 3.12) -PROJECT(coreutils LANGUAGES C) +cmake_minimum_required(VERSION 3.12) +project(coreutils LANGUAGES C) -SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) -SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) -SET(CMAKE_STATIC_LIBRARY_PREFIX "") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) +set(CMAKE_STATIC_LIBRARY_PREFIX "") -FILE(GLOB SRC_UTIL ${PROJECT_SOURCE_DIR}/src/util/*) +file(GLOB SRC_UTIL ${PROJECT_SOURCE_DIR}/src/util/*) -ADD_LIBRARY(libutil STATIC ${SRC_UTIL}) +add_library(libutil STATIC ${SRC_UTIL}) -LINK_LIBRARIES(libutil) +link_libraries(libutil) -ADD_EXECUTABLE(base64 ${PROJECT_SOURCE_DIR}/src/base64.c) -ADD_EXECUTABLE(basename ${PROJECT_SOURCE_DIR}/src/basename.c) -ADD_EXECUTABLE(cat ${PROJECT_SOURCE_DIR}/src/cat.c) -ADD_EXECUTABLE(chmod ${PROJECT_SOURCE_DIR}/src/chmod.c) -ADD_EXECUTABLE(cksum ${PROJECT_SOURCE_DIR}/src/cksum.c) -ADD_EXECUTABLE(cp ${PROJECT_SOURCE_DIR}/src/cp.c) -ADD_EXECUTABLE(dirname ${PROJECT_SOURCE_DIR}/src/dirname.c) -ADD_EXECUTABLE(echo ${PROJECT_SOURCE_DIR}/src/echo.c) -ADD_EXECUTABLE(env ${PROJECT_SOURCE_DIR}/src/env.c) -ADD_EXECUTABLE(false ${PROJECT_SOURCE_DIR}/src/false.c) -ADD_EXECUTABLE(head ${PROJECT_SOURCE_DIR}/src/head.c) -ADD_EXECUTABLE(id ${PROJECT_SOURCE_DIR}/src/id.c) -ADD_EXECUTABLE(link ${PROJECT_SOURCE_DIR}/src/link.c) -ADD_EXECUTABLE(logname ${PROJECT_SOURCE_DIR}/src/logname.c) -ADD_EXECUTABLE(mkdir ${PROJECT_SOURCE_DIR}/src/mkdir.c) -ADD_EXECUTABLE(nice ${PROJECT_SOURCE_DIR}/src/nice.c) -ADD_EXECUTABLE(orphan ${PROJECT_SOURCE_DIR}/src/orphan.c) -ADD_EXECUTABLE(od ${PROJECT_SOURCE_DIR}/src/od.c) -ADD_EXECUTABLE(pwd ${PROJECT_SOURCE_DIR}/src/pwd.c) -ADD_EXECUTABLE(rand ${PROJECT_SOURCE_DIR}/src/rand.c) -ADD_EXECUTABLE(realpath ${PROJECT_SOURCE_DIR}/src/realpath.c) -ADD_EXECUTABLE(rmdir ${PROJECT_SOURCE_DIR}/src/rmdir.c) -ADD_EXECUTABLE(sleep ${PROJECT_SOURCE_DIR}/src/sleep.c) -ADD_EXECUTABLE(sum ${PROJECT_SOURCE_DIR}/src/sum.c) -ADD_EXECUTABLE(sync ${PROJECT_SOURCE_DIR}/src/sync.c) -ADD_EXECUTABLE(tee ${PROJECT_SOURCE_DIR}/src/tee.c) -ADD_EXECUTABLE(time ${PROJECT_SOURCE_DIR}/src/time.c) -ADD_EXECUTABLE(touch ${PROJECT_SOURCE_DIR}/src/touch.c) -ADD_EXECUTABLE(true ${PROJECT_SOURCE_DIR}/src/true.c) -ADD_EXECUTABLE(tty ${PROJECT_SOURCE_DIR}/src/tty.c) -ADD_EXECUTABLE(uname ${PROJECT_SOURCE_DIR}/src/uname.c) -ADD_EXECUTABLE(unlink ${PROJECT_SOURCE_DIR}/src/unlink.c) -ADD_EXECUTABLE(wc ${PROJECT_SOURCE_DIR}/src/wc.c) -ADD_EXECUTABLE(yes ${PROJECT_SOURCE_DIR}/src/yes.c) +add_executable(base64 ${PROJECT_SOURCE_DIR}/src/base64.c) +add_executable(basename ${PROJECT_SOURCE_DIR}/src/basename.c) +add_executable(cat ${PROJECT_SOURCE_DIR}/src/cat.c) +add_executable(chmod ${PROJECT_SOURCE_DIR}/src/chmod.c) +add_executable(cksum ${PROJECT_SOURCE_DIR}/src/cksum.c) +add_executable(cp ${PROJECT_SOURCE_DIR}/src/cp.c) +add_executable(dirname ${PROJECT_SOURCE_DIR}/src/dirname.c) +add_executable(echo ${PROJECT_SOURCE_DIR}/src/echo.c) +add_executable(env ${PROJECT_SOURCE_DIR}/src/env.c) +add_executable(false ${PROJECT_SOURCE_DIR}/src/false.c) +add_executable(head ${PROJECT_SOURCE_DIR}/src/head.c) +add_executable(id ${PROJECT_SOURCE_DIR}/src/id.c) +add_executable(link ${PROJECT_SOURCE_DIR}/src/link.c) +add_executable(logname ${PROJECT_SOURCE_DIR}/src/logname.c) +add_executable(mkdir ${PROJECT_SOURCE_DIR}/src/mkdir.c) +add_executable(nice ${PROJECT_SOURCE_DIR}/src/nice.c) +add_executable(orphan ${PROJECT_SOURCE_DIR}/src/orphan.c) +add_executable(od ${PROJECT_SOURCE_DIR}/src/od.c) +add_executable(pwd ${PROJECT_SOURCE_DIR}/src/pwd.c) +add_executable(rand ${PROJECT_SOURCE_DIR}/src/rand.c) +add_executable(realpath ${PROJECT_SOURCE_DIR}/src/realpath.c) +add_executable(rmdir ${PROJECT_SOURCE_DIR}/src/rmdir.c) +add_executable(sleep ${PROJECT_SOURCE_DIR}/src/sleep.c) +add_executable(sum ${PROJECT_SOURCE_DIR}/src/sum.c) +add_executable(sync ${PROJECT_SOURCE_DIR}/src/sync.c) +add_executable(tee ${PROJECT_SOURCE_DIR}/src/tee.c) +add_executable(time ${PROJECT_SOURCE_DIR}/src/time.c) +add_executable(timeout ${PROJECT_SOURCE_DIR}/src/timeout.c) +add_executable(touch ${PROJECT_SOURCE_DIR}/src/touch.c) +add_executable(true ${PROJECT_SOURCE_DIR}/src/true.c) +add_executable(tty ${PROJECT_SOURCE_DIR}/src/tty.c) +add_executable(uname ${PROJECT_SOURCE_DIR}/src/uname.c) +add_executable(unlink ${PROJECT_SOURCE_DIR}/src/unlink.c) +add_executable(wc ${PROJECT_SOURCE_DIR}/src/wc.c) +add_executable(yes ${PROJECT_SOURCE_DIR}/src/yes.c) diff --git a/src/timeout.c b/src/timeout.c new file mode 100644 index 0000000..3ab1a00 --- /dev/null +++ b/src/timeout.c @@ -0,0 +1,257 @@ +// timeout.c +// OMKOV coreutils timeout +// Copyright (C) 2022, Jakob Wakeling +// All rights reserved. + +#define VERSION "0.1.0" + +/* +OMKOV Permissive Licence, version 1.0 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimers. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimers in the documentation and/or + other materials provided with the distribution. +* Neither the names of the copyright holders, nor the names of its contributors + may be used to endorse or promote products derived from this Software without + specific prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT +HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. +*/ + +#include "util/error.h" +#include "util/optget.h" +#include "util/util.h" + +#include <sys/wait.h> +#include <unistd.h> + +#include <errno.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> + +static struct lop lops[] = { + { "help", ARG_NUL, 256 }, + { "version", ARG_NUL, 257 }, + { NULL, 0, 0 } +}; + +static f64 itime = 0, ktime = 0; +static int tosig = SIGTERM; + +static pid_t pid = 0; +static bool tout = false; + +static f64 time_parse(const char *s); +static int timeout(char *av[]); + +static void signal_install(int sig); +static void signal_block(int sig, sigset_t *oset); +static void handle_sigalrm(int sig); +static void handle_sigchld(int sig); +static int signal_send(pid_t pid, int sig); + +static void timer(f64 duration); +static struct timespec f64totimespec(f64 seconds); + +static void hlp(void); +static void ver(void); + +int main(int ac, char *av[]) { A0 = av[0]; + struct opt opt = OPTGET_INIT; opt.str = "k:s:"; opt.lops = lops; + for (int o; (o = optget(&opt, av, 1)) != -1;) switch (o) { + case 'k': { ktime = time_parse(opt.arg); } break; + case 's': { /* TODO */ return 1; } break; + case 256: { hlp(); } return 0; + case 257: { ver(); } return 0; + default: {} return 125; + } + + if ((ac - opt.ind) < 2) { error(125, "missing operand(s)"); } + + itime = time_parse(av[opt.ind]); + return timeout(&av[opt.ind + 1]); +} + +/* Parse a duration string. */ +static inline f64 time_parse(const char *s) { + char *ep = NULL; errno = 0; f64 time = strtod(s, &ep); + if (errno != 0) { error(1, "%s: %s", s, SERR); } + if (time < 0) { error(1, "%s: duration cannot be negative", s); } + + /* Check for an apply any valid suffix */ + if (ep != NULL && ep[0] != '\0') { + switch (ep[0]) { + case 'D': { time *= 86400; } break; + case 'h': { time *= 3600; } break; + case 'm': { time *= 60; } break; + case 's': { time *= 1; } break; + default: { error(1, "%s: invalid suffix", s); } break; + } + + if (ep[1] != '\0') { error(1, "%s: invalid suffix", s); } + } + + return time; +} + +static int timeout(char *av[]) { + signal_install(tosig); + signal(SIGTTIN, SIG_IGN); + signal(SIGTTOU, SIG_IGN); + + switch ((pid = fork())) { + case -1: { error(125, "%s", SERR); } + case 0: { /* Child */ + signal(SIGTTIN, SIG_DFL); + signal(SIGTTOU, SIG_DFL); + + execvp(av[0], av); + + error((errno = ENOENT) ? 127 : 126, "%s: %s", av[0], SERR); + } break; + default: { /* Parent */ + int status; pid_t wpid; sigset_t sset; + + /* Unblock and start the timeout timer */ + /* TODO unblock */ timer(itime); + + signal_block(tosig, &sset); + + /* Wait for the timer to expire, or the child to die */ + for (; (wpid = waitpid(pid, &status, WNOHANG)) == 0;) { + sigsuspend(&sset); + } + + if (wpid == -1) { error(125, "%s", SERR); } + + /* If the child process exited normally */ + if (WIFEXITED(status)) { status = WEXITSTATUS(status); } + /* If the child process exited abnormally */ + else if (WIFSIGNALED(status)) { + int sig = WTERMSIG(status); + + status = 128 + sig; + } + else { error(125, "%s", SERR); } + + return tout ? 124 : status; + } break; + } +} + +/* Configure SIGALRM handling. */ +static void signal_install(int sig) { + struct sigaction sa0 = { .sa_handler = handle_sigalrm, .sa_flags = SA_RESTART }; + struct sigaction sa1 = { .sa_handler = handle_sigchld, .sa_flags = SA_RESTART }; + + sigemptyset(&sa0.sa_mask); + sigaction(SIGALRM, &sa0, NULL); /* Alarm signal, from the timeout timer */ + sigaction(SIGHUP, &sa0, NULL); /* Hangup signal, i.e. upon tty closing */ + sigaction(SIGINT, &sa0, NULL); /* Interrupt signal, i.e. from Ctrl+C */ + sigaction(SIGQUIT, &sa0, NULL); /* Quit signal, i.e. from Ctrl+\ */ + sigaction(SIGTERM, &sa0, NULL); /* Terminate signal, i.e. upon death */ + sigaction(sig, &sa0, NULL); /* User specified signal */ + + sigemptyset(&sa1.sa_mask); + sigaction(SIGCHLD, &sa1, NULL); /* Child signal, i.e. upon child death */ +} + +/* Block all appropriate signals, as per signal_install(). */ +static void signal_block(int sig, sigset_t *oset) { + sigset_t set; sigemptyset(&set); + sigaddset(&set, SIGALRM); + sigaddset(&set, SIGHUP); + sigaddset(&set, SIGINT); + sigaddset(&set, SIGQUIT); + sigaddset(&set, SIGTERM); + sigaddset(&set, sig); + + sigaddset(&set, SIGCHLD); + + if (sigprocmask(SIG_BLOCK, &set, oset) == -1) { warn("%s", SERR); } +} + +/* Handle SIGALRM. */ +static void handle_sigalrm(int sig) { + if (sig == SIGALRM) { tout = true; sig = tosig; } + + /* If we are the child, or the child is not yet born, then exit */ + if (pid == 0) { exit(128 + sig); } + + if (ktime) { + /* Start a new timeout, after which SIGKILL will be sent */ + int ern = errno; tosig = SIGKILL; timer(ktime); ktime = 0; errno = ern; + } + + signal_send(pid, sig); +} + +/* Handle SIGCHLD. */ +static void handle_sigchld(int sig) {} + +/* Send a signal to a process, ignoring signals in the case of a group. */ +static int signal_send(pid_t pid, int sig) { + if (pid == 0) { signal(sig, SIG_IGN); } + return kill(pid, sig); +} + +/* Start the timeout timer, after which we'll recieve a SIGALRM. */ +static void timer(f64 duration) { + struct timespec ts = f64totimespec(duration); + struct itimerspec its = { { 0, 0 }, ts }; timer_t tid; + + /* Create and set a timer with timer_create and timer_settime */ + if (timer_create(CLOCK_REALTIME, NULL, &tid) == 0) { + if (timer_settime(tid, 0, &its, NULL) == 0) { return; } + else { warn("timer_settime: %s", SERR); timer_delete(tid); } + } + else { warn("timer_create: %s", SERR); } + + /* Fallback to alarm() with single second precision upon failure */ + if (duration >= (f64)U32_MAX) { alarm(U32_MAX); } + else { alarm((u32)duration + ((u32)duration < duration)); } +} + +/* Convert an f64 value to a timespec struct. */ +static struct timespec f64totimespec(f64 seconds) { + struct timespec ts; seconds += 0.5e-9; + ts.tv_sec = (time_t)seconds; + ts.tv_nsec = (seconds - ts.tv_sec) * 1.0e+9; + return ts; +} + +/* Print help information. */ +static void hlp(void) { + puts("timeout - Invoke a command with a timelimit\n"); + puts("Usage:"); + puts(" timeout [-ks] duration command [argument...]\n"); + puts("Options:"); + puts(" -k duration Send a KILL signal after the specified duration"); + puts(" -s signal Send the specified signal instead of SIGINT"); + puts(" --help Display help information"); + puts(" --version Display version information"); +} + +/* Print version information. */ +static void ver(void) { + puts("OMKOV cryptutils timeout, version " VERSION); + puts("Copyright (C) 2022, Jakob Wakeling"); + puts("All rights reserved."); + puts("OMKOV Permissive Licence (https://www.omkov.net/OLPE)"); +}