/*
** job.c
**
** Forking, waiting and job control POSIX signal semantics.
*/

#include "rc.h"

#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <string.h>

#include <sys/ioctl.h>

#include "posix.h"
#include "sigmsgs.h"
#include "opt.h"
#include "readline.h"

/*
child processes are contained in a jobs list
each job on the list is headed by a process group leader
for the process group leader:
   n points to the next group leader
   group points to the first process in the group
	gleader points to this job
   own is valid
   id is the job number for use with job control commands
   tio contains stored terminal settings from the last time the process was suspended from the foreground
for processes in each job:
   n points to the next process
   gleader points to the group leader

There is a fairly foul dependency in rc_wait4 which means that the last process created in a pipeline must be the last one rc_wait4()ed on

*/

typedef enum {owned, disowned, quietowned} own_t;

static char *own_command [] = {"reown", "disown", "quietown"};
static char *own_status [] = {"owned", "disowned", "quietowned"};


typedef struct job_t job_t;

static struct job_t {
	int id, status, signal;
	pid_t pid;
	bool alive;
	own_t own;
	char *command;
	struct termios *tio;
	job_t *group, *gleader, *n;
} *job_list = NULL;


static void fg_bg_main (job_t *job, bool fg);
static int add_job (int pgrp, pid_t pid, bool background, char *command);
static job_t *remove_job (job_t *job);
static job_t *job_match (char *s);
extern void sigchld (int nohang);


bool forked = FALSE;


/* necessary to ignore SIGTTOU if caller of tcsetpgrp is running in the background */
#define IGN_SIGTTOU \
	struct sigaction act, oact;\
	act.sa_handler = SIG_IGN;\
	act.sa_flags = 0;\
	sigaction (SIGTTOU, &act, &oact);

#define UNIGN_SIGTTOU sigaction (SIGTTOU, &oact, NULL)


/* moves rc into a new process group and gets the controlling terminal if being run from a non job control shell */

/* restores controlling terminal to previous shell on exit */
static pid_t old_tc_pid = 0;

extern void
grab_control ()
{
	pid_t pid = getpid ();
	old_tc_pid = tcgetpgrp (0);
	if (interactive && old_tc_pid != pid) {
		/* move into new process group */
		#ifdef SYSTEMV
			setpgid (0, 0);
		#else
			setpgrp (0, pid);
		#endif
		/* grab the controlling terminal */
		{
			IGN_SIGTTOU;
			tcdrain (0);
			tcsetpgrp (0, pid);
			new_term ();
			UNIGN_SIGTTOU;
		}
	} else {
		if (old_tc_pid == pid) new_term ();
		old_tc_pid = 0;
	}
}


/* use the blocking of read() on a pipe between parent and child to ensure that parent is ready before the child has an opportunity to die */
/* only get pid after fork() - so cannot add job to list before forking */
/* all new processes are moved to a new process group so that they can be independently suspended - UNLESS rc is running non-interactive - then the shell and all its foreground children must suspend as one (background processes should be removed from the scope of suspension and may continue after the shell goes interactive, for example, if run from .rcrc) */

extern pid_t
rc_fork (bool background, int pgrp, char *command) {
	sigset_t sigset;
	int sync [2];
	char buffer;
	pid_t pid, parent_pid = getpid ();

	/* prevent broken pipes by blocking SIGINT for the duration */
	sigemptyset (&sigset);
	sigaddset (&sigset, SIGINT);
	sigprocmask (SIG_BLOCK, &sigset, NULL);
	pipe (sync);
	pid = fork();
	switch (pid) {
	case -1:
		uerror("fork");
		rc_error(NULL);
		/* NOTREACHED */
	case 0:
		forked = TRUE;
		close (sync [1]);

		pid = getpid ();
		/* move to new process group or that of the pipeline group leader */
		if (interactive || background) {
			#ifdef SYSTEMV
				setpgid (0, pgrp);
			#else
				setpgrp (0, pgrp ? pgrp : pid);
			#endif
		}

		if (interactive && !background && pgrp == 0 && tcgetpgrp (0) == parent_pid) {
			IGN_SIGTTOU;
			/* if rc is already in the background (eg doing a pipeline) then don't grab the controlling terminal */
			tcdrain (0);
			tcsetpgrp (0, pid);
			UNIGN_SIGTTOU;
		}
		/* need to add to job list in child because rc may fork again (eg to create the stages of a backgrounded pipeline) and these grandchildren may need to know about the child (to put themselves in the same process group) */
		/* putting this in simply removes special cases for child processes */
		add_job (pgrp, pid, background, command);
		/* don't quit until parent has added child to job list */
		read (sync [0], &buffer, 1);

		close (sync [0]);
		sigprocmask (SIG_UNBLOCK, &sigset, NULL);
		sigchk ();
		return 0;
	default:
		close (sync [0]);
		add_job (pgrp, pid, background, command);

		/* child can quit now */
		write (sync [1], &buffer, 1);

		close (sync [1]);
		sigprocmask (SIG_UNBLOCK, &sigset, NULL);
		return pid;
	}
}


/* find a job in the main list or in a process group off a process leader */

static job_t *
find_job (pid_t pid)
{
	job_t *job = job_list, *gjob;
	while (job && job->pid != pid) {
		if (job->group) {
			for (gjob = job->group; gjob && gjob->pid != pid; gjob = gjob->n);
			if (gjob) return gjob;
		}
		job = job->n;
	}
	return job;
}


/* no sigchk (), so the wait can be returned to without dispensing signals */

extern pid_t
rc_wait4 (pid_t pid, int *status, bool nointr) {
	job_t *job = find_job (pid);
	sigset_t sigset, osigset;

	/* first look to see if child already exited */
	if (!job) {
		errno = ECHILD; /* no children */
		*status = 0x100; /* exit(1) */
		/* don't print Byron's error message - it's non-orthogonal */
		return -1;
	}

	/* if have this one entry to sigchld(), then it doesn't have to be NOHANG */
	sigemptyset (&sigset);
	sigaddset (&sigset, SIGCHLD);
	/* if (nointr) sigaddset (&sigset, SIGINT); */
	sigprocmask (SIG_BLOCK, &sigset, &osigset);
	while (job->alive && !job->signal) {
		sigchld (0);
	}
	sigprocmask (SIG_UNBLOCK, &sigset, &osigset);

	if (!job->alive) {
		*status = job->status;
		if (!job->group || (job != job->gleader && !job->n)) remove_job (job);
	} else {
		/* displays non-fatal signal names */
		if (interactive && job->signal < NUMOFSIGNALS && *(signals [job->signal].msg) && job->own != quietowned) {
			job_t *gjob;
			for (gjob = job->gleader->group; gjob && (gjob == job || !gjob->signal); gjob = gjob->n);
			if (!gjob) fprint (2, "%s\n", signals [job->signal].msg);
		}
		*status = job->status;
	}

	return pid;
}


extern List *
sgetapids () {
	List *r;
	job_t *job = job_list, *gjob;
	for (r = NULL; job != NULL; job = job->n) {
		List *q;
		if (!job->alive) continue;
		q = nnew(List);
		q->w = nprint("%d", job->pid);
		q->m = NULL;
		q->n = r;
		r = q;
		if (job->group) {
			for (gjob = job->group; gjob; gjob = gjob->n) {
				if (gjob->alive) {
					q = nnew(List);
					q->w = nprint("%d", gjob->pid);
					q->m = NULL;
					q->n = r;
					r = q;
				}
			}
		}
	}
	return r;
}


static job_t *
first_live_job (pid_t pid)
{
	job_t *job = job_list, *gjob;
	while (job && (!job->alive || job->signal || job->pid == pid)) {
		if (job->group) {
			for (gjob = job->group; gjob && (!gjob->alive || gjob->signal || gjob->pid == pid); gjob = gjob->n);
			if (gjob) return gjob;
		}
		job = job->n;
	}
	return job;
}


/* wait for a given process, or all outstanding processes */

extern void
b_wait(char **av) {
	if (av [1] == NULL) {
		waitforall();
		return;
	}
	while (*(++ av) != NULL) {
		if (**av == '%') {
			/* wait for entire job */
			job_t *job = job_match (*av + 1), *gjob;
			if (job) {
				int procs = 1, loop = 0;
				int *stats;
				if (job->group) {
					for (gjob = job->group; gjob; gjob = gjob->n) {
						if (gjob->alive && gjob->signal) gjob->signal = 0;
						procs ++;
					}
				}
				stats = ealloc (procs * sizeof (int));
				rc_wait4 (job->pid, stats, TRUE);
				for (gjob = job->group; gjob; gjob = gjob->n) {
					rc_wait4 (job->pid, &(stats [loop]), TRUE);
					loop ++;
				}
				setpipestatus (stats, procs);
				sigchk ();
			} else {
				fprint (2, "there is no job %%%s\n", *av + 1);
				set (FALSE);
				return;
			}
		} else {
			pid_t pid = a2u (*av);
			int stat;
			if (pid == -1) {
				fprint (2, "wait: bad pid %s\n", *av);
				set (FALSE);
				return;
			}
			rc_wait4 (pid, &stat, FALSE);
			setstatus (pid, stat);
			if (getstatus () != 0) {
				fprint (2, "wait: process is not child or does not exist");
				return;
			}
			sigchk ();
		}
	}
	set (TRUE);
}


extern void
waitforall () {
	pid_t wpid = getpid (), pid;
	int status;
	job_t *job = first_live_job (wpid);
	while (job) {
		pid = rc_wait4 (job->pid, &status, FALSE);
		setstatus(pid, status);
		sigchk ();
		job = first_live_job (wpid);
	}
}


extern void
b_jobs (char **av)
{
	job_t *job, *gjob;
	char *id, *id2;

	if (av [1] != NULL) {
		arg_count ("jobs");
		return;
	}

	sigchk ();
	job = job_list;

	while (job) {
		id = nprint ("[%d] ", job->id);
		fprint (1, "%s%d", id, job->pid);
		if (!job->alive && !job->signal) fprint (1, " (exit status %d)", WEXITSTATUS (job->status));
		if (job->signal) fprint (1, " (%s)", signals [job->signal].msg);
		if (job->own != owned) fprint (1, " (%s)", own_status [job->own]);
		fprint (1, " %s\n", job->command);
		if (job->group) {
			for (id2 = id; *id2; id2++) *id2 = ' ';
			for (gjob = job->group; gjob; gjob = gjob->n) {
				fprint (1, "%s%d", id, gjob->pid);
				if (!gjob->alive && !gjob->signal) fprint (1, " (exit status %d)", WEXITSTATUS (gjob->status));
				if (gjob->signal) fprint (1, " (%s)", signals [gjob->signal].msg);
				fprint (1, " %s\n", gjob->command);
			}
		}
		if (job->alive) {
			job = job->n;
		} else {
			job = remove_job (job);
		}
	}
	set (TRUE);
}


static job_t *
default_job ()
{
	job_t *job = job_list;

	while (job && !job->alive) job = job->n;

	return job;
}


static void
change_own (char **av, own_t own)
{
	int id = 0;
	job_t *job;

	sigchk ();

	set (FALSE);
	if (av [1]) {
		if (av [2]) arg_count (own_command [own]);
		job = job_match (av [1]);
		if (job) {
			id = job->id;
		} else {
			fprint (2, "there is no job %s\n", av [1]);
			return;
		}
	} else if ((job = default_job ())) {
		job->own = own;
		set (TRUE);
		return;
	} else {
		fprint (2, "there is no job to %s\n", own_command [own]);
		return;
	}

	for (job = job_list; job && job->id != id; job = job->n);

	if (job) job->own = own;

	set (TRUE);
}


extern void
b_disown (char **av)
{
	change_own (av, disowned);
}


extern void
b_reown (char **av)
{
	change_own (av, owned);
}


extern void
b_quietown (char **av)
{
	change_own (av, quietowned);
}


static job_t *
job_match (char *s)
{
	job_t *job = job_list, *gjob;
	if (!streq (s, "%")) {
		int id = a2u (s);
		if (id > 0) {
			for (; job && job->id != id; job = job->n);
		} else {
			for (; job && !strstr (job->command, s); job = job->n) {
				if (job->group) {
					for (gjob = job->group; gjob && !strstr (gjob->command, s); gjob = gjob->n);
					if (gjob) return gjob->gleader;
				}
			}
		}
	}
	return job;
}


extern void
b_kill (char **av)
{
	int sig = SIGTERM;
	bool use_last_job = FALSE;
	av++;
	if (!*av || (**av == '-' && !*(av + 1))) use_last_job = TRUE;
	while (*av) {
		if (**av == '-') {
			if (isdigit (*(*av + 1))) {
				sig = a2u (*av + 1);
				if (sig == -1) {
					fprint (2, "kill: unknown signal %s\n", *av + 1);
					set (FALSE);
					return;
				}
			} else {
				char *name = to_lower (ncpy (*av + 1));
				if (strncmp (name, "sig", 3) == 0) name += 3;
				for (sig = 1; sig < NUMOFSIGNALS; sig++) {
					if (streq (signals [sig].name + 3, name)) break;
				}
				if (sig == NUMOFSIGNALS) {
					fprint (2, "kill: unknown signal %s\n", *av + 1);
					set (FALSE);
					return;
				}
			}
		} else {
			if (**av == '%') {
				/* signal job's process group */
				job_t *job = job_match (*av + 1);
				if (job) {
					if (kill (-job->pid, sig) == -1) {
						uerror ("kill");
						set (FALSE);
						return;
					}
				} else {
					fprint (2, "there is no job %%%s\n", *av + 1);
					set (FALSE);
					return;
				}
			} else {
				pid_t pid = a2u (*av);
				if (pid == -1) {
					fprint (2, "kill: bad pid %s\n", *av);
					set (FALSE);
					return;
				}
				if (kill (pid, sig) == -1) {
					uerror ("kill");
					set (FALSE);
					return;
				}
			}
		}
		av ++;
	}
	if (use_last_job && job_list) {
		if (kill (-job_list->pid, sig) == -1) {
			uerror ("kill");
			set (FALSE);
			return;
		}
	}
	set (TRUE);
}


static void
fg_bg (char *name, char **av, bool fg)
{
	int id = 0;
	job_t *job;

	sigchk ();

	set (FALSE);
	if (av [1]) {
		if (av [2]) arg_count (name);
		job = job_match (av [1]);
		if (job) {
			id =job->id;
		} else {
			fprint (2, "there is no job %s\n", av [1]);
			return;
		}
		for (job = job_list; job && job->id != id; job = job->n);
	} else if ((job = default_job ())) {
	} else {
		fprint (2, "there is no job to %s\n", name);
		return;
	}

	if (job) {
		if (job->alive) {
			fg_bg_main (job, fg);
		} else {
			fprint (2, "job %d is dead\n", id);
		}
	} else {
		fprint (2, "there is no job %d\n", id);
	}
}


static void
fg_bg_main (job_t *job, bool fg)
{
	int status;
	job_t *gjob;
	int procs = 1;
	if (fg && interactive && tcgetpgrp (0) == getpid ()) {
		/* give the process the controlling terminal */
		{
			IGN_SIGTTOU;
			tcdrain (0);
			tcflush (0, TCIFLUSH);
			/* restore terminal settings if a previously suspended process moves into the foreground */
			if (job->signal && job->tio) {
				tcsetattr (0, TCSADRAIN, job->tio);
				tcdrain (0);
			}
			tcsetpgrp (0, job->pid);
			UNIGN_SIGTTOU;
		}
	}
	if (job->group) {
		for (gjob = job->group; gjob; gjob = gjob->n) {
			if (gjob->alive && gjob->signal) gjob->signal = 0;
			procs ++;
		}
	}
	if (job->signal) {
		job->signal = 0;
		kill (-job->pid, SIGCONT);	/* send SIGCONT to unsuspend process group - note that process group may contain processes that rc does not know about (if, for example, the group leader is another shell running a script) */
	}
	if (fg) {
		/* assume parenthood - return with status */
		if (job->group) {
			int *stats = ealloc (procs * sizeof (int));
			int loop = 1;
			rc_wait4 (job->pid, stats, TRUE);
			for (gjob = job->group; gjob; gjob = gjob->n) {
				if (rc_wait4 (job->pid, &(stats [loop]), TRUE) == -1) {
					/* background process finished before it could be foregrounded */
					set (TRUE);
					return;
				}
				loop ++;
			}
			setpipestatus (stats, procs);
		} else {
			rc_wait4 (job->pid, &status, FALSE);
			set (status);
		}
	} else {
		set (TRUE);
	}
}


extern void
b_fg (char **av)
{
	fg_bg ("fg", av, TRUE);
}


extern void
b_bg (char **av)
{
	fg_bg ("bg", av, FALSE);
}


/* check for non-dead non-suspended jobs */

int quit_warning = 0;

extern bool
jobs_warning ()
{
	job_t *gjob;
	if (interactive && !forked && opt [opt_quit_warn] && quit_warning == 0) {
		job_t *job = job_list;
		while (job && (!job->alive || job->own != owned)) {
			if (!job->alive && job->group) {
				for (gjob = job->group; gjob && !gjob->alive; gjob = gjob->n);
				if (gjob) break;
			}
			job = job->n;
		}
		if (job) {
			if (job->signal) {
				fprint (2, "You have suspended jobs\n");
			} else {
				fprint (2, "You have running jobs\n");
			}
			quit_warning = 2;
			return FALSE;
		}
	}
	return TRUE;
}


extern void
exit_jobs (void)
{
	job_t *job = job_list;

	while (job) {
		if (job->own != disowned) {
			if (job->alive) {
				if (job->signal) {
					kill (-job->pid, SIGKILL);	/* kill suspended process group */
				} else {
					kill (-job->pid, SIGHUP);	/* hang up on bg/fg process group */
				}
			}
		}
		job = job->n;
	}
	/* give controlling terminal back to non job control shell */
	if (old_tc_pid) {
		IGN_SIGTTOU;
		/* can't use file descriptor 0 - the input routines closed it when they read EOF (no big deal, 1 works just as well) */
		tcdrain (1);
		tcsetpgrp (1, old_tc_pid);
		UNIGN_SIGTTOU;
	}
}


static int
add_job (int pgrp, pid_t pid, bool background, char *command)
{
	job_t *job = enew (job_t);
	job->pid = pid;
	job->command = ecpy (command);
	job->alive = TRUE;
	job->signal = 0;	/* bogus signal */

	if (pgrp) {
		job->gleader = job_list;
		while (job->gleader->pid != pgrp) job->gleader = job->gleader->n;
		job->n = job->gleader->group;
		job->gleader->group = job;
	} else {
		if (job_list) {
			job->id = job_list->id + 1;
		} else {
			job->id = 1;
		}
		varassign ("job", word (nprint ("%d", job->id), NULL), FALSE);
		job->own = owned;
		job->tio = NULL;
		job->group = NULL;
		job->gleader = job;
		job->n = job_list;
		job_list = job;
	}

	return job->id;
}


/* remove whole job if whole process group is dead */
/* returns next job in list */

static job_t *
remove_job (job_t *job)
{
	job_t *pjob = job_list, *gjob;
	job = job->gleader;
	if (job->group) {
		job_t *tjob;
		for (gjob = job->group; gjob && !gjob->alive; gjob = gjob->n);
		if (gjob) return job->n;
		gjob = job->group;
		while (gjob) {
			tjob = gjob->n;
			efree (gjob->command);
			efree (gjob);
			gjob = tjob;
		}
	}
	if (job == job_list) {
		job_list = job->n;
	} else {
		while (pjob && pjob->n != job) pjob = pjob->n;
		if (!pjob) return job->n;
		pjob->n = job->n;
	}
	pjob = job->n;
	efree (job->command);
	efree (job);

	/* return new next job */
	return pjob;
}


/* permanent signal handler for SIGCHLD */
/* updates job table */

extern void
sigchld (int nohang)
{
	pid_t rc_pid = getpid (), pid;
	int status;
	job_t *job = job_list;

	do {
		do {
			pid = waitpid (-1, &status, nohang | WUNTRACED);
		} while (pid == -1 && errno == EINTR);

		if (pid == -1) break;
		job = find_job (pid);
		if (job) {
			int tcpgrp = tcgetpgrp (0);
			if (WIFSTOPPED (status)) {
				/* if non-interactive - with children in single process group then ignore children getting SIGTSTP, as rc must itself have been suspended */
				if (!interactive && WSTOPSIG (status) == SIGTSTP) continue;
				job->signal = WSTOPSIG (status);
				if (job->gleader->tio) {
					efree (job->gleader->tio);
					job->gleader->tio = NULL;
				}
				if (job->pid == tcpgrp) {
					job->gleader->tio = enew (struct termios);
					tcgetattr (0, job->gleader->tio);
				}
			} else if (WIFSIGNALED (status)) {
				job->alive = FALSE;
				job->signal = WTERMSIG (status);
			} else {
				job->alive = FALSE;
				/* in case restarted other than by rc */
				job->signal = 0;
			}
			job->status = status;

			/* get back control of the terminal */
/* was:
if (interactive && job->pid == tcpgrp) {
needed altering so that if a child of an rc running non-interactive but with control of the terminal (a script) was killed, the rc running the script would enforce terminal sanity - as it will quit with an exit() (not a kill signal) and so its interactive parent will not know to enforce sanity
ah ha... there appears to be a BSD (?) convention that, if a shell exits with non-zero exit status because one of its children died with a signal, it should exit with bit seven of the exit status set plus the signal number - eg exit(0x89)
this doesn't mean that the parent can know when to enforce sanity - cos if the child that died from a signal was not the last command in the script the script shell will not quit
thus the sanity enforcement won't work if the script shell does not enforce sanity on its children while running non-interactive (any shell bar this rc)
...and so it is that I revert to a policy of terminal sanitisation on every line, as per tcsh
*/
			if (job->pid == tcpgrp || (!interactive && rc_pid == tcpgrp)) {
				IGN_SIGTTOU;
				tcdrain (0);
				tcsetpgrp (0, rc_pid);
				if (job->signal || (job->status & 0x8000)) {
					/* tcflush (0, TCIOFLUSH); don't do this here - prevents proper terminfo normalisation on suspending vi on Student1! Not helped by preceding with tcdrain (0) either */
					/* reset terminal modes */
/* useful when debugging: fprint (2, "pid %d, tc %d, interactive %d, rc %d, sig %d, stat %x\n", job->pid, tcpgrp, interactive, rc_pid, job->signal, job->status); */
					restore_sanity ();
				} else {
/* if process changed stty in a way that is still sane, then update tio_old which is used to restore terminal sanity from on getting signals */
/* doesn't actually restore sanity here if the terminal is insane - that is only done when rc needs a line of input and when a job is killed etc (above) */
/* so it's possible to be insane over multiple commands (but not over prompts) if you so desire */
					update_tio ();
				}
				UNIGN_SIGTTOU;
			}
		}
	} while (pid > 0 && nohang);
}

