/*
 * (c) 2006-2017 Thomas Maier-Komor
 * LICENSE: GPLv2, see file COPYING for details
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_ALLOCA_H
#include <alloca.h>
#endif
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.h>
#include <setjmp.h>
#include <signal.h>
#ifdef HAVE_SPAWN
#include <spawn.h>
#endif
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#ifdef HAVE_STROPTS_H
#include <stropts.h>
#endif
#ifdef HAVE_MMAP
#include <sys/mman.h>
#endif
#ifdef HAVE_WAIT4
#include <sys/resource.h>
#endif
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#ifdef HAVE_VFORK_H
#include <vfork.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

#ifdef __OpenBSD__
#include <sys/param.h>
#include <sys/sysctl.h>
#endif

#include "log.h"
#include "tokens.h"


typedef struct arg_s {
	char *arg;
	struct arg_s *prev;
} arg_t;

typedef struct job_s {
	pid_t pid;
	unsigned job;
	char **args;
	size_t numargs;
	char *in, *out, *err, *cmd, *pwd;
	int outfile;
	unsigned flags;	/* 1 = stdout, 2 = stderr, 3 = both, 4 = overwrite */
	unsigned long long start;
	struct job_s *next;
} job_t;

extern int yylex(void);
extern verbose_t Verbose;
extern const char *Buffer;
extern size_t Fill;

static arg_t *Baseargs = 0;
static size_t Numbaseargs = 0, Running = 0, Waiting = 0, Pagesize;
static long Limit = 0;
job_t *Executing = 0;
static job_t *Ready = 0, *ReadyEnd = 0, *JobCache = 0;
static arg_t *ArgCache = 0;
static int Stdout = -1, Stderr = -1, Stdin = -1, Prompt = 0, Terminate = 0,
	   ExitCode = EXIT_SUCCESS, ExitOnError = 0, InFlags = 0,
	   Input = STDIN_FILENO, RsrcUsage = 1,
	   Parsed = 0, Skipped = 0, Finished = 0;
static unsigned long long Start = 0;
static unsigned Jid = 0;
static char *Path, **Env;
static const char *Script = 0;
static volatile int InputsWaiting = 0, ChildTerminated = 0;
static pid_t PGID = 0;
static int (*gettoken)(void) = yylex;


static 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)
			exit(EXIT_FAILURE);
		break;
	case SIGCHLD:
		dbug("SIGCHLD\n");
		++ChildTerminated;
		break;
	default:
		abort();
	}
}


int input_data(char *buf, size_t max)
{
	int n, ret, trys = 1;

	do {
#if __CYGWIN__
		if (-1 == Input) {
			dbug("yyinput(): reopening input\n");
			Input = open(Script,O_RDONLY);
			if (Input == -1)
			{
				warn("unable to reopen named pipe %s: %s\n",Script,strerror(errno));
				return 0;
			}
			ret = fcntl(Input,F_SETOWN,getpid());
			if (ret != 0)
				warn("unable to set owning process for SIGPIPE of named pipe %s: %s\n",Script,strerror(errno));
			ret = fcntl(Input,F_SETFL,InFlags);
			assert(ret == 0);
		}
		ret = fcntl(Input,F_SETOWN,getpid());
		if (ret != 0)
			warn("unable to set owning process for SIGPIPE of named pipe %s: %s\n",Script,strerror(errno));
		ret = fcntl(Input,F_SETFL,InFlags);
		assert(ret == 0);
#endif
		if (Script) {
			if ((Running == 0) && (Waiting == 0)) {
				if (InFlags & O_NONBLOCK) {
					dbug("yyinput(): block on input\n");
					InFlags &= ~O_NONBLOCK;
					ret = fcntl(Input,F_SETFL,InFlags);
					assert(ret == 0);
				}
			} else {
				if ((InFlags & O_NONBLOCK) == 0) {
					dbug("yyinput(): read non-block\n");
					InFlags |= O_NONBLOCK;
					ret = fcntl(Input,F_SETFL,InFlags);
					assert(ret == 0);
				}

			}
		}
		dbug("yyinput(): read\n");
		assert(Input != -1);
		n = read(Input,buf,max);	
		if (0 == n) {
			dbug("yyinput(): shortread\n");
			if (InputsWaiting > 0)
				--InputsWaiting;
#if __CYGWIN__
			(void) close(Input);
			Input = -1;
			++ChildTerminated;
#endif
		} else if (-1 == n) {
			if (InputsWaiting > 0)
				--InputsWaiting;
			if ((errno != EAGAIN) && (errno != EINTR)) {
				warn("error reading input: %s\n",strerror(errno));
				close(Input);
				Input = -1;
			} else {
				dbug("input unavailable: %s\n",strerror(errno));
				++trys;
			}
			n = 0;
		} else {
			dbug("yyinput() = %d\n",n);
			return n;
		}
	} while (--trys);
	dbug("yyinput() = -- bailout\n",n);
	return 0;
}


int yywrap(void)
{
	return 1;
}


static void *Malloc(size_t s)
{
	int tries = 0, time = 5;
	for (;;) {
		void *r = malloc(s);
		if (r != 0)
			return r;
		warn("out of memory - retrying in %d seconds\n",time);
		(void) sleep(time);
		time += 5;
		++tries;
		if (tries == 10) {
			warn("Persisiting low memory situation: stopping.\n"
				"Please send SIGCONT to contiue.\n");
			if (-1 == raise(SIGSTOP))
				abort();
			time = 5;
			tries = 0;
		}
	}
}


#ifdef __sun
#define gettime() (gethrtime()/1000)
#else
static inline unsigned long long gettime(void)
{
	struct timeval now;
	
	(void) gettimeofday(&now,0);
	return now.tv_sec * 1000000 + now.tv_usec;
}
#endif


static inline void close_onexec(int f)
{
	if (f != -1) {
		int err = fcntl(f,F_SETFD,FD_CLOEXEC);
		assert(err != -1);
	}
}


static inline char *Strdupn(const char *s, size_t n)
{
	assert(n);
	return memcpy(Malloc(n),s,n);
}


static void name_job(job_t *j)
{
	char **a = j->args, *c;
	size_t l = 0;
	if (j->cmd)
		return;
	assert(a);
	do {
		l += strlen(*a);
		++l;
	} while (*++a);
	j->cmd = c = Malloc(l);
	a = j->args;
	do {
		c = (char *) memccpy(c,*a,0,l);
		*--c = ' ';
		++c;
	} while (*++a);
	--c;
	*c = 0;
}


static int ask_exec(job_t *j)
{
	int r;
	char ans[128];
	if (j->pid == -1)
		return 1;
	if (0 == Prompt)
		return 0;
	name_job(j);
	r = write(STDERR_FILENO,"start job '",11);
	if (-1 == r)
		warn("cannot write to terminal: %s\n",strerror(errno));
	r = write(STDERR_FILENO,j->cmd,strlen(j->cmd));
	if (-1 == r)
		warn("cannot write to terminal: %s\n",strerror(errno));
	r = write(STDERR_FILENO,"'? (hit y for yes, n for no)\n",29);
	if (-1 == r)
		warn("cannot write to terminal: %s\n",strerror(errno));
	do {
		r = read(STDERR_FILENO,ans,sizeof(ans));
		dbug("read stderr: %d : %s\n",r,ans);
		if ((-1 == r) && (errno == EINTR))
			continue;
		if (-1 == r) {
			warn("cannot read from terminal: %s\n",strerror(errno));
			j->pid = 0;	/* default to do not execute */
			return 1;
		}
		if ((0 == strncasecmp(ans,"n\n",2)) || (0 == strncasecmp(ans,"no\n",3))) {
			r = write(STDERR_FILENO,"no\n",3);
			if (-1 == r)
				warn("cannot write to terminal: %s\n",strerror(errno));
			j->pid = 0;
			return 1;
		}
	} while ((0 != strncasecmp(ans,"y\n",2)) && (0 != strncasecmp(ans,"yes\n",4)));
	r = write(STDERR_FILENO,"yes\n",4);
	if (-1 == r)
		warn("cannot write to terminal: %s\n",strerror(errno));
	return 0;
}


static void exec_job(job_t *j)
{
	static unsigned job = 0;
	int in = -1, out = -1, err = -1, log = -1;
	char pwd[PATH_MAX];
#ifdef HAVE_SPAWN
	posix_spawn_file_actions_t acts;

	ret = posix_spawn_file_actions_init(&acts);
	assert(ret == 0);
#endif
	if (ask_exec(j))
		return;
	if (j->pwd) {
		char *p = getcwd(pwd,sizeof(pwd));
		assert(p);
		if (-1 == chdir(j->pwd)) {
			warn("cannot change directory to %s: %s\n",j->pwd,strerror(errno));
			j->pid = -1;
			goto finish;
		}
	} else
		pwd[0] = 0;
	++Jid;
	j->start = gettime();
	if (j->in) {	/* stdin is redirected */
		in = open(j->in,O_RDONLY,0666);
		if (in == -1) {
			warn("cannot open input file %s: %s\n",j->in,strerror(errno));
			j->pid = -1;
			return;
		}
		dbug("job input redirected to %s\n",j->in);
	} else {
		in = Stdin;
	}
	if (j->err) {	/* stdout and stderr are redirected */
		int mode;
	       	if (j->flags & 2)
			mode = O_WRONLY|O_CREAT|O_APPEND;
		else if (j->flags & 4)
			mode = O_WRONLY|O_CREAT|O_TRUNC;
		else
			mode = O_WRONLY|O_CREAT|O_TRUNC|O_EXCL;
		err = open(j->err,mode,0666);
		if (err == -1) {
			warn("cannot open error output file %s: %s\n",j->err,strerror(errno));
			j->pid = -1;
			goto finish;
		}
		out = err;
		dbug("job error output redirected to %s\n",j->err);
	} else if (Stdout != -1) {
		err = Stderr;
	}
	if (j->out) {	/* output is redirected */
		int mode;
	       	if (j->flags & 1)
			mode = O_WRONLY|O_CREAT|O_APPEND;
		else if (j->flags & 4)
			mode = O_WRONLY|O_CREAT|O_TRUNC;
		else
			mode = O_WRONLY|O_CREAT|O_TRUNC|O_EXCL;
		out = open(j->out,mode,0666);
		if (out == -1) {
			warn("cannot open output file %s: %s\n",j->out,strerror(errno));
			j->pid = -1;
			goto finish;
		}
		dbug("job output redirected to %s\n",j->out);
	} else if (Stdout != -1) {
		out = Stdout;
	}
	if (err == -1) {
		char fname[] = "/tmp/xjobs-XXXXXX";
		err = mkstemp(fname);
		if (-1 == err) {
			warn("cannot create intermediate output file %s: %s\n",fname,strerror(errno));
			j->pid = -1;
			goto finish;
		}
		dbug("stdout/stderr will be written to %s\n",fname);
		j->outfile = err;
		close_onexec(err);
		log = err;
		if (-1 == unlink(fname))
			warn("unlinking of temporary file %s failed: %s\n",fname,strerror(errno));
		if (out == -1)
			out = err;
	}
	
#ifdef HAVE_SPAWN
	ret = posix_spawn_file_actions_adddup2(&acts,err,STDERR_FILENO);
	assert(ret == 0);
	ret = posix_spawn_file_actions_adddup2(&acts,out,STDOUT_FILENO);
	assert(ret == 0);
	ret = posix_spawn_file_actions_adddup2(&acts,in,STDIN_FILENO);
	assert(ret == 0);
	if (posix_spawn(&j->pid,j->args[0],&acts,0,j->args,Env) == 0) {
		j->job = ++job;
		name_job(j);
		info("### started job #%d, pid %ld: %s\n",job,j->pid,j->cmd);
		j->next = Executing;
		Executing = j;
		dbug("started job, %d jobs waiting, %d running\n",--Waiting,++Running);
	} else {
		warn("couldn't spawn job %s: %s\n",j->cmd,strerror(errno));
		j->pid = -1;
	}
#elif defined(HAVE_WORKING_VFORK)
	j->pid = vfork();
	if (j->pid == -1) {
		warn("error could not vfork: %s\n",strerror(errno));
		goto finish;
	}
	if (j->pid != 0) {
		j->job = ++job;
		name_job(j);
		info("### started job #%d, pid %ld: %s\n",job,j->pid,j->cmd);
		j->next = Executing;
		Executing = j;
		dbug("started job, %d jobs waiting, %d running\n",--Waiting,++Running);
	} else {
		int r;
		char msg[256];
		if (in != -1)
			(void) dup2(in,STDIN_FILENO);
		if (out != -1)
			(void) dup2(out,STDOUT_FILENO);
		if (err != -1)
			(void) dup2(err,STDERR_FILENO);
		r = execve(j->args[0],j->args,Env);
		assert(r == -1);
		sprintf(msg,"failed to execute %s: %s\n",j->args[0],strerror(errno));
		r = write(STDERR_FILENO,msg,strlen(msg));
		assert(r != -1);
		_exit(EXIT_FAILURE);
	}
#else
	j->pid = fork();
	if (j->pid == -1) {
		warn("error could not fork: %s\n",strerror(errno));
		goto finish;
	}
	if (j->pid != 0) {
		j->job = ++job;
		name_job(j);
		info("### started job #%d, pid %ld: %s\n",job,j->pid,j->cmd);
		j->next = Executing;
		Executing = j;
		dbug("started job, %d jobs waiting, %d running\n",--Waiting,++Running);
	} else {
		int r;
		if (in != -1) {
			if (-1 == dup2(in,STDIN_FILENO))
				warn("unable to redirect stdin: %s\n",strerror(errno));
		}
		if (out != -1) {
			if (-1 == dup2(out,STDOUT_FILENO))
				warn("unable to redirect stdout: %s\n",strerror(errno));
		}
		if (err != -1) {
			if (-1 == dup2(err,STDERR_FILENO))
				warn("unable to redirect stderr: %s\n",strerror(errno));
		}
		r = execve(j->args[0],j->args,Env);
		assert(r == -1);
		warn("failed to execute %s: %s\n",j->args[0],strerror(errno));
		exit(EXIT_FAILURE);
	}
#endif

finish:
#ifdef HAVE_SPAWN
	(void) posix_spawn_file_actions_destroy(&acts);
#endif
	if ((in != -1) && (in != Stdin))
		(void) close(in);
	if ((out != -1) && (out != Stdout) && (out != log))
		(void) close(out);
	if ((err != -1) && (err != Stderr) && (err != out) && (err != log))
		(void) close(err);
	if (pwd[0]) {
		int r = chdir(pwd);
		if (r == -1)
			warn("unable to change back to working directory %s: %s\n",pwd,strerror(errno));
	}

}


static int resolve_symlink(char *cmd)
{
	char lt[PATH_MAX+1];
	ssize_t l;
	/* handle symlinks */
	bzero(lt,sizeof(lt));
	while (-1 != (l = readlink(cmd,lt,PATH_MAX))) {
		char *base = cmd;
		lt[l] = '\0';
		dbug("is symlink: %s\n",lt);
		/* handle relative links */
		if (lt[0] != '/') {
			base = strrchr(cmd,'/');   
			if (base == 0) {
				base = cmd;
			} else if ((l + (base - cmd)) > PATH_MAX) {
				warn("command exceed PATH_MAX limit of your system\n");
				return -1;
			}
			++base;
		}
		(void) memcpy(base,lt,l+1);
		dbug("resolved to: %s\n",cmd);
	}
	return 0;
}


static char *complete_exe(char *exe)
{
	typedef struct cache_s {
		struct cache_s *next;
		char *file, exe[PATH_MAX];
	} cache_t;
	static cache_t *cache = 0;
	cache_t *c = cache;
	char *token, *next;
	size_t exel;

	while (c) {
		if (0 == strcmp(exe,c->exe)) {
			if (c->file == c->exe)
				return exe;
			return strdup(c->file);
		}
		c = c->next;
	}
	if ((exe[0] == '/') && (0 == access(exe,X_OK))) {
		c = Malloc(sizeof(cache_t));
		strcpy(c->exe,exe);
		c->file = c->exe;
		c->next = cache;
		cache = c;
                return exe;
	}
	exel = strlen(exe);
	if ((strncmp(exe,"./",2) == 0) || (strncmp(exe,"../",3) == 0)) {
		if (0 == access(exe,X_OK)) {
			c = Malloc(sizeof(cache_t));
			strcpy(c->exe,exe);
			c->file = c->exe;
			c->next = cache;
			cache = c;
			return exe;
		}
		if (Verbose == Debug) {
			char cmd[PATH_MAX+1];
			assert(exel < sizeof(cmd));
			(void) memcpy(cmd,exe,exel);
			(void) resolve_symlink(cmd);
		}
		return 0;
	}
	token = Path;
	next = strchr(Path,':');
        while (token && *token) {
		char cmd[PATH_MAX];
		size_t off;

		if (next)
			*next = 0;
		if ((off = strlen(token)) >= PATH_MAX) {
			warn("command exceed PATH_MAX limit of your system\n");
			return 0;
		}
		(void) memcpy(cmd, token, off+1);
		if (cmd[off-1] != '/') {
			cmd[off] = '/';
			++off;
		}
		if (exel + off >= PATH_MAX) {
			warn("command exceed PATH_MAX limit of your system\n");
			return 0;
		}
		(void) memcpy(cmd+off, exe, exel+1);
		if (next) {
			*next = ':';
			token = next + 1;
			next = strchr(token,':');
		} else
			token = 0;
		dbug("candidate: %s\n",cmd);
		if ((Verbose == Debug) && resolve_symlink(cmd))
			continue;
		if (0 == access(cmd,X_OK)) {
			dbug("using: %s\n",cmd);
			c = Malloc(sizeof(cache_t));
			strcpy(c->exe,exe);
			c->file = (char *) strdup(cmd);
			c->next = cache;
			cache = c;
                        return strdup(cmd);
		}
        }
        return 0;
}


static int start_jobs(void)
{
	int num = 0;
	while ((Running < Limit) && Ready) {
		job_t *j = Ready;
		if ((j->args == 0) && (Running > 0)) {
			/* sequence point */
			dbug("sequence point - waiting for all processes to finish\n");
			return 0;
		}
		Ready = j->next;
		if (Ready == 0)
			ReadyEnd = 0;
		if (j->args) {
			exec_job(j);
			free(j->out);
			free(j->err);
			free(j->in);
			if (j->pid == 0) {
				warn("### skipping job: %s\n", j->cmd ?  j->cmd : j->args[0]);
				++Skipped;
			} else if (j->pid == -1) {
				char **args;
				warn("### error starting job: %s\n", j->cmd ?  j->cmd : j->args[0]);
				ExitCode = EXIT_FAILURE;
				args = &j->args[Numbaseargs];
				while (*args) {
					free(*args);
					++args;
				}
				free(j->cmd);
				free(j->args);
				assert(j != JobCache);
				j->next = JobCache;
				JobCache = j;
			}
		} else {
			dbug("starting jobs after sequence point\n");
			assert(j != JobCache);
			j->next = JobCache;
			JobCache = j;
		}
		++num;
	}
	return num;
}


static void add_job(arg_t *a, size_t n, char *in, char *out, char *err, char *pwd, int app)
{
	char **argv, *exe;
	int i = n + Numbaseargs + 1;
	job_t *j;

	dbug("add_job(%p,%d,\"%s\",\"%s\",\"%s\",\"%s\",%d)\n",a,n,in?in:"",out?out:"",err?err:"",pwd?pwd:"",app);
	assert(a || n == 0);
	j = JobCache;
	if (j)
		JobCache = j->next;
	else
		j = Malloc(sizeof(job_t));
	argv = Malloc(sizeof(char *) * i);
	j->in = in;
	j->err = err;
	j->out = out;
	j->pwd = pwd;
	j->flags = app;
	j->outfile = -1;
	j->args = argv;
	j->pid = 0;
	j->next = 0;
	j->cmd = 0;
	--i;
	argv[i] = 0;
	j->numargs = i;
	while (n) {
		arg_t *c = a;
		assert(a);
		a = c->prev;
		--n;
		--i;
		argv[i] = c->arg;
		c->prev = ArgCache;
		ArgCache = c;
	}
	assert(a == 0);
	a = Baseargs;
	while (i) {
		arg_t *c = a;
		assert(a);
		a = c->prev;
		--i;
		argv[i] = c->arg;
	}
	assert(a == 0);
	exe = complete_exe(argv[0]);
	if (0 == exe) {
		warn("cannot find executable %s\n",argv[0]);
		j->pid = -1;
		exe = argv[0];
	}
	if ((argv[0] != exe) && (Numbaseargs == 0))
		free(argv[0]);
	argv[0] = exe;
	if (Verbose >= Debug) {
		if (in)
			dbug("stdin: %s\n",in);
		if (out)
			dbug("stdout: %s\n",out);
		if (err)
			dbug("stderr: %s\n",err);
		while (*argv) {
			dbug("argument %ld: <%s>\n",(long)(argv - j->args), *argv);
			++argv;
		}
	}
	j->next = 0;
	if (ReadyEnd)
		ReadyEnd->next = j;
	else
		Ready = j;
	ReadyEnd = j;
	dbug("%d jobs still waiting in the queue\n",++Waiting);
	++Parsed;
	start_jobs();
	if (Ready)
		dbug("job queued\n");
}


static const char *timestr(long long usec, char *ustr)
{
	static char str[64];
	char *s = ustr ? ustr : str;
	unsigned days, hours, mins, secs;
	double sec;
	secs = usec / 1000000L;
	usec = usec % 1000000L;
	days = secs / (60 * 60 * 24);
	secs -= days * 60 * 60 * 24;
	hours = secs / (60 * 60);
	secs -= hours * 60 * 60;
	mins = secs / 60;
	secs -= mins * 60;
	assert(secs < 60);
	sec = (double)secs + (double)usec * 1E-6;
	if (days)
		s += sprintf(s,"%d days, ",days);
	if (days || hours) {
		s += sprintf(s,"%d:",hours);
		s += sprintf(s,"%02d:",mins);
	} else {
		s += sprintf(s,"%d:",mins);
	}
	if ((days || hours || mins) && (sec < 10)) {
		*s = '0';
		++s;
	}
	s += sprintf(s,"%.4g",sec);
	if (days || hours) {
		sprintf(s," hours");
	} else if (mins) {
		sprintf(s," mins.");
	} else {
		sprintf(s," secs.");
	}
	return ustr ? ustr : str;
}


static inline const char *timevstr(struct timeval t, char *ustr)
{
	long long dt = t.tv_sec * 1000000 + t.tv_usec;
	return timestr(dt,ustr);
}


static void display_summary(job_t *j, int ret, long long dt, struct rusage *rusage, int showcommand)
{
	char real[64],user[64],sys[64];
	char *s, *msg;

	msg = alloca(strlen(j->cmd) + 128);
	s = msg;
	if (showcommand)
		s += sprintf(msg,"### job #%d (%s), pid %6ld, ",j->job,j->cmd,(long)j->pid);
	else
		s += sprintf(msg,"### ");
	if (WIFSIGNALED(ret))
		s += sprintf(s,"terminated by signal %d (%s), ",WTERMSIG(ret),strsignal(WTERMSIG(ret)));
	else
		s += sprintf(s,"exitcode %d, ",WEXITSTATUS(ret));
	if (rusage && RsrcUsage)
		s += sprintf(s,"real: %s, user: %s, sys: %s\n",timestr(dt,real),timevstr(rusage->ru_utime,user),timevstr(rusage->ru_stime,sys));
	else
		s += sprintf(s,"real: %s\n",timestr(dt,0));
	if (WIFSIGNALED(ret) || WEXITSTATUS(ret)) 
		warn("%s",msg);
	else
		info("%s",msg);
}


void clear_job(pid_t pid, int ret, struct rusage *rusage)
{
	job_t *j = Executing, *lj = 0;
	char **args;
	struct stat st;
	unsigned long long now;
	long long dt;

	dbug("clear_job(%d,%d)\n",pid,ret);
	while (j && (j->pid != pid)) {
		lj = j;
		j = j->next;
	}
	if (j == 0) {
		/* consider the following bourne shell-script:
		 * #!/bin/sh
		 * ls -1 | ./a.out
		 *********************
		 * In this case a.out might be exec'ed by the shell
		 * _after_ forking childs for the other parts of the
		 * pipe. In consequence, depending on implementation,
		 * a.out can get a return value != -1 from wait that is
		 * associated with a process that cannot be found in our
		 * job list.  Be aware that the above script explicitly
		 * refers to /bin/sh not to /bin/bash. I.e. this issue
		 * concerns the original bourne shell.
		 */
		return;
	}
	now = gettime();
	dt = now-j->start;
	if (lj)
		lj->next = j->next;
	else
		Executing = j->next;
	st.st_size = 0;
	if (j->outfile != -1) {
		if (-1 == fstat(j->outfile,&st))
			warn("could not stat output file: %s\n",strerror(errno));
	}
#if HAVE_MMAP
	if (st.st_size > Pagesize) {
		void *mem = mmap(0,st.st_size,PROT_READ,MAP_SHARED,j->outfile,0);
		if (mem != (void *)-1) {
			int r;
			info("### output of job #%d, pid %ld: %s\n",j->job,pid,j->cmd);
			r = fflush(stdout);
			if (r != 0)
				warn("unable to flush stdout: %s\n",strerror(errno));
			r = write(STDOUT_FILENO,mem,st.st_size);
			if (-1 == r)
				warn("unable to write output of job %d to stdout: %s\n",j->job,strerror(errno));
			r = munmap(mem,st.st_size);
			assert(r == 0);
			display_summary(j,ret,dt,rusage,0);
		} else {
			warn("could not mmap output of child: %s\n",strerror(errno));
			st.st_size = 0;
			display_summary(j,ret,dt,rusage,0);
		}
	} else
#endif
	if (st.st_size > 0) {
		int r, s0 = 0;
		size_t bsize = st.st_size + strlen(j->cmd) + 200;
#if __STDC_VERSION__ >= 199901L
		char buf[bsize];
#elif defined(HAVE_ALLOCA_H)
		char *buf;
		buf = alloca(bsize);
#else
		char *buf;
		buf = Malloc(bsize);
#endif
		if (Verbose >= Info) {
			s0 = snprintf(buf,bsize,"### output of job #%d, pid %ld: %s\n",j->job,(long)pid,j->cmd);
		}
		r = pread(j->outfile,buf+s0,st.st_size,0);
		if (r == -1)
			warn("error reading intermediate output file: %s\n",strerror(errno));
		else
			s0 += r;
		r = write(STDOUT_FILENO,buf,s0);
		if (r == -1)
			warn("unable to write result of job %d to stdout: %s\n",j->job,strerror(errno));
		display_summary(j,ret,dt,rusage,0);
#if (__STDC_VERSION < 199901L) && !defined(HAVE_ALLOCA_H)
		free(buf);
#endif
	} else {
		display_summary(j,ret,dt,rusage,1);
	}
	if ((ret != 0) && (ExitOnError)) {
		warn("### job failed - terminating...\n");
		Terminate = 1;
	}
	if (j->outfile != -1)
		(void) close(j->outfile);
	args = &j->args[Numbaseargs];
	while (*args) {
		free(*args);
		++args;
	}
	++Finished;
	free(j->cmd);
	free(j->args);
	assert(j != JobCache);
	j->next = JobCache;
	JobCache = j;
	dbug("%d jobs running\n",--Running);
}


static void usage(void)
{
	(void) printf(
	"xjobs [options] [command [option|argument ...]]\n"
	"xjobs [options] -- [option|argument ...]\n"
	"========================================\n"
	"valid options are:\n"
	"-h           : print this usage information and exit\n"
	"-L <log>     : set log file to <log> (default: stderr)\n"
	"-j <num>     : maximum number of jobs to execute in parallel (default %ld)\n"
	"-j <times>x  : set maximum number based on available processors (e.g. 0.5x)\n"
	"-s <file>    : script to execute (default: read from stdin)\n"
	"-l <num>     : combine <num> lines to a single job\n"
	"-n           : redirect stdout of childs to /dev/null\n"
	"-N           : redirect stdout and stderr of childs to /dev/null\n"
	"-d           : direct unbuffered output of stdout and stderr\n"
	"-v <level>   : set verbosity to level\n"
	"               (0=silent,1=error,2=warning,3=info,4=debug)\n"
	"-p           : prompt user, whether job should be started\n"
	"-q <num>     : limit queue to <num> entries\n"
	"-t           : print total time before exiting\n"
	"-e           : exit if a job terminates with an error\n"
	"-0           : one argument per job terminated by a null-character\n"
	"-1           : one argument per job terminated by a new-line\n"
#ifdef HAVE_WAIT4
	"-r           : omit display of resource usage\n"
#endif
	"--           : last option to xjobs, following options are passed to jobs\n"
	"-V           : print version and exit\n"
	, Limit
	);
	exit(0);
}


static void version(void)
{
	(void) printf(
	"xjobs version "VERSION"\n"
	"Copyright 2006-2015, Thomas Maier-Komor\n"
	"License: GPLv2\n"
	);
	exit(0);
}


static void parse_option_j(const char *arg)
{
	double lf;
	char f;
	switch (sscanf(arg,"%lg%c",&lf,&f)) {
		case 2:
			if (f != 'x') {
#ifdef _SC_NPROCESSORS_ONLN
				lf *= sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(_SC_NPROCESSORS_CONF)
				lf *= sysconf(_SC_NPROCESSORS_CONF);
#elif defined(__OpenBSD__)
				int mib[2], nproc;
				size_t len = sizeof(nproc);
				mib[0] = CTL_HW;
				mib[1] = HW_NCPU;
				if (-1 == sysctl(mib,2,&nproc,&len,0,0)) {
					warn("unable to determine number of processors: %s\nassuming 1 processors\n",strerror(errno));
					nproc = 1;
				}
				lf *= nproc;
#else
				warn("unable to determine number of processors, assuming 1\n");
				lf *= 1;
#endif
			} else
				error("invalid suffix in argument for option -j\n");
			/*FALLTHROUGH*/
		case 1:
			Limit = (long)ceil(lf);
			if (Limit <= 0)
				error("invalid argument for option -j\n");
			dbug("maximum number of jobs set to %d\n",Limit);
			break;
		default:
			error("missing argument to option -j\n");
	}
}


static char get_stdin_char(void)
{
	static char buf[8*1024];
	static char *at = buf, *end = buf;
	if (at == end) {
		int num = read(STDIN_FILENO,buf,sizeof(buf));
		if (-1 == num) {
			fprintf(stderr,"error reading stdin: %s\n",strerror(errno));
			return 0;
		} else if (0 == num) {
			return 0;
		} else {
			at = buf;
			end = buf+num;
		}
	}
	return *at++;
}



static int read_to_0(void)
{
	static char buf[PATH_MAX+1] = "";
	char *b = buf, c;
	if (buf[0] != 0) {
		buf[0] = 0;
		return EOL;
	}
	do {
		c = get_stdin_char();
		*b = c;
		++b;
		assert(b-buf < sizeof(buf));
	} while (c);
	if (*buf == 0)
		return 0;
	Fill = b - buf;
	Buffer = buf;
	return UQUOTED;
}


static int read_to_nl(void)
{
	static char buf[PATH_MAX+1] = "";
	char *b = buf, c;
	if (buf[0] != 0) {
		buf[0] = 0;
		return EOL;
	}
	do {
		c = get_stdin_char();
		*b = c;
		++b;
		assert(b-buf < sizeof(buf));
	} while (c && (c != '\n'));
	if (*buf == 0)
		return 0;
	*--b = 0;
	Fill = b - buf;
	Buffer = buf;
	return UQUOTED;
}


static void open_script(const char *a)
{
	int ret;
	struct stat st;

	Input = open(a,O_RDONLY);
	if (Input == -1)
		error("could not open input file %s: %s\n",optarg,strerror(errno));
	dbug("opening input script %s\n",optarg);
	ret = fstat(Input, &st);
	assert(ret != -1);
	if (S_ISFIFO(st.st_mode)) {
		struct sigaction sig;
		dbug("input script is a named pipe\n");
		sig.sa_handler = processSignal;
		sigemptyset(&sig.sa_mask);
		sigaddset(&sig.sa_mask,SIGPOLL);
		sig.sa_flags = SA_RESTART;
		ret = sigaction(SIGPOLL,&sig,0);
		assert(ret == 0);
#ifndef __CYGWIN__
		(void) open(optarg,O_WRONLY);
#endif
#ifndef __sun
		ret = fcntl(Input,F_SETOWN,getpid());
		if (ret != 0)
			warn("unable to set owning process for SIGPIPE of named pipe %s: %s\n",optarg,strerror(errno));
#endif
		ret = fcntl(Input,F_SETFL,O_RDONLY|O_NONBLOCK
#ifdef FASYNC
				| FASYNC
#endif
			   );
		assert(ret == 0);
#ifndef FASYNC
		ret = ioctl(Input,I_SETSIG,S_RDNORM);
		if (ret == -1)
			error("unable to setup SIGPOLL: %s\n",strerror(errno));
#endif
		InFlags = fcntl(Input,F_GETFL);
		Script = optarg;
	}
	close_onexec(Input);
	dbug("input file set to %s\n",optarg);
}


void total_summary(void)
{
	unsigned long long now = gettime();
	double td = (now-Start)/1000000.0;
	unsigned min = (unsigned)floor(td) / 60, hr = min / 60;
	td -= min * 60;
	min -= hr * 60;
	if (hr > 0)
		info("total: %u parsed, %u skipped, %u executed in %02dhr %02dmin %05.3gsec\n",Parsed,Skipped,Finished,hr,min,td);
	else if (min > 0)
		info("total: %u parsed, %u skipped, %u executed in %02dmin %05.3gsec\n",Parsed,Skipped,Finished,min,td);
	else
		info("total: %u parsed, %u skipped, %u executed in %.3gsec\n",Parsed,Skipped,Finished,td);
}


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;
		}
	}
	total_summary();
	exit(ExitCode);
}


void init_limit(void)
{
#ifdef _SC_NPROCESSORS_ONLN
	Limit = sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(_SC_NPROCESSORS_CONF)
	Limit = sysconf(_SC_NPROCESSORS_CONF);
#elif defined(__OpenBSD__)
	int mib[2], nproc;
	size_t len = sizeof(nproc);
	mib[0] = CTL_HW;
	mib[1] = HW_NCPU;
	if (-1 == sysctl(mib,2,&nproc,&len,0,0)) {
		warn("unable to determine number of processors: %s\nassuming 1 processor\n",strerror(errno));
		nproc = 1;
	}
	Limit = nproc;
#else
	warn("unable to determine number of available processors - assuming 1 processor\n");
	Limit = 1;
#endif
	dbug("%d processors currently online (default job limit)\n",Limit);
}


int main(int argc, char **argv, char **env)
{
	typedef enum { init, std, white, dquoted, squoted, desc, sesc } state_t;
	int i, flags = 0, ret;
       	unsigned long lines = 1, lcount = 0;
	char *jobin = 0, *jobout = 0, *joberr = 0, *jobpwd = 0;
	extern char *optarg;
	extern int optind, optopt;
	arg_t *args = 0;
	size_t numargs = 0, q_len = 0;
	struct sigaction sig;
	token_t t;

	Start = gettime();
	Env = env;
	setenv("POSIXLY_CORRECT","1",1);
	Pagesize = sysconf(_SC_PAGESIZE);
	Path = getenv("PATH");
	init_limit();
	while ((i = getopt(argc,argv,"01dehj:L:l:nNpq:rs:tv:V")) != -1) {
		switch (i) {
		default:
			abort();
			break;
		case '0':
			if (gettoken != yylex)
				error("conflicting options -0 and -1");
			dbug("set scanning mode to 1 argument ending on \\0\n");
			gettoken = read_to_0;
			break;
		case '1':
			if (gettoken != yylex)
				error("conflicting options -0 and -1");
			dbug("set scanning mode to 1 argument ending on \\n\n");
			gettoken = read_to_nl;
			break;
		case 'd':
			dbug("stdout and stderr will be unbuffered\n");
			Stdout = dup(STDOUT_FILENO);
			Stderr = dup(STDERR_FILENO);
			break;
		case 'e':
			dbug("user requests to exit on error\n");
			ExitOnError = 1;
			break;
		case 'h':
			usage();
			break;
		case 'j': 
			parse_option_j(optarg);
			break;
		case 'L':
			set_log(optarg);
			break;
		case 'l':
			if (1 == sscanf(optarg,"%lu",&lines) && (lines > 0))
				dbug("combining %lu lines to a single command\n",lines);
			else
				error("error in argument to option -l\n");
			break;
		case 'N':
			Stdout = open("/dev/null",O_WRONLY);
			if (Stdout == -1)
				error("could not open /dev/null: %s\n",strerror(errno));
			else
				dbug("stdout and stderr redirected to /dev/null\n");
			Stderr = Stdout;
			break;
		case 'n':
			Stdout = open("/dev/null",O_WRONLY);
			if (Stdout == -1)
				error("could not open /dev/null: %s\n",strerror(errno));
			else
				dbug("stdout redirected to /dev/null\n");
			break;
		case 'p':
			{
				Prompt = 1;
				dbug("enabling prompt mode\n");
			} break;
		case 'q':
			{
				unsigned tmp;
				if (1 == sscanf(optarg,"%u",&tmp) && (tmp > 0)) {
					q_len = tmp;
					dbug("limiting queue length to %lu elements\n",q_len);
				} else
					error("error in argument to option -q\n");
			}
			break;
		case 'r':
			RsrcUsage = 0;
			dbug("disabling display of resource usage\n");
			break;
		case 's':
			open_script(optarg);
			break;
		case 't':
			/* ignored for backward compatibility */
			break;
		case 'v':
			{
				int v;
				if (1 == sscanf(optarg,"%d",&v)) {
					Verbose = v;
					if (Verbose < Silent || Verbose > Debug)
						error("value out of range for option -v\n");
					dbug("verbose set to %d\n",Verbose);
				} else
					error("missing or invalid argument for option -v\n");
			}
			break;
		case 'V':
			version();
			break;
		case '?':
			error("unkonwn option -%c\n",optopt);
			break;
		case ':':
			error("option -%c requires an operand\n",optopt);
			break;
		}
	}
        dbug("PATH=\"%s\"\n", Path);

	while (optind < argc) {
		if (0 == strcmp("<",argv[optind])) {
			if (Stdin != -1)
				(void) close(Stdin);
			if (argv[++optind] == 0)
				error("missing argument for input redirector <\n");
			Stdin = open(argv[++optind],O_RDONLY);
			if (-1 == Stdin)
				error("could not open default stdin for jobs (%s): %s\n",argv[optind],strerror(errno));
		} else if (0 == strcmp(">",argv[optind])) {
			if (Stdout != -1)
				(void) close(Stdout);
			if (argv[++optind] == 0)
				error("missing argument for output redirector >\n");
			Stdout = open(argv[optind],O_WRONLY|O_CREAT|O_EXCL,0666);
			if (-1 == Stdout)
				error("could not open stdout for jobs (%s): %s\n",argv[optind],strerror(errno));
		} else if (0 == strcmp(">>",argv[optind])) {
			if (Stdout != -1)
				(void) close(Stdout);
			if (argv[++optind] == 0)
				error("missing argument for output redirector >>\n");
			Stdout = open(argv[optind],O_WRONLY|O_APPEND);
			if (-1 == Stdout)
				error("could not append to stdout for jobs (%s): %s\n",argv[optind],strerror(errno));
		} else if (0 == strcmp(">&",argv[optind])) {
			if (Stdout != -1)
				(void) close(Stdout);
			if (argv[++optind] == 0)
				error("missing argument for output redirector >&\n");
			Stderr = Stdout = open(argv[optind],O_WRONLY|O_CREAT|O_EXCL,0666);
			if (-1 == Stdout)
				error("could not open stdout/stderr for jobs (%s): %s\n",argv[optind],strerror(errno));
		} else if (0 == strcmp(">>&",argv[optind])) {
			if (Stdout != -1)
				(void) close(Stdout);
			if (argv[++optind] == 0)
				error("missing argument for output redirector >>&\n");
			Stdout = open(argv[optind],O_WRONLY|O_APPEND);
			if (-1 == Stdout)
				error("could not append to stdout/stderr for jobs (%s): %s\n",argv[optind],strerror(errno));
		} else {
			arg_t *a = Malloc(sizeof(arg_t));
			if (Numbaseargs == 0) {
				a->arg = complete_exe(argv[optind]);
				if (a->arg == 0)
					error("cannot find %s in PATH\n",argv[optind]);
			} else {
				a->arg = argv[optind];
			}
			a->prev = Baseargs;
			Baseargs = a;
			++Numbaseargs;
		}
		++optind;
	}
	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);
	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);
			args = 0;
			numargs = 0;
			jobin = 0;
			jobout = 0;
			joberr = 0;
			jobpwd = 0;
			flags = 0;
			lcount = 0;
			if (q_len && (q_len == Waiting))
				break;
			continue;
		case ERR:
			info("ignoring erroneous line\n");
			continue;
		case 0:
			dbug("token: 0\n");
			break;
		}
		
		if (start_jobs())
			continue;
		if (ChildTerminated) {
			int flags = WNOHANG, ret;
			struct sigaction orig;
                        siginfo_t si;
			pid_t pid;
			--ChildTerminated;
			if (q_len && (q_len == Waiting)) {
				dbug("queue is full\n");
			} else {
				flags = WNOHANG;
			}
			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);
			}
#ifdef HAVE_WAIT4
			if (RsrcUsage) {
				struct rusage rusage;
				if (0 < (pid = wait4(0,&ret,InputsWaiting == 0 ? 0 : flags,&rusage))) 
					clear_job(pid,ret,&rusage);
			} else
#endif
#ifdef __sun
                        if ((0 == waitid(P_PGID,PGID, &si, ((q_len && q_len == Waiting) ? 0 : WNOHANG) | WEXITED)) && (si.si_pid != 0))
                                clear_job(si.si_pid,si.si_status,0);
#else

			if ((pid = waitpid(0,&ret,flags)) > 0) {
				clear_job(pid,ret,0);
			}
#endif
			if (InputsWaiting == 0) {
				ret = sigaction(SIGPOLL,&orig,0);
				assert(ret == 0);
			}
			start_jobs();
		}
	}
	if (numargs)
		add_job(args,numargs,jobin,jobout,joberr,jobpwd,flags);

	while ((Executing || Ready) && (Terminate == 0)) {
		pid_t pid;
		int ret;
#ifdef HAVE_WAIT4
		struct rusage rusage;
#endif
		start_jobs();
#ifdef HAVE_WAIT4
		pid = wait4(0,&ret,0,&rusage);
		clear_job(pid,ret,&rusage);
#else
		pid = wait(&ret);
		clear_job(pid,ret,0);
#endif
	}
	finalize();
}

/* vim:tw=0
 */
