/*
 * Copyright 1993,1994 Globetrotter Software, Inc.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of Globetrotter Software not be used
 * in advertising or publicity pertaining to distribution of the software
 * without specific, written prior permission.  Globetrotter Software makes
 * no representations about the suitability of this software for any purpose.
 * It is provided "as is" without express or implied warranty.
 *
 * GLOBETROTTER SOFTWARE DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.  IN NO
 * EVENT SHALL GLOBETROTTER SOFTWARE BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
 * OF THIS SOFTWARE.
 *
 * Author:  Jim McBeath, Globetrotter Software, jimmc@globes.com
 */
/* app.c - interface to applications being run from htimp
 *
 * Jim McBeath, November 18, 1993
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/file.h>
#ifndef FIONREAD
#include <sys/filio.h>
#endif
#include <errno.h>
#include "htimp.h"
#include "app.h"

#ifdef O_NONBLOCK
#define NONBLOCKFLAG O_NONBLOCK
#else
#define NONBLOCKFLAG FNDELAY
#endif

extern int ImpDebug;
extern int ImpUseUnderExit;

AppInfo *AppList;
JobInfo *JobList;
int ImpKillTimeout;
int ImpCleanupTimeout;
int ImpMaxJobs;
int ImpJobCount;	/* number of active jobs */

void
ImpSetMaxJobs(n)
int n;
{
	ImpMaxJobs = n;
}

void
ImpSetKillTimeout(n)
int n;
{
	ImpKillTimeout = n;
}

void
ImpSetCleanupTimeout(n)
int n;
{
	ImpCleanupTimeout = n;
}

AppInfo *
ImpNewApp(name)
char *name;		/* name of the app */
{
	AppInfo *app;

	app = ImpMalloc(sizeof(*app));
	memset((void *)app,'\0',sizeof(*app));
	app->next = AppList;
	app->name = ImpStrSave(name);
	app->killtimeout = ImpKillTimeout;
	app->cleanuptimeout = ImpCleanupTimeout;
	AppList = app;
	return app;
}

AppInfo *
ImpFindApp(appname)
char *appname;
{
	AppInfo *app;

	for (app=AppList; app; app=app->next) {
		if (strcmp(appname,app->name)==0)
			return app;
	}
	return (AppInfo *)0;	/* not found */
}

JobInfo *
ImpNewJob(app)
AppInfo *app;
{
	JobInfo *job;
	char buf[50];
	int i;

	job = ImpMalloc(sizeof(*job));
	memset((void *)job,'\0',sizeof(*job));
	job->next = JobList;
	JobList = job;
	job->app = app;
	for (i=0; i<HANDLE_LENGTH; i++) {
		buf[i] = ImpRandChar();
	}
	buf[i] = 0;
	strcpy(job->handle,buf);
	return job;
}

int	/* 0 if OK */
ImpStartJob(f,job,str)
FILE *f;
JobInfo *job;
char *str;	/* the args str for the job */
{
	int ddi[2];	/* pipe for stdin */
	int ddo[2];	/* pipe for stdout */
	int t;
	pid_t pid;
	int i;
	char *path;
	char *argv0;

	t = pipe(ddi);
	if (t<0) {
		ImpReplyError(f,"+Can't get input pipe to start job for %s",
			job->app);
		return -1;
	}
	t = pipe(ddo);
	if (t<0) {
		close(ddi[0]);
		close(ddi[1]);
		ImpReplyError(f,"+Can't get output pipe to start job for %s",
			job->app);
		return -1;
	}
	pid = fork();
	if (pid<0) {
		ImpReplyError(f,"+Could not fork to start job for %s",job->app);
		return -1;
	}
	if (pid==0) {
		/* child */
		close(ddi[1]);	/* close output half of our output pipe */
		if (ddi[0]!=0) {
			/* if not already stdin, move to stdin */
			dup2(ddi[0],0);
			close(ddo[0]);
		}
		close(ddo[0]);	/* close input half of our output pipe */
		if (ddo[1]!=1) {
			/* if not already stdout, move to stdout */
			dup2(ddo[1],1);
			close(ddo[1]);
		}
		/* leave stderr where it is, close everything else */
		for (i=ImpGetDescCount()-1; i>2; i--)
			close(i);
#if 0
		/* turn off signal handlers */
		signal(SIGPIPE,SIG_DFL);
		signal(SIGINT,SIG_DFL);
		signal(SIGTERM,SIG_DFL);
#endif
		path = job->app->execpath;
		argv0 = strrchr(path,'/');
		if (argv0) argv0++;
		else argv0 = path;
		/* TBD - break str into separate args */
		execl(path,argv0,str,(char *)0);
		ImpUseUnderExit = 1;	/* use _exit(), not exit() */
		ImpFatal("+Can't execute %s",argv0);
		/* NOTREACHED */
	} else {
		/* parent */
		job->pid = pid;
		close(ddi[0]);
		close(ddo[1]);
		job->stdind = ddi[1];	/* we write to his stdin */
		job->stdoutd = ddo[0];	/* and read from his stdout */
		t = fcntl(job->stdoutd,F_SETFL,NONBLOCKFLAG |
				fcntl(job->stdoutd,F_GETFL,0));
			/* make it non-blocking */
		/* TBD - check for error? */
		ImpJobCount++;
		if (ImpDebug) {
			fprintf(stderr,"Start job %s (%s %s)\n",job->handle,
				job->app->name,str?str:"");
		}
		return 0;	/* everything OK */
	}
}

/* Kill a running job */
void
ImpKillJob(job)
JobInfo *job;
{
	pid_t pid;
	struct timeval to;

	if (!job->pid)
		return;
	if (ImpDebug) {
		fprintf(stderr,"Send SIGTERM to job %s\n",job->handle);
	}
	kill(job->pid,SIGTERM);
	to.tv_sec = 1;	/* give app a bit of time to exit */
	to.tv_usec = 0;
	select(0,(fd_set *)0,(fd_set *)0,(fd_set *)0,&to);
		/* wait for SIGCHLD */
	pid = waitpid(job->pid,NULL,WNOHANG);	/* see if it exited */
	if (pid<0) {
		/* didn't exit with SIGTERM, push harder */
		if (ImpDebug) {
			fprintf(stderr,"Send SIGKILL to job %s\n",
				job->handle);
		}
		kill(job->pid,SIGKILL);
		pid = waitpid(job->pid,NULL,0);
	}
	job->pid = 0;
	job->killed = 1;
	if (ImpDebug) {
		fprintf(stderr,"Killed job %s\n",job->handle);
	}
}

void
ImpCloseJob(job)
JobInfo *job;
{
	pid_t pid;
	struct timeval to;

	if (job->pid)
		ImpKillJob(job);
	ImpJobCount--;
	close(job->stdind);
	close(job->stdoutd);
	time(&job->killedtime);
/* Leave the handle there for a while as a stale handle */
}

JobInfo *
ImpFindJob(handle)
char *handle;
{
	JobInfo *job;

	for (job=JobList; job; job=job->next) {
		if (strcmp(handle,job->handle)==0)
			return job;
	}
	return (JobInfo *)0;	/* not found */
}

/* Kill the job and remove it from the internal list */
void
ImpQuitJob(job)
JobInfo *job;
{
	JobInfo **jp;

	if (!job->killedtime)
		ImpCloseJob(job);	/* stop it first */
	for (jp= &JobList; *jp; jp= &((*jp)->next)) {
		if (*jp==job) {
			if (ImpDebug) {
				fprintf(stderr,"Cleaned up job %s\n",
					job->handle);
			}
			*jp = job->next;	/* remove from list */
			ImpFree((void *)job);	/* free it up */
			return;
		}
	}
}

int	/* 1 if any children were reaped */
ImpCheckChildren()
{
	pid_t pid;
	JobInfo *job;

	pid = waitpid((pid_t)-1,NULL,WNOHANG);	/* see if any exited */
	if (pid<=0)
		return 0;	/* nothing found */
	for (job=JobList; job; job=job->next) {
		/* look for the dead child */
		if (job->pid==pid) {
			if (ImpDebug) {
				fprintf(stderr,"Child exited: job %s\n",
					job->handle);
			}
			job->pid = 0;
			ImpCloseJob(job);
			return 1;
		}
	}
	fprintf(stderr,"Master could not find child pid=%d\n",pid);
	return 0;	/* Hmmm, couldn't find it */
}

void
ImpCheckJobs()
{
	JobInfo *job, *nextjob;
	int n, t;
	time_t curtime;

	time(&curtime);
	for (job=JobList; job; job=nextjob) {
		nextjob = job->next;	/* in case we remove job */
		if (!job->pid) {
			/* Dead, see if we should clean up */
			n = curtime - job->killedtime;
			if (n>job->app->cleanuptimeout) {
				if (ImpDebug)
					fprintf(stderr,
						"Check: cleanup job %s\n",
						job->handle);
				ImpQuitJob(job);	/* remove all traces */
			}
		} else {
			/* Still alive, see if we should kill it */
			n = curtime - job->lastcmdtime;
			t = job->app->killtimeout;
			if (t>0 && n>t) {
				if (ImpDebug)
					fprintf(stderr,"Check: kill job %s\n",
						job->handle);
				ImpCloseJob(job);	/* kill it */
				if (job->app->cleanuptimeout==0)
					ImpQuitJob(job); /* cleanup now */
			}
		}
	}
	/* TBD - also need to check for jobs that died on us without
	 * our telling them so */
}

/* end */
