/*
 ****************************************************************************
 * Name:        maxtime
 * Version:     0.35
 * Author:      Rene Uittenbogaard
 * Date:        2004-07-16
 * License:     This program is free software.
 *              It is distributed without any warranty, even without
 *              the implied warranties of merchantability or fitness
 *              for a particular purpose.
 *              You can redistribute it and/or modify it under the terms
 *              described by the GNU General Public License version 2.
 * Usage:       maxtime [-g] [-k] [-v] [-w] [--] <time> <command> [<arg1> ...]
 * Compilation: gcc -o maxtime maxtime.c
 * Required by: setbse
 * Description: Runs external command, but time out after <time> seconds.
 *              Returns its exit code, if possible.
 *
 */

#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>

#define ARGV0           "maxtime"
#define WAITSIGINTERVAL 100000

#define ERR_ARGC        101
#define ERR_ARGS        102
#define ERR_SIGACT      103
#define ERR_FORK        104
#define ERR_EXEC        105
#define ERR_SIGNALED    106
#define ERR_MALLOC      107
#define ERR_FPCONV      108
#define ERR_FPVAL       109
#define ERR_TIMER       110

// #define DEBUG        1

static const char id[] = "@(#) " ARGV0 " 2004-07-16 version 0.35";

struct itimerval *atimer, *oatimer;
float period;
pid_t cpid;
int *cstatus;
int opt_g = 0;
int opt_k = 0;
int opt_v = 0;
int opt_w = 0;
int killed = 0;

/*
 ****************************************************************************
 * signal handler (ALRM)
 */
void cexpired(int signal) {
        int convicts;

        setitimer(ITIMER_REAL, oatimer, NULL);
        killed = 1;
        if (opt_g) convicts = -cpid;
        else       convicts = cpid;
        if (opt_v) fprintf(stderr, ARGV0 ": caught SIGALRM, killing child process %s%d\n",
                (opt_g ? "group " : ""), cpid);
        if (opt_v) fprintf(stderr, ARGV0 ": sending TERM\n");
        if (!kill(convicts, SIGTERM) && opt_k) {
                usleep(WAITSIGINTERVAL);
                if (opt_v) fprintf(stderr, ARGV0 ": sending KILL\n");
                kill(convicts, SIGKILL);
        }
        if (opt_w) {
                if (opt_v) fprintf(stderr, ARGV0 ": waiting for child to finish..\n");
                wait(cstatus);
        }
}

void usage() {
        fprintf(stderr, "Usage: " ARGV0 " [-g] [-k] [-v] [-w] [--] <time> <command> [ <arg1> ... ]\n");
}

/*
 ****************************************************************************
 * main
 */
int main(int argc, char **argv) {
        extern char **environ;
        extern char *optarg;
        extern int optind, opterr, optopt;
        extern int errno;

        struct sigaction *alrm_act, *chld_act;
        int option;
        const char *optstring = "+gkvw"; // the + means: do not permute args

        while ((option = getopt(argc, argv, optstring)) > 0) {
                switch (option) {
                        case 'g': opt_g = 1; break;
                        case 'k': opt_k = 1; break;
                        case 'v': opt_v = 1; break;
                        case 'w': opt_w = 1; break;
                        default : usage();   exit(ERR_ARGS);
                }
        }
        if (argc < optind+2) {
                usage();
                exit(ERR_ARGC);
        }
        if (!sscanf(argv[optind], "%f", &period)) {
                if (opt_v) fprintf(stderr, ARGV0 ": error: alarm period is not a valid number\n");
                exit(ERR_FPCONV);
        }
#ifdef DEBUG
        printf("opts gkvw: %d%d%d%d optind: %d period: %f\n", opt_g, opt_k, opt_v, opt_w, optind, period);
#endif
        if (period <= 0) {
                if (opt_v) fprintf(stderr, ARGV0 ": error: alarm period must be positive\n");
                exit(ERR_FPVAL);
        }
        argv += optind + 1;
        argc -= optind + 1;

        /* set SIGCHLD handler to default NOW:
         * it seems that there are situations in which for user 'root' on HP systems,
         * the default action is to ignore the SIGCHLD signal. This is not what we want.
         */
        if ( !(chld_act = malloc(sizeof(struct sigaction))) ) {
                if (opt_v) perror(ARGV0 ": error: cannot malloc() for alarm handler");
                exit(ERR_MALLOC);
        }
        chld_act->sa_sigaction = NULL;
        chld_act->sa_flags     = 0;
        chld_act->sa_handler   = SIG_DFL;
        if (sigemptyset( &chld_act->sa_mask) && opt_v)
                fprintf(stderr, ARGV0 ": warning: could not empty sigactionmask\n");
        if (sigaction(SIGCHLD, chld_act, NULL) < 0) {
                if (opt_v) perror(ARGV0 ": error: cannot install sigchild handler");
                exit(ERR_SIGACT);
        }

        // now fork
        if ((cpid = fork()) < 0) {
                // fork failed
                if (opt_v) perror(ARGV0 ": error: cannot fork()");
                exit(ERR_FORK);
        } else if (!cpid) {
                // child
                if (opt_g)
//                      if (setpgrp())
                        if (setpgid(0,0)) // works better on HP-UX
                                if (opt_v) perror(ARGV0 ": warning: could not start new process group");
                execvp(argv[0], argv);
                if (opt_v) fprintf(stderr, ARGV0 ": error: exec(%s) failed\n", argv[0]);
                exit(ERR_EXEC);
        } else {
                // parent
                if ( !(alrm_act = malloc(sizeof(struct sigaction))) ) {
                        if (opt_v) perror(ARGV0 ": error: cannot malloc() for alarm handler");
                        exit(ERR_MALLOC);
                }
                alrm_act->sa_sigaction = NULL;
                alrm_act->sa_flags     = 0;
                alrm_act->sa_handler   = cexpired;
                if (sigemptyset( &alrm_act->sa_mask) && opt_v)
                        fprintf(stderr, ARGV0 ": warning: could not empty sigactionmask\n");
                if (sigaction(SIGALRM, alrm_act, NULL) < 0) {
                        if (opt_v) perror(ARGV0 ": error: cannot install alarm handler");
                        exit(ERR_SIGACT);
                }
                if ( !(cstatus = malloc(sizeof(int))) ) {
                        if (opt_v) perror(ARGV0 ": error: cannot malloc() for exit status");
                        exit(ERR_MALLOC);
                }
                if ( !(atimer  = malloc(sizeof(struct itimerval)))
                ||   !(oatimer = malloc(sizeof(struct itimerval)))
                ) {
                        if (opt_v) perror(ARGV0 ": error: cannot malloc() for timer data");
                        exit(ERR_MALLOC);
                }
                atimer->it_interval.tv_sec  = (long) period;
                atimer->it_interval.tv_usec = (long) ((period - (long)period) * 1000000);
                atimer->it_value = atimer->it_interval;
#ifdef DEBUG
                printf("split %f into: %ld s + %ld us\n", period, atimer->it_interval.tv_sec, atimer->it_interval.tv_usec);
#endif
                if (opt_v) fprintf(stderr, ARGV0 ": setting timer to %3.1f seconds\n", period);
                if (setitimer(ITIMER_REAL, atimer, oatimer)) {
                        if (opt_v) perror(ARGV0 ": error: cannot set timer");
                        exit(ERR_TIMER);
                }
                wait(cstatus);
                if (WIFEXITED(*cstatus) && !killed) {
                        if (opt_v) fprintf(stderr, ARGV0 ": child exited with status %d\n", WEXITSTATUS(*cstatus));
                        exit(WEXITSTATUS(*cstatus));
                } else {
                        if (opt_v) fprintf(stderr, ARGV0 ": child was killed\n");
                        exit(ERR_SIGNALED);
                }
        }
}