#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

struct timeout_command {
   unsigned int delay;
   char** argv;
   struct timeout_command* next;
};

static volatile sig_atomic_t alarm_signal = 0;
static void alarm_handler(int signal) {
   alarm_signal = 1;
}

int main(int argc, char** argv) {
   char* cmdname = *argv++; --argc;
   char* usage = "Usage: %s delimiter command "
		 "delimiter { timeout command }\n";
   if (argc <= 5) {
      /* we expect at least two delimiters, a command,
         a timeout and another command */
      fprintf(stderr, usage, cmdname); exit(255);
   }

   /* find command enclosed in delimiters */
   char* delimiter = *argv++; --argc;
   char** command = argv++; --argc;
   while (argc > 0 && strcmp(*argv, delimiter) != 0) {
      --argc; ++argv;
   }
   if (argc == 0) {
      fprintf(stderr, "%s: second delimiter, i.e. \"%s\" is missing\n",
	 cmdname, delimiter);
      fprintf(stderr, usage, cmdname); exit(255);
   }
   /* replace delimiter by a null pointer and skip it */
   *argv++ = 0; --argc;
   if (argc == 0) {
      fprintf(stderr, "%s: timeout command is missing\n", cmdname);
      fprintf(stderr, usage, cmdname); exit(255);
   }

   /* collect timeout commands */
   struct timeout_command* head = 0;
   struct timeout_command* tail = 0;
   while (argc > 0) {
      char* timeout_string = *argv++; --argc;
      char* endptr;
      long int value = strtol(timeout_string, &endptr, 0);
      if (*endptr || value <= 0) {
	 fprintf(stderr, "%s: invalid timeout value: %s\n",
	    cmdname, timeout_string);
	 fprintf(stderr, usage, cmdname); exit(255);
      }
      unsigned int timeout = value;
      if (argc == 0) {
	 fprintf(stderr, "%s: timeout command is missing\n", cmdname);
	 fprintf(stderr, usage, cmdname); exit(255);
      }
      char** tcommand = argv++; --argc;
      while (argc > 0 && strcmp(*argv, delimiter) != 0) {
	 --argc; ++argv;
      }
      if (argc > 0) {
	 /* replace delimiter by a null pointer and skip it */
	 *argv++ = 0; --argc;
      }
      struct timeout_command* cmd = malloc(sizeof(struct timeout_command));
      if (!cmd) {
	 perror("malloc"); exit(255);
      }
      cmd->delay = timeout; cmd->argv = tcommand; cmd->next = 0;
      if (tail) {
	 tail->next = cmd;
      } else {
	 head = cmd;
      }
      tail = cmd;
   }

   /* fork command */
   pid_t pid = fork();
   if (pid < 0) {
      perror("fork"); exit(255);
   }
   if (pid == 0) {
      execvp(command[0], command);
      perror(command[0]); exit(255);
   }

   /* substitute "%" arguments by process id */
   size_t pidlen = snprintf(0, 0, "%d", (int) pid);
   char* pidstr = malloc(pidlen + 1);
   if (!pidstr) {
      perror("malloc"); exit(255);
   }
   snprintf(pidstr, pidlen + 1, "%d", (int) pid);
   for (struct timeout_command* tcmd = head; tcmd; tcmd = tcmd->next) {
      for (char** argp = tcmd->argv; *argp; ++argp) {
	 if (strcmp(*argp, "%") == 0) {
	    *argp = pidstr;
	 }
      }
   }

   /* wait for the process and execute timeout commands */
   int stat; pid_t child;
   do {
      struct timeout_command* cmd = 0;
      if (head) {
	 cmd = head; head = head->next;
	 if (signal(SIGALRM, alarm_handler) == SIG_ERR) {
	    perror("alarm"); exit(255);
	 }
	 alarm_signal = 0;
	 alarm(cmd->delay);
      }
      while ((child = wait(&stat)) > 0 && child != pid);
      if (child < 0) {
	 if (errno == EINTR && alarm_signal) {
	    /* execute timeout command */
	    pid_t timepid = fork();
	    if (timepid < 0) {
	       perror("fork"); exit(255);
	    }
	    if (timepid == 0) {
	       execvp(cmd->argv[0], cmd->argv);
	       perror(cmd->argv[0]); exit(255);
	    }
	 } else {
	    perror("wait"); exit(255);
	 }
      }
      if (cmd) free(cmd);
   } while (child != pid);
   if (WIFEXITED(stat)) {
      exit(WEXITSTATUS(stat));
   } else {
      exit(255);
   }

   /* release storage */
   free(pidstr);
   struct timeout_command* tcmd = head;
   while (tcmd) {
      struct timeout_command* discard = tcmd;
      tcmd = tcmd->next;
      free(discard);
   }
}
