//
//	MiscSubprocess.m -- a Obj-C wrapper around Unix child processes
//		Originally written by Drew Davidson (c) 1994 by Drew Davidson.
//		Modified by Don Yacktman for inclusion into the MiscKit.
//				Version 1.0.  All rights reserved.
//		This notice may not be removed from this source code.
//
//	This object is included in the MiscKit by permission from the author
//	and its use is governed by the MiscKit license, found in the file
//	"LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
//	for a list of all applicable permissions and restrictions.
//	

/*----------------------------------------------------------------------------
	$Source$

	SYNOPSIS
		Handles a subprocess that is fork()'ed and handles delegate notification
	of events that occur such as output and errors.

	From Subprocess example by Charles L. Oei
									pty support by Joe Freeman
									with encouragement from Kristofer Younger
									Subprocess Example, Release 2.0
									NeXT Computer, Inc.
									
	Modified to support signals SIGSTOP and SIGCONT, and to wait for error
	code from terminated process, and to add delegate methods for the output by
	Drew Davidson
	
	REVISIONS
	$Log$
----------------------------------------------------------------------------*/
#import <libc.h>
#import <misckit/misckit.h>

extern int	wait4(int, union wait *, int, struct rusage *);
static void	stdoutFdHandler(int theFd, id self);
static void	stderrFdHandler(int theFd, id self);

#define PIPE_ERROR	"Error starting UNIX pipes to subprocess."
#define VFORK_ERROR	"Error starting UNIX vfork of subprocess."
#define EXEC_ERROR	"Error starting UNIX exec of subprocess."

@interface MiscSubprocess(private)

- _childDidExit;
- _stdoutBuffer;
- _stderrBuffer;
- _fdHandler:(int)theFd method:(SEL)aSelector buffer:aString;
- _setRunning:(BOOL)yn;

@end

@implementation MiscSubprocess

/*----------------------------< PRIVATE METHODS >----------------------------*/
/*
 * cleanup after a child process exits
 */
- _childDidExit
{	union wait				w;
	int						status = 0;
	MiscSubprocessEndCode	code = Misc_UnknownEndCode;
	
	if (wait4(childPid,&w,WUNTRACED,NULL) > 0)
	{	IMP		done;
	
		if (WIFEXITED(w))
		{	code = Misc_Exited;
			status = w.w_retcode;
		}
		else
		{	if (WIFSTOPPED(w))
			{	code = Misc_Stopped;
				status = w.w_stopsig;
			}
			else
			{	if (WIFSIGNALED(w))
				{	code = Misc_Signaled;
					status = w.w_termsig;
				}
			}
		}
		DPSRemoveFD(stdoutFromChild);
		DPSRemoveFD(stderrFromChild);
		fclose(fpFromChild);
		close(stdoutFromChild);
		close(stderrFromChild);
		fclose(fpToChild);
		running = NO;
		done = [delegate methodFor:@selector(subprocess:done::)];
		done(delegate,@selector(subprocess:done::),self,status,code);
	}
	return(self);
}

- _stdoutBuffer
{
	return(stdoutBuffer);
}

- _stderrBuffer
{
	return(stderrBuffer);
}

/*
 * DPS handler for output from subprocess
 */
- _fdHandler:(int)theFd method:(SEL)aSelector buffer:aString
{	int			bufferCount,
					maxLength;

	do
	{	int			currentLength = [aString strlen];
		
		maxLength = [aString capacity] - currentLength;
		bufferCount = read(theFd,[aString buffer] + currentLength,maxLength);
		if (bufferCount <= 0)
		{	if (currentLength > 0)
				[self flushBuffer:aString];
			[self _childDidExit];
			return(self);
		}
		[aString fixStringLengthAt:(bufferCount + currentLength)];
		if (bufferCount == maxLength)
			[aString setCapacity:([aString capacity] * 2.0)];
	} while (bufferCount == maxLength);
	[self flushBuffer:aString];
	return(self);
}

/*
 * DPS handler for output from subprocess
 */
static void stdoutFdHandler(int theFd,id self)
{
	[self _fdHandler:theFd method:@selector(subprocess:output:) buffer:[self _stdoutBuffer]];
}

static void stderrFdHandler(int theFd,id self)
{
	[self _fdHandler:theFd method:@selector(subprocess:stderrOutput:) buffer:[self _stderrBuffer]];
}

- _setRunning:(BOOL)yn
{
	running = yn;
	return(self);
}

/*---------------------------< INIT/FREE METHODS >---------------------------*/
- init
{
	return([self init:NULL]);
}

- init:(const char *)aString
{
	return([self init:aString withDelegate:nil]);
}

- init:(const char *)aString withDelegate:theDelegate
{
	[super init];
	
	stdoutBuffer = [[MiscString allocFromZone:[self zone]] initCapacity:BUFSIZ];
	stderrBuffer = [[MiscString allocFromZone:[self zone]] initCapacity:BUFSIZ];
	environment = [[MiscStringArray allocFromZone:[self zone]] init];
	[self setDelegate:theDelegate];
	if (aString)
		[self execute:aString];
	return(self);
}

- free
{
	[self terminate:self];
	[stdoutBuffer free];
	[stderrBuffer free];
	[environment free];
	return([super free]);
}

/*-----------------------------< OTHER METHODS >-----------------------------*/
- execute:(const char *)aString
{	int		pipeTo[2];
	int		pipeFrom[2];
	int		pipeStderr[2];	/* for stderr to different fd */
	int		numFds,
				fd;
	int		processGroup;
	char		hail[BUFSIZ];
	
	if ([self isRunning])
		return(nil);
	if (pipe(pipeTo) < 0 || pipe(pipeFrom) < 0 || pipe(pipeStderr) < 0)
	{	[delegate perform:@selector(subprocess:error:) with:self with:(void *)PIPE_ERROR];
		return(nil);
	}
	
	switch (childPid = vfork())
	{	case -1:						/* error */
			[delegate perform:@selector(subprocess:error:) with:self with:(void *)VFORK_ERROR];
			return(self);
			break;
		case 0:						/* child */
			dup2(pipeTo[0], 0);
			dup2(pipeFrom[1], 1);			/* get stdout from process */
			dup2(pipeStderr[1], 2);			/* get stderr here */
			
			numFds = getdtablesize();
			for (fd = 3; fd < numFds; fd++)
				close(fd);
			
			processGroup = getpid();
			ioctl(0,TIOCSPGRP,(char *)&processGroup);
			setpgrp(0,processGroup);
			
			fgets(hail,BUFSIZ,stdin);
			
			[self execChild:aString];
			[delegate perform:@selector(subprocess:error:) with:self with:(void *)EXEC_ERROR];
			[self error:"vfork (child) returned!"];
			break;
		default:					/* parent */
			close(pipeTo[0]);
			close(pipeFrom[1]);
			close(pipeStderr[1]);

			fpToChild = fdopen(pipeTo[1], "w");
			stdoutFromChild = pipeFrom[0];
			fpFromChild = fdopen(pipeFrom[0],"r");
				
			stderrFromChild = pipeStderr[0];
	
			/*
			 * Set buffering method, also make it use its own buffers
			 */
			setbuf(fpToChild,NULL);					/* no buffering */
			setbuf(fpFromChild,NULL);
			DPSAddFD(stdoutFromChild,(DPSFDProc)stdoutFdHandler,self,NX_MODALRESPTHRESHOLD+1);
			DPSAddFD(stderrFromChild,(DPSFDProc)stderrFdHandler,self,NX_MODALRESPTHRESHOLD+1);
			running = YES;
			fputs("Run, you bastard!\n",fpToChild);
			break;
	}
	return(self);
}

- execChild:(const char *)aString
{
	/*
	 * we exec a /bin/sh so that cmds are easier to specify for the user
	 */
	execle("/bin/sh", "sh", "-c", aString, 0, [environment stringArray]);
	return(self);
}

- setDelegate:anObject
{
	delegate = anObject;
	return(self);
}

- delegate
{
	return(delegate);
}

- environment
{
	return(environment);
}

- (SEL)outputMethodForBuffer:aBuffer
{	SEL		aSelector;

	if (aBuffer == [self _stdoutBuffer])
		aSelector = @selector(subprocess:output:);
	else
	{	if (aBuffer == [self _stderrBuffer])
			aSelector = @selector(subprocess:stderrOutput:);
		else
			aSelector = NULL;
	}
	return(aSelector);
}

- flushBuffer:aString as:aBuffer
{
	if ([aString strlen] > 0)
	{	SEL		aSelector = [self outputMethodForBuffer:aBuffer];
	
		if (aSelector)
			[delegate perform:aSelector with:self with:(id)[aString stringValue]];
		[aString freeString];
	}
	return(nil);
}

- flushBuffer:aString
{
	return([self flushBuffer:aString as:aString]);
}

- send:(const char *)string withNewline:(BOOL)wantNewline
{
	fputs(string,fpToChild);
	if (wantNewline)
		fputc('\n',fpToChild);
	return(self);
}

- send:(const char *)string
{
	[self send:string withNewline:YES];
	return(self);
}

/*
 * Returns the process id of the process (and therefore the process group
 * of the job)
 */
- (int)pid
{
	return(childPid);
}

- pause:sender
{
	if (!paused)
	{	killpg(childPid,SIGSTOP);			/* pause the process group */
		paused = YES;
	}
	return(self);
}

- resume:sender
{
	if (paused)
	{	killpg(childPid,SIGCONT);			/* resume the process group */
		paused = NO;
	}
	return(self);
}

- (BOOL)isPaused
{
	return(paused);
}

- (BOOL)isRunning
{
	return(running);
}

- terminate:sender
{
	if (running)
		killpg(childPid,SIGKILL);
	return(self);
}

/*
 * effectively sends an EOF to the child process stdin
 */
- terminateInput
{
	fclose(fpToChild);
	return(self);
}

@end

@implementation Object(MiscSubprocessDelegate)

- subprocess:sender done:(int)status :(MiscSubprocessEndCode)code
{
	return(self);
}

- subprocess:sender output:(const char *)buffer
{
	return(self);
}

- subprocess:sender stderrOutput:(const char *)buffer
{
	return(self);
}

- subprocess:sender error:(const char *)errorString
{
	perror(errorString);
	return(self);
}

@end