/*
**	Program:	init.c
**	Purpose:	Provides gross process scheduling based on a
**			system run-state, and provides a mechanism for
**			dynamic alteration of the run-state.
**
**	Version:	1 - 95/02/20 - Initial Version
**	Release:	0 - 95/02/20 - Initial Release
**			1 - 95/03/24 - Clear ut_user and ut_line fields
**				       when changing to DEAD_PROCESS
**	Author:		Lew Pitcher
**	Language:	C (ACK for Minix)
**
**	Notes:		This program is invoked in two ways: by the
**			Minix bootstrap, to perform the "proc1" processing,
**			and by any user process, to change the system state.
**			The program makes the distinction by testing it's
**			pid; if the program is running with a pid of 1, then
**			it is the "proc1" process, and the appropriate logic
**			is executed. If the program is not running with a
**			pid of 1, then it is a user process, and can only
**			change the system run-state indicator.
*/

#include <sys/types.h>
#include <utmp.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <ctype.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>

/*
** generic definitions
*/
#define	FALSE	0			/* FALSE truth value	*/
#define TRUE	(!FALSE)		/* TRUE truth value	*/

/*
** init specific definitions
*/
#define	PROC1_PID	((pid_t) 1)	/* mom's process id	*/
#define LEVELS		"0123456s"	/* legal run levels	*/
#define INITTAB		"/etc/inittab"	/* path to inittab	*/
#define	INITBUF		257		/* size of inittab buf	*/

#define ITAB_HEAD "#@ emergency inittab created by proc1\n"
#define ITAB_TTY0 "tty0::respawn:/bin/login </dev/tty0 >/dev/tty0 2>&1\n"

#define NOT_PROC1 ((pid_t) -1)	/* returned on ECHILD / EINTR	*/
#define NOT_CHILD ((pid_t) 0)	/* returned for orphans		*/

/************************************************************************/
/*		forward references for functions in init.c		*/
/************************************************************************/
int	getstate(),	/* retrieve RUN_LVL value, return run level	*/
	getitab();	/* get fields of next valid entry in inittab	*/
pid_t	spawn(),	/* spawn child process, return it's process id	*/
	procexit();	/* wait for child termination, return its pid	*/
utmp_t	*setstate();	/* format RUN_LVL record, return ptr to record	*/
void	notify();	/* SIGHUP signal handler routine		*/
char	*split();	/* break up string, return ptr to substring	*/
 
/*****************************************
/*	global variables (part 1)	**
/****************************************/
char	*runlevels = LEVELS,		/* telinit, proc1 legal levels	*/
	*path_utmp = UTMP;		/* telinit, proc1 utmp filename	*/

/*
** main() determines whether the program was invoked for process scheduling
** or to change the system run state, and performs the appropriate actions.
** In the case of process scheduling, main() passes control to proc1(),
** which never terminates. For run state changes, main() parses the input
** and requests the change itself.
*/ 
main(argc,argv)
int argc;
char *argv[];
{
	int exitcode = 1;	/* default telinit exit code	*/

	if (getpid() == PROC1_PID)	proc1();	/* never returns */

	/* telinit <state> */
	if ((argc == 2) && (argv[1][1] == '\0'))
	{
		int state;

		state = tolower(argv[1][0]);	/* get requested state */
		if (strchr(runlevels,state) != NULL) 
		{
			struct utmp	work;

			/* construct new runlevel record */
			setstate(&work,state);
			utmpname(path_utmp);
			if (getutid(&work) != U_NULL)
			{
				pututline(&work);
				exitcode = 0;
			}
			endutent();
		}
		else if (state == 'q')	exitcode = 0;
	}

	if (exitcode == 0)
	{
		printf("%s: Run level is set to %c\n",argv[0],getstate());
		kill(SIGHUP,PROC1_PID);
	}
	else
		fprintf(stderr,"Usage: %s {%sq}\n",argv[0],runlevels);

	exit(exitcode);
}

/*************************************************************************
*******				PROC1 processing		   *******
*************************************************************************/

/*****************************************
/*	global variables (part 2)	**
/****************************************/
int	change_state = FALSE;
char	*path_itab = INITTAB,
	initbuf[INITBUF];	/* line buffer for inittab fgets	*/

/*
** proc1() is the mother of all user processes.
**
** It performs system initialization once, then continiously performs
** user process initialization, spawning and respawning user processes
** as required. The system initialization includes the creation of
** utmp and inittab files if they do not exist. The (minimum) inittab
** file created by proc1() will contain a single entry that will
** permit the operator to repair the system, since a missing inittab
** is cause for proc1 failure.
*/
proc1()
{
	int signum,		/* general signal number	*/
	    cur_state;		/* current processing state	*/
	struct utmp utmp;	/* BOOT_TIME and RUN_LVL record	*/
	FILE *fp;		/* inittab stream file pointer	*/
	char *field[4];		/* inittab fields		*/

	/* disable all interrupts for now */
	for (signum = 1; signum <= _NSIG; ++signum) signal(signum,SIG_IGN);

	/* make sure that we can get at inittab */
	if (access(path_itab,F_OK|R_OK) == -1)
	{
		fp = fopen(path_itab,"w");
		fputs(ITAB_HEAD,fp);
		fputs(ITAB_TTY0,fp);
		fclose(fp);
	}

	/* set default runstate (assume no initdefault entry)	*/
	cur_state = '1';

	/* process inittab entries */
	fp = fopen(path_itab,"r");
	while (getitab(fp,field) == TRUE)
	{
		if ((strcmp(field[2],"sysinit") == 0) && (*field[3] != '\0'))
		{
			pid_t child;

			child = spawn(field[3]);
			while (wait() != child)	/* just wait */;
		}
		else if (strcmp(field[2],"initdefault") == 0)
			cur_state = *field[1];
		else continue;
	}
	fclose(fp);

	/* ensure a valid initial runstate */
	if (strchr(runlevels,cur_state) == NULL) cur_state = '1';

	/* truncate utmp on reboot to clean out old records	*/
	close(creat(path_utmp,0664));	/* delete utmp contents	*/
	
	/* write BOOT_TIME record to utmp and wtmp */
	memset(&utmp,0,sizeof utmp);
	strncpy(utmp.ut_user,"reboot",sizeof utmp.ut_user);
	strncpy(utmp.ut_id,"~~",sizeof utmp.ut_id);
	strncpy(utmp.ut_line,"~",sizeof utmp.ut_line);
	utmp.ut_pid = PROC1_PID;
	utmp.ut_type = BOOT_TIME;
	utmp.ut_time = time(NULL);
	record(&utmp);

	/* done system initialization, drop into user processing */
	change_state = TRUE;
	for (;;)	/* this loop done for each new run state	*/
	{
		/* first, take care of state change processes		*/
		signal(SIGHUP,SIG_IGN);		/* no interrupts please */
		inittab(cur_state);		/* change states here	*/
		record(setstate(&utmp,cur_state));

		/* now, take care of respawning child processes		*/
		change_state = FALSE;		/* state now stable	*/
		signal(SIGHUP,notify);		/* allow telinit signal	*/
		while (change_state == FALSE) 
		{
			inittab(cur_state);	/* respawn if required	*/
			procexit();		/* wait on exit or kill	*/
		}

		/* telinit changed the state, we gotta get it from utmp	*/
		cur_state = getstate();
	}
}

/*
** inittab() processes all inittab entries for a specified state
** transition. If the global variable state_change is FALSE, then
** only the "respawn" entries for the state are processed, otherwise
** all the entries are processed.
*/
inittab(state)
int state;
{
	FILE *fp;
	char *field[4];

	if ((fp = fopen(path_itab,"r")) == NULL) return;
	while (getitab(fp,field) == TRUE)
	{
		struct utmp utmp,	/* utmp record for id lookup	*/
			    *urec;	/* utmp record returned in tlu	*/
		int f_wait,		/* TRUE when we want to wait	*/
		    f_respawn;		/* TRUE when we want to respawn	*/

		/* no 'states' means *all* states */
		if (*field[1] == '\0')	field[1] = runlevels;

		if (strcmp(field[2],"wait") == 0)
		{
			f_wait	  = TRUE;	/* wait		*/
			f_respawn = FALSE;	/* dont respawn	*/
		}
		else if (strcmp(field[2],"once") == 0)
		{
			f_wait	  = FALSE;	/* dont wait	*/
			f_respawn = FALSE;	/* dont respawn	*/
		}
		else if (strcmp(field[2],"respawn") == 0)
		{
			f_wait	  = FALSE;	/* dont wait	*/
			f_respawn = TRUE;	/* respawn	*/
		}
		else continue;	/* "sysinit", etc.	*/

		/* gotta have some process to exec	*/
		if (*field[3] == '\0')		continue;

		/* build child's utmp record */
		memset(&utmp,0,sizeof utmp);
		strncpy(utmp.ut_id,field[0],sizeof utmp.ut_id);
		utmp.ut_type = INIT_PROCESS;
		utmp.ut_time = time(NULL);

		/* check through utmp for matching process record */
		utmpname(path_utmp); urec = getutid(&utmp); endutent();

		/* take whatever processing action is necessary:
		**	if inittab entry is not for the current state
		**		if there is an active process for it
		**			kill the active process
		**		else (there is no active process for it)
		**			do nothing
		**		fi
		**	else (inittab entry is for current state)
		**		if the process is inactive
		**			if we want to start the process
		**				start the process
		**			else (we dont want to start it)
		**				do nothing
		**			fi
		**		else (process is already active)
		**			do nothing
		**		fi
		**	fi
		*/
		if (strchr(field[1],state) == NULL)
			if ((urec!=U_NULL) && (urec->ut_type!=DEAD_PROCESS))
				kill(urec->ut_pid,SIGKILL);
			else ;	/* do nothing */
		else if ((urec==U_NULL) || (urec->ut_type==DEAD_PROCESS))
			if ((f_respawn == TRUE) || (change_state == TRUE))
			{
				utmp.ut_pid = spawn(field[3]);
				record(&utmp);
				while (f_wait && (procexit() != utmp.ut_pid))
						/* just wait */;
			}
			else /* do nothing */ ;
		else /* do nothing */ ;
	}
	fclose(fp);
}

/*
** spawn() forks off a child process that will execute the given command.
** The parent process returns the child's process id to the caller, while
** the child exec's the binary specified in the command string.
** NB: Only rudementary i/o redirection parsing is performed on the command
** string. If a complex command is necessary, use the inittab entry to
** invoke sh, and use the shell to run the command.
*/
pid_t spawn(cmd)
char *cmd;
{
	/************> variable used by parent process <*********/
	pid_t child;

	/************> variables used by child process <*********/
	char *binpath,		/* name of binary to exec	*/
	     *argv[16],		/* should be enough argv's	*/
	     *env[1],		/* tiny environment list	*/
	     *path_i,		/* stdin redirection path	*/
	     *path_o,		/* stdout redirection path	*/
	     *path_e;		/* stderr redirection path	*/
	int num;		/* index, loop counter		*/

	/****************************************************************/
	if ((child = fork()) != 0)	return child;	/* parent only	*/
	/****************************************************************/

	/********************* beginning of child *********************/
	env[0] = NULL;
	path_i = path_o = path_e = "/dev/null";
	binpath = argv[0] = strtok(cmd," \t\n");

	/* build arglist, exclude special args from list */
	for (num = 1; (argv[num] = strtok(NULL," \t\n")) != NULL; ++num)
	{
		if (argv[num][0] == '<')
			path_i = &argv[num--][1];	
		else if (argv[num][0] == '>')
			path_o = &argv[num--][1];
		else if ((argv[num][0] == '2') && (argv[num][1] == '>'))
			path_e = &argv[num--][2];
	}

	/* close all files */
	for (num = 0;num < NFILES; ++num) close(num);

	/* open stdin */
	open(path_i,O_RDWR);

	/* open stdout */
	if (*path_o == '>')
		open(path_o+1,O_RDWR|O_APPEND|O_CREAT,0666);
	else 	open(path_o,O_RDWR);

	/* open stderr */
	if (strcmp(path_e,"&1") == 0)
		dup(1);		/* stderr = stdout */
	else	if (*path_e == '>')
			open(path_e+1,O_RDWR|O_APPEND|O_CREAT,0666);
		else	open(path_e,O_RDWR);
	
	/* reset all signal handler flags */
	for (num = 1; num <= _NSIG; ++num)	signal(num,SIG_DFL);

	/* execute the required command */
	execve(binpath,argv,env);

	/* if we get here, then execv failed */
	exit(127);		/* exit with fail rc */
	/*********************** end of child ***********************/
}

/*
** procexit() will wait for the termination of an orphan or child 
** process, or an external interrupt. If the wait is interruped, he
** returns the NOT_PROC1 returncode to the caller, if wait returns
** an orphan process, procexit returns the NOT_CHILD returncode to
** the caller, otherwise, he returns the pid of the child process.
** Additionally, the termination of a child process will be recorded
** in the utmp file by procexit()
*/
pid_t procexit()
{
	pid_t pid;
	int status;
	struct utmp work,
		    *utmp;

	/* return to caller if no_children or interrupt_received */
	if ((pid = wait(&status)) == (pid_t) -1) return NOT_PROC1;

	/* child processes are recorded in utmp, orphans are not */
	utmpname(path_utmp);
	do
		utmp = getutent();
	while ((utmp != U_NULL) && (utmp->ut_pid != pid));
	endutent();
	if (utmp == U_NULL) return NOT_CHILD;	/* orphan process */

	/** He wasn't an orphan - complete death certificate & tell mom	*/
	memcpy(&work,utmp,sizeof work);		/* death certificate	*/
	work.ut_type = DEAD_PROCESS;		/* "He's dead, Jim!"	*/
	work.ut_time = time(NULL);		/* time of death	*/
	strncpy(work.ut_user,"",sizeof work.ut_user);	/* delete name	*/
	strncpy(work.ut_line,"",sizeof work.ut_line);	/* and address	*/
	record(&work);				/* register the death 	*/

	return pid;	/* tell mom which child died */
}

/*
** notify() intercepts SIGHUP for proc1, and sets a global variable to
** indicate that the runstate has changed. This then causes the procexit()
** loop in proc1() to terminate, and inittab will be reprocessed.
*/
void notify(signum)
int signum;
{
	signal(signum,SIG_IGN);
	change_state = TRUE;
	signal(signum,notify);
}

/*************************************************************************
**				Utility Routines			**
*************************************************************************/

/*
** setstate() initializes a utmp record for a given run level.
*/
struct utmp *setstate(utmp,state)
struct utmp *utmp;
int state;
{
	memset(utmp,0,sizeof *utmp);
	strncpy(utmp->ut_user,"runlvl ?",sizeof utmp->ut_user);
	utmp->ut_user[7] = state;
	utmp->ut_type	 = RUN_LVL;
	utmp->ut_pid	 = getpid();
	utmp->ut_time	 = time(NULL);
	return utmp;
}

/*
** getstate() retrieves the runstate from utmp and passes
** the runstate back to the calling routine.
*/
int getstate()
{
	struct utmp *utmp;

	utmpname(path_utmp);
	do
		utmp = getutent();
	while ((utmp != U_NULL) && (utmp->ut_type != RUN_LVL));
	endutent();

	return (utmp != U_NULL ? utmp->ut_user[7] : '?');
}

/*
** record() will update inplace by ut_id the utmp file, and
** will append the utmp record to the wtmp file
*/
record(urec)
struct utmp *urec;
{
	int fd;

	utmpname(path_utmp);
	getutid(urec);
	pututline(urec);
	endutent();

	if ((fd = open(WTMP,O_WRONLY|O_APPEND)) != -1)
	{
		write(fd,urec,sizeof *urec);
		close(fd);
	}
	sync();
}

/*
** getitab() reads forward in the inittab file until it gets a valid entry
** or until it hits End-Of-File. getitab() will return TRUE with a valid
** entry (and will have put pointers to the fields into the 'field' array),
** or will return FALSE on EOF.
*/
int getitab(fp,field)
FILE *fp;
char *field[];
{
	while (fgets(initbuf,sizeof initbuf,fp) != NULL)
	{
		split(initbuf,'#');
		field[0] = initbuf;
		if ((field[1] = split(field[0],':')) == NULL)
			continue;	/* not id:---			*/
		if ((field[2] = split(field[1],':')) == NULL)
			continue;	/* not id:states:---		*/
		if ((field[3] = split(field[2],':')) == NULL)
			continue;	/* not id:states:action:proc	*/
		while (isspace(*field[3]))	++field[3];
		return TRUE;
	}
	return FALSE;
}

/*
** split() breaks a string into two substrings by replacing the first
** occurence of the seperator character with a '\0'. The address of
** the second substring is returned, or NULL if no seperator was found.
*/
char *split(string,sep)
char *string, sep;
{
	if (string == NULL)	return NULL;
	while (*string != '\0')
		if (*string == sep)
		{
			*string = '\0';
			return string+1;
		}
		else ++string;
	return NULL;
}
