Subversion Repositories DevTools

Rev

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 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 <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_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_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;
    }
}