/******************************************************************************
 jProcessUtil.cc

	Routines for creating child processes.

	Copyright © 1997 John Lindal. All rights reserved.

 ******************************************************************************/

#include <jProcessUtil.h>
#include <JThisProcess.h>
#include <JPtrArray.h>
#include <JString.h>
#include <JProcessError.h>
#include <JStdError.h>
#include <JInPipeStream.h>
#include <jStreamUtil.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <jSignal.h>
#include <sys/wait.h>
#include <jFileUtil.h>
#include <jDirUtil.h>
#include <jUNIXUtil.h>
#include <jErrno.h>
#include <jMissingProto.h>
#include <jAssert.h>

static JBoolean theIncludeCWDOnPathFlag = kFalse;

// Private functions

void		JCleanArg(JString* arg);
JBoolean	JProgramOnPath(const JCharacter* programName, JString* fixedName);

/******************************************************************************
 JPrepArgForExec

	Inserts backslashes in front of double quotes and backslashes and then
	puts double quotes around the entire string.

 ******************************************************************************/

JString
JPrepArgForExec
	(
	const JCharacter* arg
	)
{
	JString str = arg;

	const JSize length = str.GetLength();
	for (JIndex i=length; i>=1; i--)
		{
		const JCharacter c = str.GetCharacter(i);
		if (c == '"' || c == '\\')
			{
			str.InsertSubstring("\\", i);
			}
		}

	str.Prepend("\"");
	str.Append("\"");
	return str;
}

/******************************************************************************
 JCleanArg (private)

	Removes single backslashes.

 ******************************************************************************/

void
JCleanArg
	(
	JString* arg
	)
{
	JSize length = arg->GetLength();
	for (JIndex i=1; i<=length; i++)
		{
		if (arg->GetCharacter(i) == '\\')
			{
			arg->RemoveSubstring(i,i);
			length--;
			}
		}
}

/******************************************************************************
 JExecute

	Splits up the given string by whitespace (except that which is quoted)
	and passes the resulting array of strings to the main version of JExecute().

 ******************************************************************************/

JError
JExecute
	(
	const JCharacter*		origStr,
	pid_t*					childPID,
	const JExecuteAction	toAction,
	int*					toFD,
	const JExecuteAction	fromAction,
	int*					fromFD,
	const JExecuteAction	errAction,
	int*					errFD
	)
{
	assert( origStr != NULL && origStr[0] != '\0' );

	// parse the string into a list of arguments

	JPtrArray<JString> argList;

	JString str = origStr;
	str.TrimWhitespace();

	JSize length = str.GetLength();
	while (length > 0)
		{
		JString* arg = NULL;

		JIndex i;
		if (str.GetFirstCharacter() == '"')
			{
			i = 2;
			while (i <= length && str.GetCharacter(i) != '"')
				{
				if (str.GetCharacter(i) == '\\')
					{
					i++;
					}
				i++;
				}

			if (i <= length)
				{
				arg = new JString(str.GetSubstring(2, i-1));
				assert( arg != NULL );
				str.RemoveSubstring(1,i);
				}
			else if (length > 1)
				{
				arg = new JString(str.GetSubstring(2, length));
				assert( arg != NULL );
				str.Clear();
				}
			else
				{
				str.Clear();
				}
			}
		else if (str.LocateSubstring(" ", &i))
			{
			arg = new JString(str.GetSubstring(1, i-1));
			assert( arg != NULL );
			str.RemoveSubstring(1,i);
			}
		else
			{
			arg = new JString(str);
			assert( arg != NULL );
			str.Clear();
			}

		if (arg != NULL)
			{
			JCleanArg(arg);
			argList.Append(arg);
			}

		str.TrimWhitespace();
		length = str.GetLength();
		}

	// pass the arguments to JExecute

	const JSize argc = argList.GetElementCount();

	const JCharacter** argv = new const JCharacter* [ argc+1 ];
	assert( argv != NULL );

	for (JIndex i=1; i<=argc; i++)
		{
		argv[i-1] = *(argList.NthElement(i));
		}
	argv[argc] = NULL;

	const JError err = JExecute(argv, (argc+1) * sizeof(JCharacter*), childPID,
								toAction, toFD, fromAction, fromFD,
								errAction, errFD);

	// clean up

	argList.DeleteAll();
	delete [] argv;

	return err;
}

/******************************************************************************
 JExecute

	argv[0] can either be a full path or just a name.  If it is just a
	name, we search for it just like the shell would.

	argv[] must be terminated with a NULL entry.

	count must be sizeof(argv[]).  JExecute automatically divides by
	sizeof(char*).  We required this so we can check that argv is NULL
	terminated.  It is way too easy to forget to do this otherwise.

	If childPID != NULL, it is set to the pid of the child process.
	Otherwise, JExecute() blocks until the child exits.

	Actions:

		kJIgnoreConnection  don't create a pipe, use default connection

		kJCreatePipe        create a pipe and return the end that the
		                    parent should use

		kJAttachToFD        connect the child to the descriptor passed in
		                    via the corresponding int* (don't create pipe)

		kJTossOutput        connects output to /dev/null
		                    (only works for fromAction and errAction)

		kJAttachToFromFD    connect stderr to stdout
		                    (only works for errAction)

	For kJAttachToFD, toFD has to be something that can be read from, and
	fromFD and errFD have to be something that can be written to.

	Can return JProgramNotAvailable, JNoProcessMemory, JNoKernelMemory.

	*** Security Note:
		This function calls execvp(), which uses the current search path to
		find the program to run.  In most cases, this is desirable because
		each UNIX system has its own ideas about where to put "standard"
		programs.  For suid root programs, however, this is dangerous.
		Therefore, when writing suid root programs, always call this function
		with a full path so you know exactly which binary is being run.

 ******************************************************************************/

JError
JExecute
	(
	const JCharacter*		argv[],
	const JSize				count,
	pid_t*					childPID,
	const JExecuteAction	toAction,
	int*					toFD,
	const JExecuteAction	fromAction,
	int*					fromFD,
	const JExecuteAction	errAction,
	int*					errFD
	)
{
	assert( argv[ (count/sizeof(JCharacter*)) - 1 ] == NULL );

	assert( toAction != kJTossOutput && toAction != kJAttachToFromFD );
	assert( fromAction != kJAttachToFromFD );

	assert( (toAction != kJCreatePipe && toAction != kJAttachToFD) ||
			toFD != NULL );
	assert( (fromAction != kJCreatePipe && fromAction != kJAttachToFD) ||
			fromFD != NULL );
	assert( (errAction != kJCreatePipe && errAction != kJAttachToFD) ||
			errFD != NULL );

	JString progName;
	if (!JProgramOnPath(argv[0], &progName))
		{
		return JProgramNotAvailable(argv[0]);
		}
	argv[0] = progName.GetCString();

	int fd[3][2];

	if (toAction == kJCreatePipe)
		{
		const JError err = JCreatePipe(fd[0]);
		if (!err.OK())
			{
			return err;
			}
		}

	if (fromAction == kJCreatePipe)
		{
		const JError err = JCreatePipe(fd[1]);
		if (!err.OK())
			{
			if (toAction == kJCreatePipe)
				{
				close(fd[0][0]);
				close(fd[0][1]);
				}
			return err;
			}
		}

	if (errAction == kJCreatePipe)
		{
		const JError err = JCreatePipe(fd[2]);
		if (!err.OK())
			{
			if (toAction == kJCreatePipe)
				{
				close(fd[0][0]);
				close(fd[0][1]);
				}
			if (fromAction == kJCreatePipe)
				{
				close(fd[1][0]);
				close(fd[1][1]);
				}
			return err;
			}
		}

	pid_t pid;
	const JError err = JThisProcess::Fork(&pid);
	if (!err.OK())
		{
		if (toAction == kJCreatePipe)
			{
			close(fd[0][0]);
			close(fd[0][1]);
			}
		if (fromAction == kJCreatePipe)
			{
			close(fd[1][0]);
			close(fd[1][1]);
			}
		if (errAction == kJCreatePipe)
			{
			close(fd[2][0]);
			close(fd[2][1]);
			}
		return err;
		}

	// child

	else if (pid == 0)
		{
		const int stdinFD = fileno(stdin);
		if (toAction == kJCreatePipe)
			{
			dup2(fd[0][0], stdinFD);
			close(fd[0][0]);
			close(fd[0][1]);
			}
		else if (toAction == kJAttachToFD)
			{
			dup2(*toFD, stdinFD);
			close(*toFD);
			}

		const int stdoutFD = fileno(stdout);
		if (fromAction == kJCreatePipe)
			{
			dup2(fd[1][1], stdoutFD);
			close(fd[1][0]);
			close(fd[1][1]);
			}
		else if (fromAction == kJAttachToFD)
			{
			dup2(*fromFD, stdoutFD);
			close(*fromFD);
			}
		else if (fromAction == kJTossOutput)
			{
			FILE* nullFile = fopen("/dev/null", "a");
			int nullfd     = fileno(nullFile);
			dup2(nullfd, stdoutFD);
			fclose(nullFile);
			}

		const int stderrFD = fileno(stderr);
		if (errAction == kJCreatePipe)
			{
			dup2(fd[2][1], stderrFD);
			close(fd[2][0]);
			close(fd[2][1]);
			}
		else if (errAction == kJAttachToFD)
			{
			dup2(*errFD, stderrFD);
			close(*errFD);
			}
		else if (errAction == kJTossOutput)
			{
			FILE* nullFile = fopen("/dev/null", "a");
			int nullfd     = fileno(nullFile);
			dup2(nullfd, stderrFD);
			fclose(nullFile);
			}
		else if (errAction == kJAttachToFromFD && fromAction != kJIgnoreConnection)
			{
			dup2(stdoutFD, stderrFD);
			}

		ACE_OS::execvp(argv[0], const_cast<char* const*>(argv));

		cerr << "Unable to run program \"" << argv[0] << '"' << endl;
		cerr << endl;
		cerr << "JExecute()::execvp() failed" << endl;
		cerr << "Errno value: " << jerrno() << endl;

		JThisProcess::Exit(1);
		return JNoError();
		}

	// parent

	else
		{
		if (toAction == kJCreatePipe)
			{
			close(fd[0][0]);
			*toFD = fd[0][1];
			}
		if (fromAction == kJCreatePipe)
			{
			close(fd[1][1]);
			*fromFD = fd[1][0];
			}
		if (errAction == kJCreatePipe)
			{
			close(fd[2][1]);
			*errFD = fd[2][0];
			}

		if (childPID == NULL)
			{
			return JWaitForChild(pid);
			}
		else
			{
			*childPID = pid;
			return JNoError();
			}
		}
}

/******************************************************************************
 JWaitForChild

	Wait until a child process finishes.  If !block and no child has
	finished, *pid=0.

	status can be NULL.

 ******************************************************************************/

JError
JWaitForChild
	(
	const JBoolean	block,
	pid_t*			pid,
	int*			status
	)
{
	int err;
	do
		{
		jclear_errno();
		*pid = waitpid(-1, status, (block ? 0 : WNOHANG));
		err = jerrno();
		}
		while (err == EINTR);

	return JNoError();
}

JError
JWaitForChild
	(
	const pid_t	pid,
	int*		status
	)
{
	int err;
	do
		{
		jclear_errno();
		waitpid(pid, status, 0);
		err = jerrno();
		}
		while (err == EINTR);

	return JNoError();
}

/******************************************************************************
 JDecodeChildExitReason

	Returns one of:

		kJChildFinished  -- *result contains return value from main()
		kJChildSignalled -- *result contains signal that was sent
		kJChildStopped   -- *result contains signal that was sent

 ******************************************************************************/

JChildExitReason
JDecodeChildExitReason
	(
	const int	status,
	int*		result
	)
{
	if (WIFEXITED(status))
		{
		*result = WEXITSTATUS(status);
		return kJChildFinished;
		}
	else if (WIFSIGNALED(status))
		{
		*result = WTERMSIG(status);
		return kJChildSignalled;
		}
	else if (WIFSTOPPED(status))
		{
		*result = WSTOPSIG(status);
		return kJChildStopped;
		}

	*result = 0;
	return kJChildFinished;
}

/******************************************************************************
 JSendSignalToChild

 ******************************************************************************/

JError
JSendSignalToChild
	(
	const pid_t	pid,
	const int	signal
	)
{
	jclear_errno();
	if (kill(pid, signal) == 0)
		{
		return JNoError();
		}

	const int err = jerrno();
	if (err == EINVAL)
		{
		return JInvalidSignal();
		}
	else if (err == ESRCH)
		{
		return JInvalidProcess();
		}
	else if (err == EPERM)
		{
		return JCanNotSignalProcess();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JSendSignalToGroup

 ******************************************************************************/

JError
JSendSignalToGroup
	(
	const pid_t	pgid,
	const int	signal
	)
{
	jclear_errno();
	if (killpg(pgid, signal) == 0)
		{
		return JNoError();
		}

	const int err = jerrno();
	if (err == EINVAL)
		{
		return JInvalidSignal();
		}
	else if (err == ESRCH)
		{
		return JInvalidProcess();
		}
	else if (err == EPERM)
		{
		return JCanNotSignalProcess();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JGetPGID

	On FreeBSD, getpgid() isn't implemented because POSIX decided that
	it wasn't necessary since shells don't need it!  Morons!

	So we use the proc file system's 'status' file, which has the format

		name pid ppid pgid ...

	FreeBSD implementation written by Ivan Pascal <pascal@info.tsu.ru>

 ******************************************************************************/

#if defined __FreeBSD__

// This was never tested with PowerPC linux, so it remains undefined
// until somebody wants to test it!  I seriously doubt that the format
// of /proc files are the same as FreeBSD.
//
// || (defined __linux__ && defined __powerpc__)

JError
JGetPGID
	(
	const pid_t	pid,
	pid_t*		pgid
	)
{
	jclear_errno();

	JString fileName = "/proc/";
	fileName += JString(pid, 0);
	fileName += "/status";

	FILE* statusFile = NULL;
	const JError err = JFOpen(fileName, "r", &statusFile);
	if (!err.OK())
		{
		return err;
		}

	const int n = fscanf(statusFile, "%*s %*u %*u %u", pgid);
	if (n != 1)
		{
		return JUnexpectedError(jerrno());
		}

	fclose(statusFile);
	return JNoError();
}

#else

JError
JGetPGID
	(
	const pid_t	pid,
	pid_t*		pgid
	)
{
	jclear_errno();
	*pgid = getpgid(pid);
	if (*pgid != -1)
		{
		return JNoError();
		}

	const int err = jerrno();
	if (err == EINVAL || err == ESRCH)
		{
		return JInvalidProcess();
		}
	else if (err == EPERM)
		{
		return JProcessAccessDenied();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

#endif

/******************************************************************************
 JRunProgram

	This convenience function runs the specified command and blocks until
	it finishes.  If the program prints anything to stderr, *errOutput
	contains the text.

	If you don't want to block, use JSimpleProcess.

 ******************************************************************************/

JError
JRunProgram
	(
	const JCharacter*	cmd,
	JString*			errOutput
	)
{
	JProcess* p;
	int errFD;
	const JError err =
		JProcess::Create(&p, cmd,
						 kJIgnoreConnection, NULL,
						 kJIgnoreConnection, NULL,
						 kJCreatePipe, &errFD);
	if (err.OK())
		{
		// we let JReadAll() block because otherwise the pipe might get full

		JInPipeStream input(errFD, kTrue);
		JReadAll(input, errOutput);
		input.close();

		p->WaitUntilFinished();
		const JBoolean success = p->SuccessfulFinish();
		delete p;

		errOutput->TrimWhitespace();
		if (success)
			{
			return JNoError();
			}
		else
			{
			return JRunProgramError(*errOutput);
			}
		}
	else
		{
		errOutput->Clear();
		return err;
		}
}

/******************************************************************************
 JProgramOnPath

	Returns kTrue if the given program can be run by JExecute().

	This function only checks for full paths (starting with /).
	It ought to check for partial paths too, but it is insane to try
	to run a program specified by a partial path because one never
	knows what the working directory is.

 ******************************************************************************/

JBoolean
JProgramOnPath
	(
	const JCharacter* programName
	)
{
	JString s;
	return JProgramOnPath(programName, &s);
}

JBoolean
JProgramOnPath
	(
	const JCharacter*	programName,
	JString*			fixedName
	)
{
	*fixedName = programName;

	if (JStringEmpty(programName))
		{
		return kFalse;
		}
	else if (programName[0] == '/')
		{
		return JFileExecutable(programName);
		}

	JString fullName = programName;
	if (fullName.Contains("/"))
		{
		const JString dir = JGetCurrentDirectory();
		fullName          = JCombinePathAndName(dir, fullName);
		return JFileExecutable(fullName);
		}

	// check each directory in the exec path list

	const JCharacter* cpath = getenv("PATH");

	JString path(cpath == NULL ? "" : cpath);
	if (theIncludeCWDOnPathFlag)
		{
		path.Prepend(".:");
		}

	if (path.IsEmpty())
		{
		return kFalse;
		}

	JIndex colonIndex;
	while (path.LocateSubstring(":", &colonIndex))
		{
		if (colonIndex > 1)
			{
			const JString dir = path.GetSubstring(1, colonIndex-1);
			fullName          = JCombinePathAndName(dir, programName);
			if (JFileExists(fullName) && JFileExecutable(fullName))
				{
				if (dir == ".")
					{
					fixedName->Prepend("./");	// in case we added this to PATH
					}
				return kTrue;
				}
			}

		path.RemoveSubstring(1, colonIndex);
		}

	if (path.IsEmpty())
		{
		return kFalse;
		}

	fullName = JCombinePathAndName(path, programName);
	return JFileExecutable(fullName);
}

/******************************************************************************
 Include CWD on Path

	Turn this option on to automatically include ./ on the execution path.

 ******************************************************************************/

JBoolean
JWillIncludeCWDOnPath()
{
	return theIncludeCWDOnPathFlag;
}

void
JShouldIncludeCWDOnPath
	(
	const JBoolean includeCWD
	)
{
	theIncludeCWDOnPathFlag = includeCWD;
}
