Skip to content

Instantly share code, notes, and snippets.

@NolanDeveloper
Last active March 8, 2024 19:13
Show Gist options
  • Save NolanDeveloper/733eea62d3f3e2daf74af9e7343776e5 to your computer and use it in GitHub Desktop.
Save NolanDeveloper/733eea62d3f3e2daf74af9e7343776e5 to your computer and use it in GitHub Desktop.
Small demo utility functionally similar to timeout(1). It shows how to start child process, pass execv() error to the parent and how to wait child process with timeout.
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
void print_help(const char *program_name) {
printf("Help:\n");
printf("\t%s [--terminate-after SECONDS] [--kill-after SECONDS] -- command with args...\n\n", program_name);
printf("\tRun the bash command with the specified arguments.\n");
printf("\t--terminate-after specifies number of seconds after which SIGTERM is sent to the command.\n");
printf("\t--kill-after specifies number of seconds after which SIGKILL is sent to the command.\n\n");
printf("Example:\n");
printf("\t%s --terminate-after 1 --kill-after 2 -- find / -name 'some-file.txt'\n", program_name);
}
#define DIE(...) die_internal(false, __FILE__, __LINE__, __VA_ARGS__)
#define DIE_ERRNO(...) die_internal(true, __FILE__, __LINE__, __VA_ARGS__)
void die_internal(bool show_errno, const char *file, int line_number, const char *format, ...) {
fprintf(stderr, "ERROR at [%s:%d]: ", file, line_number);
if (show_errno) {
fprintf(stderr, "(errno %d %s) ", errno, strerror(errno));
}
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
_exit(-1);
}
void add_flags(int fd, int flags) {
int result = fcntl(fd, F_GETFD);
if (result < 0) {
DIE_ERRNO("fcntl() failed.\n");
}
int old_flags = result;
result = fcntl(fd, F_SETFD, old_flags | flags);
if (result < 0) {
DIE_ERRNO("fcntl() failed.\n");
}
}
void do_nothing_with_signal(int signal) {
(void) signal;
}
int sigchld_pipe_fds[2];
void handle_sigchld(int signal) {
(void) signal;
char value = 1;
write(sigchld_pipe_fds[1], &value, 1);
}
void setup_wait_child(void) {
int result = pipe(sigchld_pipe_fds);
if (result < 0) {
DIE_ERRNO("pipe() failed.\n");
}
add_flags(sigchld_pipe_fds[0], O_NONBLOCK);
add_flags(sigchld_pipe_fds[1], O_NONBLOCK);
signal(SIGCHLD, &handle_sigchld);
}
bool calculate_time_left(struct timespec end_time, struct timeval *remainig) {
struct timespec now;
int result = clock_gettime(CLOCK_MONOTONIC, &now);
if (result < 0) {
DIE_ERRNO("clock_gettime() failed.\n");
}
remainig->tv_sec = end_time.tv_sec - now.tv_sec;
remainig->tv_usec = (end_time.tv_nsec - now.tv_nsec) / 1000;
if (remainig->tv_usec < 0) {
remainig->tv_sec -= 1;
remainig->tv_usec += 1000000;
}
return remainig->tv_sec > 0 || (remainig->tv_sec == 0 && remainig->tv_usec >= 0);
}
bool wait_child(pid_t pid, struct timespec end_time) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(sigchld_pipe_fds[0], &fds);
for (;;) {
struct timeval remaining;
bool is_there_some_time_left = calculate_time_left(end_time, &remaining);
if (!is_there_some_time_left) {
return false;
}
int result = select(sigchld_pipe_fds[0] + 1, &fds, NULL, NULL, &remaining);
if (result < 0 && errno == EINTR) {
continue;
}
if (result < 0) {
DIE_ERRNO("select() failed.\n");
}
if (result == 0) {
return false;
}
if (result != 1) {
DIE("Unexpected select() result: %d.\n", result);
}
int status;
result = waitpid(pid, &status, WNOHANG);
if (result < 0) {
DIE_ERRNO("waitpid() failed.\n");
}
if (WIFEXITED(status) || WIFSIGNALED(status)) {
return true;
}
}
}
long seconds_from_string(const char *parameter_name, const char *string) {
char *endptr;
int base = 10;
long value = strtol(string, &endptr, base);
if (string == endptr) {
DIE("Parameter %s has an invalid value, \"%s\" is not a number.\n", parameter_name, string);
}
if (value < 0) {
DIE("Parameter %s has a negative value which is not allowed.\n", parameter_name);
}
return value;
}
void parse_arguments(int argc, char *argv[], long *terminate_after, long *kill_after) {
static struct option long_options[] = {
{"terminate-after", required_argument, 0, 't' },
{"kill-after", required_argument, 0, 'k' },
{"help", no_argument, 0, 'h' },
{0, 0, 0, 0 }
};
*terminate_after = INT_MAX;
*kill_after = INT_MAX;
int long_option = 0;
for (;;) {
int result = getopt_long(argc, argv, "t:k:h", long_options, &long_option);
if (result < 0) {
break;
}
char option = result;
switch (option) {
case 't':
*terminate_after = seconds_from_string("--terminate-after", optarg);
break;
case 'k':
*kill_after = seconds_from_string("--kill-after", optarg);
break;
case 'h':
print_help(argv[0]);
exit(0);
case '?':
DIE("Unexpected option: %s\n", argv[optind - 1]);
break;
default:
DIE("getopt_long returned unexpected option: %c\n", option);
}
}
if (*kill_after < *terminate_after) {
*terminate_after = *kill_after;
}
}
void calculate_timeouts(
long terminate_after,
long kill_after,
struct timespec *sigterm_time,
struct timespec *sigkill_time) {
struct timespec now;
int result = clock_gettime(CLOCK_MONOTONIC, &now);
if (result < 0) {
DIE_ERRNO("clock_gettime() failed.\n");
}
*sigterm_time = now;
sigterm_time->tv_sec += terminate_after;
*sigkill_time = now;
sigkill_time->tv_sec += kill_after;
}
pid_t spawn_child(char *argv[]) {
int exec_error_pipe_fds[2];
int result = pipe(exec_error_pipe_fds);
if (result < 0) {
DIE_ERRNO("pipe(exec_error_pipe_fds) failed.\n");
}
add_flags(exec_error_pipe_fds[1], FD_CLOEXEC);
pid_t pid = fork();
if (pid < 0) {
DIE_ERRNO("fork() failed.\n");
}
if (pid == 0) {
close(exec_error_pipe_fds[0]);
result = setpgid(0, 0);
if (result < 0) {
DIE_ERRNO("setpgid() failed.\n");
}
result = execvp(argv[0], argv);
if (result < 0) {
DIE_ERRNO("execvp() failed.\n");
}
_exit(0);
}
close(exec_error_pipe_fds[1]);
char c;
result = read(exec_error_pipe_fds[0], &c, sizeof(c));
if (result < 0) {
DIE_ERRNO("read() failed.\n");
}
if (result != 0) {
DIE("Impossible happened: read() succeeded.\n");
}
return pid;
}
void print_progress(const char *format, ...) {
struct timespec now;
int result = clock_gettime(CLOCK_MONOTONIC, &now);
if (result < 0) {
DIE_ERRNO("clock_gettime() failed.\n");
}
int nsec_per_msec = 1000000;
fprintf(stderr, "[%5lds%4ldms] ", now.tv_sec, now.tv_nsec / nsec_per_msec);
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
void wait_then_sigterm_then_sigkill(pid_t pid,
struct timespec sigterm_time,
struct timespec sigkill_time) {
pid_t group_id = getpgid(pid);
if (group_id < 0) {
DIE("getpgrp() failed.\n");
}
bool is_completed = wait_child(pid, sigterm_time);
if (is_completed) {
return;
}
print_progress("Timeout terminate-after reached. Child has not finished on time. Sending SIGTERM.\n");
int result = kill(-group_id, SIGTERM);
if (result < 0) {
DIE("kill() failed.\n");
}
is_completed = wait_child(pid, sigkill_time);
if (is_completed) {
return;
}
print_progress("Timeout kill-after reached. Child has not finished on time. Sending SIGKILL.\n");
result = kill(-group_id, SIGKILL);
if (result < 0) {
DIE("kill() failed.\n");
}
}
int main(int argc, char *argv[]) {
print_progress("Start\n");
long terminate_after, kill_after;
parse_arguments(argc, argv, &terminate_after, &kill_after);
struct timespec sigterm_time, sigkill_time;
calculate_timeouts(terminate_after, kill_after, &sigterm_time, &sigkill_time);
setup_wait_child();
signal(SIGTERM, do_nothing_with_signal);
signal(SIGINT, do_nothing_with_signal);
pid_t child_pid = spawn_child(argv + optind);
wait_then_sigterm_then_sigkill(child_pid, sigterm_time, sigkill_time);
print_progress("Finish\n");
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment