Rev 5934 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*============================================================================** Copyright (c) VIX TECHNOLOGY (AUST) LTD**============================================================================**** 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 <options>* command <command args>*** 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 timeCopyright (C) 2008-2015 Free Software Foundation, Inc.This program is free software: you can redistribute it and/or modifyit under the terms of the GNU General Public License as published bythe 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 ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See theGNU General Public License for more details.You should have received a copy of the GNU General Public Licensealong with this program. If not, see <http://www.gnu.org/licenses/>.*/#include <stdio.h>#include <unistd.h>#include <fcntl.h>#include <stdlib.h>#include <sys/wait.h>#if ! defined(HOST_SOLARIS)#include <error.h>#endif#include <errno.h>#include <limits.h>#include <signal.h>#include <stdarg.h>#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_DUMPABLEif (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;#elsereturn 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 thesignals registered in install_signal_handlers(), that are sent after wepropagate the first one, which hopefully won't be an issue. Note thisprocess can be implicitly multithreaded due to some timer_settime()implementations, therefore a signal sent to the group, can be sentmultiple 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 ournewly 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'smore 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 asthen we would need to worry about foreground and background groupsand propagating signals between them. */setpgid (0, 0);/* Setup handlers before fork() so that wehandle 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_DFL);signal (SIGTTOU, SIG_DFL);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;}}