/*
 *  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 <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>	// for Solaris
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifdef STDC_HEADERS
# include <stddef.h>
# include <stdlib.h>
#elif defined HAVE_STDLIB_H
# include <stdlib.h>
#endif

#ifdef HAVE_MMAP
#include <sys/mman.h>
#endif

#ifdef HAVE_SPAWN
#include <spawn.h>
#endif

#ifdef HAVE_VFORK_H
#include <vfork.h>
#endif

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

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

extern volatile int Terminate;
extern int ExitCode;

job_t *Executing = 0, *Ready = 0, *ReadyEnd = 0, *JobCache = 0;
arg_t *ArgCache = 0;
unsigned Skipped = 0, Finished = 0, Numbaseargs = 0, Running = 0,
	 Waiting = 0, Failed = 0, Parsed = 0;
const char *JobIn = 0;

static arg_t *Baseargs = 0;


void add_basearg(arg_t *a)
{
	if (Numbaseargs == 0) {
		char *exe = complete_exe(a->arg);
		if (exe == 0)
			error("cannot find %s in PATH\n",a->arg);
		a->arg = exe;
	}
	dbug("basic argument %d: %s\n",Numbaseargs,a->arg);
	a->prev = Baseargs;
	Baseargs = a;
	++Numbaseargs;
}


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[-1] = ' ';
	} while (*++a);
	c[-1] = 0;
}


void add_job(arg_t *a, size_t n, char *in, char *out, char *err, char *pwd, int app)
{
	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));
	job_t *j = JobCache;
	if (j)
		JobCache = j->next;
	else
		j = Malloc(sizeof(job_t));
	int i = n + Numbaseargs + 1;
	char **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);
	char *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");
}


void display_status()
{
	static int lF = -1, lX = -1, lR = -1, lW = -1;
	if ((lW == Waiting) && (lF == Finished) && (lX == Failed) && (lR == Running))
		return;
	status("*** %s%u queued, %s%u finished, %s%u failed, %s%u running%s%c"
		, TiYellow
		, Waiting
		, PfxDone
		, Finished
		, PfxFail
		, Failed
		, TiGreen
		, Running
		, TiNoAttr
		, HaveTty ? '\r' : '\n');
	lF = Finished;
	lX = Failed;
	lR = Running;
	lW = Waiting;
}


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

	if (Verbose < Info)
		return;
	assert(j);
	size_t asize = strlen(j->cmd) + 128;
	char msg[asize];
	s = msg;
	s += snprintf(s,asize,"%s### job #%d: '%s'",ret?PfxFail:PfxDone,j->job,j->cmd);
	if (ShowPID)
		s += snprintf(s,asize-(s-msg),", pid %6ld",(long)j->pid);
	if (WIFSIGNALED(ret))
		s += snprintf(s,asize-(s-msg),": terminated by signal %d (%s), ",WTERMSIG(ret),strsignal(WTERMSIG(ret)));
	else
		s += snprintf(s,asize-(s-msg),": exitcode %d, ",WEXITSTATUS(ret));
	s += snprintf(s,asize-(s-msg),"real: %s",timestr(dt,real,sizeof(real)));
	if (rusage && RsrcUsage) {
		if (rusage->ru_utime.tv_sec || rusage->ru_utime.tv_usec > 1000)
			s += snprintf(s,asize-(s-msg),", user: %s",timevstr(rusage->ru_utime,user,sizeof(user)));
		if (rusage->ru_stime.tv_sec || rusage->ru_stime.tv_usec > 1000)
			s += snprintf(s,asize-(s-msg),", sys: %s",timevstr(rusage->ru_stime,sys,sizeof(sys)));
	}
	*s++ = '\n';
	write_log(Info,msg,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;
	if (j->outfile != -1) {
		if (-1 == fstat(j->outfile,&st)) {
			warn("could not stat output file: %s\n",strerror(errno));
			st.st_size = 0;
		}
	}
	if ((Verbose >= Info) && (Echo != flag_off)) {
		size_t cl = strlen(j->cmd);
		char buf[TiClrEolL+cl+1];
		if (TiClrEolL)
			memcpy(buf,TiClrEol,TiClrEolL);
		memcpy(buf+TiClrEolL,j->cmd,cl);
		buf[TiClrEolL+cl] = '\n';
		Write(STDOUT_FILENO,buf,TiClrEolL+cl+1);
	}
	display_summary(j,ret,dt,rusage);
	int mw = 0;
#ifdef HAVE_MMAP
	if (st.st_size > (Pagesize<<1)) {
		void *mem = mmap(0,st.st_size,PROT_READ,MAP_SHARED,j->outfile,0);
		if (mem != (void *)-1) {
			if (-1 == Write(STDOUT_FILENO,mem,st.st_size))
				warn("unable to write output of job %d to stdout: %s\n",j->job,strerror(errno));
			else
				mw = 1;
			int r = munmap(mem,st.st_size);
			assert(r == 0);
		} else {
			warn("could not mmap output of child: %s\n",strerror(errno));
		}
	}
#endif
	if (mw == 0) {
		char buf[4096];
		char *b = buf;
		if (st.st_size > sizeof(buf))
			b = malloc(st.st_size);
		int r = pread(j->outfile,b,st.st_size,0);
		if (-1 == r)
			warn("error reading intermediate output file: %s\n",strerror(errno));
		else if (-1 == Write(STDOUT_FILENO,b,r))
			warn("unable to write result of job %d to stdout: %s\n",j->job,strerror(errno));
		if (b != buf)
			free(b);
	}
	if (ret != 0) {
		++Failed;
		if (ExitOnError) {
			warn("%s### job failed - terminating...\n",PfxFail);
			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);
	bzero(j,sizeof(job_t));
	j->next = JobCache;
	JobCache = j;
	dbug("%d jobs running\n",--Running);
	display_status();
}


static int ask_exec(job_t *j)
{
	char buf[4096];
	if (j->pid == -1)
		return 1;
	if (0 == Prompt)
		return 0;
	name_job(j);
	size_t n = snprintf(buf,sizeof(buf),"%s%sstart job '%s'?\n(enter y for yes, no for no, q for quit) ",TiNoAttr,TiClrEol,j->cmd);
	if (-1 == Write(STDERR_FILENO,buf,n))
		warn("cannot write to terminal: %s\n",strerror(errno));
	do {
		int r = read(STDERR_FILENO,buf,sizeof(buf));
		dbug("read stderr: %d : %s\n",r,buf);
		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(buf,"q\n",2)) || (0 == strncasecmp(buf,"quit\n",3))) {
			if (-1 == Write(STDERR_FILENO,"quit\n",5))
				warn("cannot write to terminal: %s\n",strerror(errno));
			j->pid = 0;
			Terminate = 1;
			return 1;
		}
		if ((0 == strncasecmp(buf,"n\n",2)) || (0 == strncasecmp(buf,"no\n",3))) {
			if (-1 == Write(STDERR_FILENO,"no\n",3))
				warn("cannot write to terminal: %s\n",strerror(errno));
			j->pid = 0;
			return 1;
		}
	} while ((0 != strncasecmp(buf,"y\n",2)) && (0 != strncasecmp(buf,"yes\n",4)));
	if (-1 == Write(STDERR_FILENO,"yes\n",4))
		warn("cannot write to terminal: %s\n",strerror(errno));
	return 0;
}


static void resolve_jobid(job_t *j)
{
	j->in = replace_string_l(j->in,"$(jobid)",j->job);
	j->out = replace_string_l(j->out,"$(jobid)",j->job);
	j->err = replace_string_l(j->err,"$(jobid)",j->job);
}


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

	int ret = posix_spawn_file_actions_init(&acts);
	assert(ret == 0);
#endif
	j->job = ++jobid;
	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 for job %u: %s\n",j->pwd,jobid,strerror(errno));
			j->pid = -1;
			goto finish;
		}
	} else
		pwd[0] = 0;
	j->start = gettime();
	if ((j->in == 0) && (JobIn != 0))
		j->in = Strdup(JobIn);
	resolve_jobid(j);
	if (j->in) {	/* stdin is redirected */
		in = open(j->in,O_RDONLY,0666);
		if (in == -1) {
			warn("cannot open input file %s for job %u: %s\n",j->in,jobid,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) {
		name_job(j);
		if (ShowPID)
			status("%s### started job #%d, pid %ld: %s\n",PfxStart,jobid,j->pid,j->cmd);
		else
			status("%s### started job #%d: %s\n",PfxStart,jobid,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;
	}
#else
#if defined(HAVE_WORKING_VFORK)
	j->pid = vfork();
#else
	j->pid = fork();
#endif
	if (j->pid == -1) {
		warn("error could not fork: %s\n",strerror(errno));
		goto finish;
	}
	if (j->pid != 0) {
		name_job(j);
		if (ShowPID)
			status("%s### started job #%d, pid %ld: %s\n",PfxStart,jobid,j->pid,j->cmd);
		else
			status("%s### started job #%d: %s\n",PfxStart,jobid,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);	// do not use exit after execve!
	}
#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] != 0) && (chdir(pwd) == -1))
		error("unable to change back to working directory %s: %s\n",pwd,strerror(errno));
}


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");
			display_status();
			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) {
				/* user interaction returned 'no' */
				info("### 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;
	}
	display_status();
	return num;
}


