/*
 *  Copyright (C) 2006-2019, Thomas Maier-Komor
 *
 *  This is the source code of xjobs.
 *
 *  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 2 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 "config.h"

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifdef HAVE_STROPTS_H
#include <stropts.h>
#endif

#ifdef HAVE_WAIT4
#include <sys/resource.h>
#endif


/* SUSv3 does not have PATH_MAX */
#ifndef PATH_MAX
#define PATH_MAX _XOPEN_PATH_MAX
#endif

#if __STDC_VERSION__ < 199901L
#define inline
#endif

#ifndef SIGPOLL
#define SIGPOLL SIGIO
#endif

#include "log.h"
#include "tokens.h"
#include "support.h"
#include "jobctrl.h"
#include "settings.h"
#include "colortty.h"


extern int yylex(void);

char **Env;
int ExitCode = EXIT_SUCCESS, InputsWaiting = 0, Terminate = 0;
volatile int ChildTerminated = 0;

static unsigned long long Start = 0;
static pid_t PGID = 0;
static int Started = 0;


RETSIGTYPE processSignal(int sig)
{
	switch (sig) {
	case SIGPOLL:
		dbug("SIGPOLL\n");
		++InputsWaiting;
		break;
	case SIGTERM:
		dbug("SIGTERM\n");
		Terminate = 2;
		ExitCode = EXIT_FAILURE;
		break;
	case SIGINT:
		dbug("SIGINT\n");
		if (++Terminate == 2) {
			if (TiNoAttr && TiNoAttr[0])
				Write(STDOUT_FILENO,TiNoAttr,strlen(TiNoAttr));
			_exit(EXIT_FAILURE);
		}
		break;
	case SIGCHLD:
		dbug("SIGCHLD\n");
		++ChildTerminated;
		break;
	default:
		abort();
	}
}


void finalize(void)
{
	while (Executing) {
		pid_t pid;
		int ret;
#ifdef HAVE_WAIT4
		struct rusage rusage;
		pid = wait4(0,&ret,0,&rusage);
		if ((pid == -1) && (errno == EINTR))
			continue;
		clear_job(pid,ret,&rusage);
#else
		pid = wait(&ret);
		if ((pid == -1) && (errno == EINTR))
			continue;
		clear_job(pid,ret,0);
#endif
		if (pid == -1) {
			warn("error waiting for children: %s\n",strerror(errno));
			ExitCode = EXIT_FAILURE;
			break;
		}
	}
	if (Started) {
		if ((Verbose < Info) && (TiClrEolL > 0)) {
			Write(STDERR_FILENO,TiClrEol,TiClrEolL);
		} else {
			char dt[64];
			timestr(gettime()-Start,dt,sizeof(dt));
			if (Failed)
				info("summary: %s%u failed, %s%u succeeded, %sexecuted in %s\n"
					,PfxFail,Failed,PfxDone,Finished-Failed,TiNoAttr,dt);
			else
				info("summary: %s%u succeeded, %sexecuted in %s\n"
					,PfxDone,Finished-Failed,TiNoAttr,dt);
		}
	}
	close(STDOUT_FILENO);
	close(STDERR_FILENO);
	if (PgPid) {
		int st;
		waitpid(PgPid,&st,0);
	}
	exit(ExitCode);
}


void parse_input(const char *a0, const char *a1)
{
	const char *fn = 0;
	if (a0[1]) {
		fn = a0+1;
	} else if (a1) {
		++optind;
		fn = a1;
	} else {
		error("missing argument for input redirector\n");
	}
	if (strstr(fn,"$(jobid)")) {
		if (JobIn)
			error("unable to handle duplicate job input\n");
		dbug("setting common jobin to %s\n",fn);
		JobIn = fn;
	} else {
		if (Stdin != -1)
			error("unable to handle duplicate input\n");
		Stdin = open(fn,O_RDONLY);
		if (-1 == Stdin)
			error("could not open default stdin for jobs (%s): %s\n",fn,strerror(errno));
	}
}


void parse_args(int argc, char **argv)
{
	while (optind < argc) {
		char *arg = argv[optind];
		char *a = arg;
		int lt = 0, gt = 0, as = 0, e = 0;
		do {
			if (*a == '<')
				++lt;
			else if (*a == '>')
				++gt;
			else if (*a == '&')
				++as;
			else 
				e = 1;
			++a;
		} while ((e == 0) && (*a));
		if (lt && gt)
			error("syntax error in argument '%s'\n",arg);
		if (lt > 1)
			error("invalid use of operator < in argument '%s'\n",arg);
		if (gt > 2)
			error("invalid use of operator > in argument '%s'\n",arg);
		if (as > 1)
			error("invalid use of operator & in argument '%s'\n",arg);
		if ((gt == 0) && (as == 1))
			error("invalid use of operator & in argument '%s'\n",arg);
		if (as && lt)
			error("invalid combination of operators & and < in argument '%s'\n",arg);
		char *value = (*a) ? a : argv[++optind];
		if (lt == 1) {
			parse_input(arg,value);
		} else if (gt > 0) {
			int flags = 0;
			int mode = 0;
			if (gt == 1) {
				// >x
				flags = O_WRONLY|O_CREAT|O_EXCL;
				mode = 0666;
			} else if (gt == 2) {
				// >>x
				flags = O_WRONLY|O_APPEND;
			}
			Stdout = open(value,flags,mode);
			if (as) {
				// >>& or >&
				Stderr = Stdout;
			}
			dbug("output set to %s\n",value);
		} else {
			arg_t *a = Malloc(sizeof(arg_t));
			a->arg = argv[optind];
			add_basearg(a);
		}
		++optind;
	}
}


static void wait_and_clear(int h)
{
	int ret;
	int flags = 0;
	if ((h == 0) && ((QLen == 0) || (Waiting < QLen)))
		flags = WNOHANG;
	display_status();
#ifdef HAVE_WAIT4
	if (RsrcUsage) {
		struct rusage rusage;
		pid_t pid = wait3(&ret,flags,&rusage);
		if (0 < pid) 
			clear_job(pid,ret,&rusage);
		else
			dbug("wait4(%u,%u,%x): %s\n",Waiting,QLen,flags,strerror(errno));
	} else
#endif
	{
#if _POSIX_C_SOURCE >= 200809L
		siginfo_t si;
		if ((0 == waitid(P_PGID,PGID,&si,flags|WEXITED)) && (si.si_pid != 0))
			clear_job(si.si_pid,si.si_status,0);
#else
		pid_t pid = waitpid(0,&ret,flags);
		if ((pid > 0) && WIFEXITED(ret))
			clear_job(pid,WEXITSTATUS(ret),0);
#endif
	}
}


int main(int argc, char **argv, char **env)
{
	int i, flags = 0, ret;
	unsigned long lcount = 0;
	char *jobin = 0, *jobout = 0, *joberr = 0, *jobpwd = 0;
	arg_t *args = 0;
	size_t numargs = 0;
	struct sigaction sig;
	token_t t;

	Start = gettime();
	for (i = 1; i < argc; ++i) {
		const char *arg = argv[i];
		if ((arg[0] != '-') || (arg[1] != 'v'))
			continue;
		const char *verbose = 0;
		if (arg[2])
			verbose = arg+2;
		else if ((++i == argc) || (argv[i][0] == '-'))
			error("missing argument to option -v\n");
		else
			verbose = argv[i];
		if ((verbose[0] >= '0') && (verbose[0] <= '5') && (verbose[1] == 0))
			Verbose = verbose[0] - '0';
		else
			error("invalid argument for option -v\n");
	}
	Env = env;
	setenv("POSIXLY_CORRECT","1",1);
	init_limit();
	init_defaults(argv[0]);
	parse_options(argc,argv);
	parse_args(argc,argv);
	init_terminal();

	if (Stdin == -1) {	/* no default input redirector */
		Stdin = open("/dev/null",O_RDONLY);
		if (Stdin == -1)
			warn("could not open /dev/null: %s\n",strerror(errno));
	}

	close_onexec(Stdin);
	close_onexec(Stdout);
	close_onexec(Stderr);
	PGID = getpgid(getpid());

	sig.sa_handler = processSignal;
	sigemptyset(&sig.sa_mask);
	sigaddset(&sig.sa_mask,SIGTERM);
	sig.sa_flags = 0;
	ret = sigaction(SIGTERM,&sig,0);
	assert(ret == 0);
	sigemptyset(&sig.sa_mask);
	sigaddset(&sig.sa_mask,SIGINT);
	ret = sigaction(SIGINT,&sig,0);
	assert(ret == 0);
	sigemptyset(&sig.sa_mask);
	sigaddset(&sig.sa_mask,SIGCHLD);
	ret = sigaction(SIGCHLD,&sig,0);
	assert(ret == 0);
	Started = 1;
	while ((Terminate == 0) && (0 != (t = gettoken()) || Script)) {
		switch (t) {
		default:
			abort();
			break;
		case CD:
			jobpwd = Strdupn(Buffer,Fill+1);
			dbug("CD %s\n",jobpwd);
			continue;
		case SEQUPT:
			{
				job_t *j = Malloc(sizeof(job_t));
				j->args = 0;
				if (ReadyEnd)
					ReadyEnd->next = j;
				else
					Ready = j;
				ReadyEnd = j;
				continue;
			}
		case INPUT:
			t = yylex();
			if ((t != UQUOTED) && (t != DQUOTED) && (t != SQUOTED)) {
				warn("invalid argument for input redirector <\n");
				do t = yylex(); while (t && (t != EOL));
			} else {
				jobin = Strdupn(Buffer,Fill+1);
			}
			continue;
		case APPEND:
			flags |= 1;
			/*FALLTHROUGH*/
		case OUTPUT:
			t = yylex();
			if ((t != UQUOTED) && (t != DQUOTED) && (t != SQUOTED)) {
				warn("invalid argument for output redirector >\n");
				do t = yylex(); while (t && (t != EOL));
			} else {
				jobout = Strdupn(Buffer,Fill+1);
			}
			continue;
		case OVWROUTPUT:
			flags |= 4;
			t = yylex();
			if ((t != UQUOTED) && (t != DQUOTED) && (t != SQUOTED)) {
				warn("invalid argument for output redirector >!\n");
				do t = yylex(); while (t && (t != EOL));
			} else {
				jobout = Strdupn(Buffer,Fill+1);
			}
			continue;
		case APPERR:
			flags |= 3;
			/*FALLTHROUGH*/
		case ERRLOG:
			t = yylex();
			if ((t != UQUOTED) && (t != DQUOTED) && (t != SQUOTED)) {
				warn("invalid argument for log redirector >&\n");
				do t = yylex(); while (t && (t != EOL));
			} else {
				joberr = Strdupn(Buffer,Fill+1);
			}
			continue;
		case UQUOTED:
		case DQUOTED:
		case SQUOTED:
			{
				arg_t *a = ArgCache;
				if (a)
					ArgCache = a->prev;
				else
					a = Malloc(sizeof(arg_t));
				a->arg = Strdupn(Buffer,Fill+1);
				a->prev = args;
				args = a;
				++numargs;
				continue;
			}
		case EOL:
			if (++lcount < Lines)
				break;
			if (numargs || jobin || jobout || joberr) {
				add_job(args,numargs,jobin,jobout,joberr,jobpwd,flags);
				display_status();
			}
			args = 0;
			numargs = 0;
			jobin = 0;
			jobout = 0;
			joberr = 0;
			jobpwd = 0;
			flags = 0;
			lcount = 0;
			if (QLen && (QLen >= Waiting))
				break;
			continue;
		case LEXERR:
			warn("ignoring erroneous line\n");
			continue;
		case 0:
			dbug("token: 0\n");
			break;
		}

		int n = start_jobs();
		if ((n != 0) && ((QLen == 0) || (Waiting < QLen)))
			continue;
		if ((QLen > 0) && (Waiting >= QLen))
			wait_and_clear(0);
		if (ChildTerminated) {
			int ret;
			struct sigaction orig;
			--ChildTerminated;
			if (InputsWaiting == 0) {
				dbug("wait blocking - listening for new pipe connection\n");
				sig.sa_handler = processSignal;
				sigemptyset(&sig.sa_mask);
				sigaddset(&sig.sa_mask,SIGPOLL);
				sig.sa_flags = 0;
				ret = sigaction(SIGPOLL,&sig,&orig);
				assert(ret == 0);
			}
			wait_and_clear(0);
			if (InputsWaiting == 0) {
				ret = sigaction(SIGPOLL,&orig,0);
				assert(ret == 0);
			}
			start_jobs();
		}
	}
	if (numargs) {
		add_job(args,numargs,jobin,jobout,joberr,jobpwd,flags);
		display_status();
	}

	while ((Executing || Ready) && (Terminate == 0)) {
		start_jobs();
		wait_and_clear(1);
	}
	finalize();
}

/* vim:tw=0
 */
