/*============================================================================ ** COPYRIGHT - VIX IP PTY LTD ("VIX"). ALL RIGHTS RESERVED. **============================================================================ ** ** Project/Product : ** Filename : timeout_unix.c ** Author(s) : DDP ** ** Description : Unix program to limit the wall-clock time that an program can run ** ** Create an other process ** Kill it after it has run for a specified length of time ** ** Used to help JATS to: ** Limit compilation times ** Handle silly compilers that pop up error messages with an OK ** Limit Unit Test run times ** ** Usage : timeout * command * ** Where options are: ** -time:nnn[.nnn][s] - Timeout in seconds ** Floating point number with suffix ** Suffix - s - seconds ** m - minutes ** h - hours ** d - days ** Command - Users command ** command args - Arguments passed to the command ** ** Note: This code MUST be kept in sync with code for all platforms supported ** under JATS ** ** Note: Large chunks lifted from the Linux command of the same name ** See copyright notice below ** ** Information : ** Compiler : ANSI C ** Target : UNIX ONLY ** **==========================================================================*/ /* timeout -- run a command with bounded time Copyright (C) 2008-2015 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #if ! defined(HOST_SOLARIS) #include #endif #include #include #include #include #include "string.h" #include "strings.h" /* ** Exit Codes */ #define EXIT_TIMEDOUT 124 /* job timed out*/ #define EXIT_CANCELED 125 /* internal error*/ #define EXIT_CANNOT_INVOKE 126 /* error executing job */ #define EXIT_ENOENT 127 /* couldn't find job to exec */ static unsigned int timeOut = 10; /* Seconds */ static int monitored_pid; static int timed_out = 0; static int term_signal = SIGTERM; /* same default as kill command. */ static unsigned int kill_after = 5; typedef int bool; #define true 1 #define false 0 /* ** Prototypes (forward declarations) */ /*---------------------------------------------------------------------------- ** FUNCTION : apply_time_suffix ** ** DESCRIPTION : Helper function to process time suffix ** Supports s,h,m,d ** ** INPUTS : x - Ref to value to modify ** suffix_char - Possible suffix char ** ** RETURNS : True - suffix was OK ** ----------------------------------------------------------------------------*/ static int apply_time_suffix (double *x, char suffix_char) { int multiplier; switch (suffix_char) { case 0: case 's': multiplier = 1; break; case 'm': multiplier = 60; break; case 'h': multiplier = 60 * 60; break; case 'd': multiplier = 60 * 60 * 24; break; default: return 0; } *x *= multiplier; return 1; } /*---------------------------------------------------------------------------- ** FUNCTION : parseDuration ** ** DESCRIPTION : Parse the timeout duration ** Allowed - number + 1char suffix ** 1 Char suffix may be: ** 'm' - Minutes ** 'h' - Hours ** 'd' - Days ** The number is an unsigned integer ** ** INPUTS : ptr - Input string (Floating point) ** ** RETURNS : Unsigned integer ** May not return. May exit with error message ** ----------------------------------------------------------------------------*/ static unsigned int parseDuration (const char* str) { double duration; unsigned int intDuration; unsigned int duration_floor; char *ep; int ok = 1; errno = 0; duration = strtod(str, &ep); if (duration < 0 ) ok = 0; if ( ep == str ) ok = 0; if (errno != 0) ok = 0; if (*ep && *(ep + 1)) ok = 0; if(ok) ok = apply_time_suffix (&duration, *ep); if (UINT_MAX <= duration) ok = 0; if(!ok) { fprintf(stderr, "Invalid duration"); exit (EXIT_CANCELED) ; } /* ** Round up - unless exact */ duration_floor = (unsigned int)duration; intDuration = duration_floor + (duration_floor < duration); /*fprintf(stderr, "parseDuration: %f (%u)\n", duration, intDuration);*/ return intDuration; } #if defined(HOST_SOLARIS) /*---------------------------------------------------------------------------- ** FUNCTION : error ** ** DESCRIPTION : Poor mans emulation of the Linux function 'error' ** ** ** INPUTS : status - not used ** errnum - Error number ** format - Format string ** ... - Arguments for format ** ** RETURNS : Nothing ** ----------------------------------------------------------------------------*/ static void error(int status, int errnum, const char *format, ...) { va_list params; va_start(params, format); vfprintf (stderr, format, params); errno = errnum; perror (" "); va_end(params); } #endif /* Try to disable core dumps for this process. Return TRUE if successful, FALSE otherwise. */ static bool disable_core_dumps (void) { #if HAVE_PRCTL && defined PR_SET_DUMPABLE if (prctl (PR_SET_DUMPABLE, 0) == 0) return true; error (0, errno, "warning: disabling core dumps failed"); return false; #elif HAVE_SETRLIMIT && defined RLIMIT_CORE /* Note this doesn't disable processing by a filter in /proc/sys/kernel/core_pattern on Linux. */ if (setrlimit (RLIMIT_CORE, &(struct rlimit) {0,0}) == 0) return true; error (0, errno, "warning: disabling core dumps failed"); return false; #else return false; #endif } static void unblock_signal (int sig) { sigset_t unblock_set; sigemptyset (&unblock_set); sigaddset (&unblock_set, sig); if (sigprocmask (SIG_UNBLOCK, &unblock_set, NULL) != 0) error (0, errno, "warning: sigprocmask"); } /* Start the timeout after which we'll receive a SIGALRM. Round DURATION up to the next representable value. Treat out-of-range values as if they were maximal, as that's more useful in practice than reporting an error. '0' means don't timeout. */ static void settimeout (unsigned int duration, bool warn) { /* We configure timers below so that SIGALRM is sent on expiry. Therefore ensure we don't inherit a mask blocking SIGALRM. */ unblock_signal (SIGALRM); alarm (duration); } /* send SIG avoiding the current process. */ static int send_sig (int where, int sig) { /* If sending to the group, then ignore the signal, so we don't go into a signal loop. Note that this will ignore any of the signals registered in install_signal_handlers(), that are sent after we propagate the first one, which hopefully won't be an issue. Note this process can be implicitly multithreaded due to some timer_settime() implementations, therefore a signal sent to the group, can be sent multiple times to this process. */ if (where == 0) signal (sig, SIG_IGN); return kill (where, sig); } static void cleanup (int sig) { if (sig == SIGALRM) { timed_out = 1; sig = term_signal; } if (monitored_pid) { if (kill_after) { int saved_errno = errno; /* settimeout may reset. */ /* Start a new timeout after which we'll send SIGKILL. */ term_signal = SIGKILL; settimeout (kill_after, false); kill_after = 0; /* Don't let later signals reset kill alarm. */ errno = saved_errno; } /* Send the signal directly to the monitored child, in case it has itself become group leader, or is not running in a separate group. */ send_sig (monitored_pid, sig); /* The normal case is the job has remained in our newly created process group, so send to all processes in that. */ send_sig (0, sig); if (sig != SIGKILL && sig != SIGCONT) { send_sig (monitored_pid, SIGCONT); send_sig (0, SIGCONT); } } else /* we're the child or the child is not exec'd yet. */ _exit (128 + sig); } static void install_signal_handlers (int sigterm) { struct sigaction sa; sigemptyset (&sa.sa_mask); /* Allow concurrent calls to handler */ sa.sa_handler = cleanup; sa.sa_flags = SA_RESTART; /* Restart syscalls if possible, as that's more likely to work cleanly. */ sigaction (SIGALRM, &sa, NULL); /* our timeout. */ sigaction (SIGINT, &sa, NULL); /* Ctrl-C at terminal for example. */ sigaction (SIGQUIT, &sa, NULL); /* Ctrl-\ at terminal for example. */ sigaction (SIGHUP, &sa, NULL); /* terminal closed for example. */ sigaction (SIGTERM, &sa, NULL); /* if we're killed, stop monitored proc. */ sigaction (sigterm, &sa, NULL); /* user specified termination signal. */ } /*---------------------------------------------------------------------------- ** FUNCTION : main ** ** DESCRIPTION : Main entry point ** ** ** INPUTS : argc - Number of args ** argv - Address of args ** ** RETURNS : ** ----------------------------------------------------------------------------*/ int main (int argc, char **argv) { int argp; /* ** Process command line arguments ** Options for this program will be located first */ for ( argp = 1; argp < argc ; argp++ ) { /* ** Scan until first non option argument */ if ( *argv[argp] != '-' ) break; if ( strncasecmp( argv[argp], "-TIME:", 6 ) == 0) { timeOut = parseDuration(argv[argp] + 6); /* fprintf(stderr, "Timeout: %d\n", timeOut); */ } else { fprintf(stderr, "Error: Unknown option: %s\n", argv[argp] ); exit(EXIT_CANCELED); } } /* ** Need at least one more argument - the program to run */ if ( argp >= argc || argc <= 1 ) { fputs("Error: Insufficient number of arguments given\n", stderr); fputs(" Need at least the name of the program\n", stderr); exit(EXIT_CANCELED); } /* Ensure we're in our own group so all subprocesses can be killed. Note we don't just put the child in a separate group as then we would need to worry about foreground and background groups and propagating signals between them. */ setpgid (0, 0); /* Setup handlers before fork() so that we handle any signals caused by child, without races. */ install_signal_handlers (term_signal); signal (SIGTTIN, SIG_IGN); /* Don't stop if background child needs tty. */ signal (SIGTTOU, SIG_IGN); /* Don't stop if background child needs tty. */ signal (SIGCHLD, SIG_DFL); /* Don't inherit CHLD handling from parent. */ monitored_pid = fork (); if (monitored_pid == -1) { error (0, errno, "fork system call failed"); return EXIT_CANCELED; } else if (monitored_pid == 0) { /* Child */ int exit_status; char lBuf[30]; /* Expose the Timeout as an EnvVar */ snprintf(lBuf, sizeof(lBuf), "GBE_JOBLIMIT=%u", timeOut); if (putenv( lBuf )) { error (0, errno, "putenv call failed"); return EXIT_CANCELED; } /* exec doesn't reset SIG_IGN -> SIG_DFL. */ signal (SIGTTIN, SIG_IGN); signal (SIGTTOU, SIG_IGN); execvp (argv[argp], &argv[argp]); /* exit like sh, env, nohup, ... */ exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE; error (0, errno, "failed to run command \"%s\"", argv[argp]); return exit_status; } else { /* Parent */ pid_t wait_result; int status; settimeout (timeOut, true); while ((wait_result = waitpid (monitored_pid, &status, 0)) < 0 && errno == EINTR) continue; if (wait_result < 0) { /* shouldn't happen. */ error (0, errno, "error waiting for command"); status = EXIT_CANCELED; } else { if (WIFEXITED (status)) status = WEXITSTATUS (status); else if (WIFSIGNALED (status)) { int sig = WTERMSIG (status); if (WCOREDUMP (status)) error (0, 0, "the monitored command dumped core"); if (!timed_out && disable_core_dumps ()) { /* exit with the signal flag set. */ signal (sig, SIG_DFL); raise (sig); } status = sig + 128; /* what sh returns for signaled processes. */ } else { /* shouldn't happen. */ error (0, 0, "unknown status from command (%d)", status); status = EXIT_FAILURE; } } if (timed_out) { fprintf(stderr, "Process exceeded time limit (%d Secs)\n", timeOut); status = EXIT_TIMEDOUT; } return status; } }