//
//	MiscSubprocess.m -- a Obj-C wrapper around Unix child processes
//
//		Modified by Don Yacktman for inclusion into the MiscKit.
//		Fixed up by Carl Lindberg, Don Yacktman, and Steve Hayman.
//				Version 1.3.  All rights reserved.
//		This notice may not be removed from this source code.
//

/*----------------------------------------------------------------------------
	$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 (removed) 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
	
	Modified for MiscKit inclusion by Don Yacktman
	Debugged by Carl Lindberg, Don Yacktman, Steve Hayman
	PTY support added back in by Carl Lindberg
	Synchronous operation by Steve Hayman
                          revised by Nicholas Droux and Carl Lindberg

	Revised and converted to FoundationKit by Carl Lindberg

	REVISIONS
	$Log$
----------------------------------------------------------------------------*/
#import "MiscSubprocess.h"
#import <libc.h>
#import <foundation/NSString.h>
#import <foundation/NSData.h>
#import <foundation/NSDictionary.h>
#import <foundation/NSException.h>
#import <dpsclient/dpsNeXT.h>
#import <appkit/Application.h>  // for NX_MODALRESPTHRESHOLD

extern int errno;
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."
#define PTY_ERROR      @"Error grabbing ptys for subprocess."

#define	PTY_TEMPLATE   "/dev/pty??"
#define	PTY_LENGTH     11


static void getptys(int *master, int *slave) // attempt to setup the ptys
{
    char device[PTY_LENGTH];
    char *block, *num;
    char *blockLoc; // specifies the location of block for the device string
    char *numLoc;   // specifies the pty name with the digit ptyxD
    char *msLoc;    // specifies the master (ptyxx) or slave (ttyxx)
    
    struct sgttyb setp =
		{B9600, B9600, (char)0x7f, (char)0x15, (CRMOD|ANYP)};
    struct tchars setc =
		{CINTR, CQUIT, CSTART, CSTOP, CEOF, CBRK};
    struct ltchars sltc =
		{CSUSP, CDSUSP, CRPRNT, CFLUSH, CWERASE, CLNEXT};
    int	lset =
		(LCRTBS|LCRTERA|LCRTKIL|LCTLECH|LPENDIN|LDECCTQ);
    int	setd = NTTYDISC;
    
    strcpy(device, PTY_TEMPLATE); // string constants are not writable
    blockLoc = &device[ strlen("/dev/pty") ];
    numLoc = &device[ strlen("/dev/pty?") ];
    msLoc = &device[ strlen("/dev/") ];

    for (block = "pqrs"; *block; block++)
    {
		*blockLoc = *block;
		for (num = "0123456789abcdef"; *num; num++)
		{
		    *numLoc = *num;
		    *master = open(device, O_RDWR);
		    if (*master >= 0)
		    {
				*msLoc = 't';
				*slave = open(device, O_RDWR);
				if (*slave >= 0)
				{
				    (void) ioctl(*slave, TIOCSETP, (char *)&setp);
				    (void) ioctl(*slave, TIOCSETC, (char *)&setc);
				    (void) ioctl(*slave, TIOCSETD, (char *)&setd);
				    (void) ioctl(*slave, TIOCSLTC, (char *)&sltc);
				    (void) ioctl(*slave, TIOCLSET, (char *)&lset);
				    return;
				}
				//  Change it back if the open  of the "slave" of the pty fails.
				*msLoc = 'p';	// to cause it to open the master end of pty
								// the next time through the loop.
		    }
		} /* hunting through a bank of ptys */
    } /* hunting through blocks of ptys in all the right places */

    *master = -1;
    *slave = -1;
}

@interface MiscSubprocess(private)

- _childDidExit;
- _setRunning:(BOOL)yn;
- (void)_handleFd:(int)theFd delegateOutputMethod:(SEL)outputMethod;
- (void)_messageDelegateWith:(SEL)aSelector :(id)obj1 :(id)obj2;
- (void)_addCurrentEnvironment;

@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;
		}
		
		if ( dpsStdoutFromChild >= 0 ) DPSRemoveFD(dpsStdoutFromChild);
		if ( dpsStderrFromChild >= 0 ) DPSRemoveFD(dpsStderrFromChild);
		
		fclose(fpFromChild);
		close(stdoutFromChild);
		close(stderrFromChild);
		fclose(fpToChild);
		running = NO;

		if (delegate && [delegate respondsToSelector:@selector(subprocess:done::)])
		{
            /*
             * This would generate a warning if I called [delegate
             * subprocess:done::] directly, so I do it this way.  You could
             * normally use perform:withObject: to get around this, but
             * this has three parameters.  I should probably just put the
             * @interface NSObject (MiscSubprocessDelegate) section back in
             * instead, but, well...
             */
			done = [delegate methodForSelector:@selector(subprocess:done::)];
			done(delegate,@selector(subprocess:done::),self,status,code);
		}
	}

	// send error to delegate if wait4 returned value < 0 ?

	return self;
}


static NSData *dataFromFd(int theFd)
{
	int				numBytesRead;
	int				readOffset = 0;
	int				bufferSize = 1024;
	char			*readBuffer = malloc(bufferSize+1);
	BOOL			finishedReading = NO;


	do {
		numBytesRead = read(theFd, readBuffer+readOffset, bufferSize-readOffset);
		
		if (numBytesRead >=0) readBuffer[readOffset+numBytesRead] = 0;

		finishedReading = (numBytesRead == bufferSize-readOffset)? NO:YES;

		if (!finishedReading) {
			readOffset += numBytesRead;
			bufferSize *= 3;
			readBuffer = (char*)realloc(readBuffer, bufferSize+1);
		}

	} while (!finishedReading);

	if (readOffset+numBytesRead > 0)
	{
		readBuffer = (char *)realloc(readBuffer, readOffset+numBytesRead);
		return [NSData dataWithBytesNoCopy:readBuffer length:readOffset+numBytesRead];
	}
	else
	{
		free(readBuffer);
		return nil;
	}
	
}


- (void)_handleFd:(int)theFd doneIndicator:(BOOL *)doneIndicator
		outputSelector:(SEL)outputSelector dataOutputSelector:(SEL)dataOutputSelector
{
	NSData			*outputData;
	
	outputData = dataFromFd(theFd);
	
	if ([outputData length] <= 0)  // this means that the Fd has finished outputting
	{
		*doneIndicator = YES;	// *doneIndicator is either stdoutIsDone or stderrIsDone
		if (stdoutIsDone && stderrIsDone)
		{
			[self _childDidExit];
		}
	}
	else
	{
		[self _messageDelegateWith:dataOutputSelector :self :outputData];

		if (delegate && [delegate respondsToSelector:outputSelector])
		{
			NSString *outputString = [NSString stringWithCString:[outputData bytes]
			                                              length:[outputData length]];
			[self _messageDelegateWith:outputSelector :self :outputString];
		}
	}
}

- (void)_handleStdoutFd:(int)theFd
{
	[self _handleFd:theFd doneIndicator:&stdoutIsDone
		outputSelector:@selector(subprocess:output:)
		dataOutputSelector:@selector(subprocess:outputData:)];
}

- (void)_handleStderrFd:(int)theFd
{
	[self _handleFd:theFd doneIndicator:&stderrIsDone
		outputSelector:@selector(subprocess:stderrOutput:)
		dataOutputSelector:@selector(subprocess:stderrOutputData:)];
}

/*
 * DPS handler for output from subprocess
 */
static void stdoutFdHandler(int theFd, id self)
{
	[self _handleStdoutFd:theFd];
}
static void stderrFdHandler(int theFd, id self)
{
	[self _handleStderrFd:theFd];
}

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

/*---------------------------< INIT/FREE METHODS >---------------------------*/

- init:(NSString *)aString withDelegate:theDelegate
		keepEnvironment:(BOOL)keepEnv withPtys:(BOOL)ptyFlag
		asynchronously:(BOOL)async
{
	[super init];

	usePtys = ptyFlag;
	asynchronous = async;
	dpsStdoutFromChild = dpsStderrFromChild = -1;	// DPSAddFd() not in use yet
	stdoutIsDone = stderrIsDone = NO;

	environment = [[NSMutableDictionary alloc] init];
	if (keepEnv) [self _addCurrentEnvironment];

	[self setDelegate:theDelegate];
	[self setExecArgs:@"/bin/sh" :@"sh" :@"-c"];
	if (aString)
		[self execute:aString withPtys:usePtys asynchronously:asynchronous];
	return self;
}

- (void)dealloc
{
	[self terminate:self];
	[self _childDidExit];	// does DPSRemoveFd for us

	[environment release];
	[execArg1 release];
	[execArg2 release];
	[execArg3 release];
	[super dealloc];
}

/*-----------------------------< OTHER METHODS >-----------------------------*/

- execute:(NSString *)aString withPtys:(BOOL)ptyFlag asynchronously:(BOOL)async
{
	int		pipeTo[2], pipeFrom[2], pipeStderr[2]; // for stderr to different fd
	int		tty, numFds, fd, processGroup;
	char	hail[BUFSIZ];
	NSString *errorString;

	if ([self isRunning]) return nil;

	if (ptyFlag)
	{
		tty = open("/dev/tty", O_RDWR);
		getptys(&masterPty,&slavePty);
		if (masterPty <= 0 || slavePty <= 0)
		{
			[self _messageDelegateWith:@selector(subprocess:error:) :self :PTY_ERROR];
			return nil;
		}
		if (pipe(pipeStderr) < 0)
		{
			errorString = [NSString stringWithFormat:@"%@: %s", PIPE_ERROR, strerror(errno)];
			[self _messageDelegateWith:@selector(subprocess:error:) :self :errorString];
			return nil;
		}

		/*
		 * remove the controlling tty if launched from a shell, but not Workspace;
		 * so that we have job control over the parent application in shell
		 * and so that subprocesses can be restarted in Workspace
		 */
		if  ((tty<0) && ((tty = open("/dev/tty", 2))>=0)) {
			ioctl(tty, TIOCNOTTY, 0);
			close(tty);
		}
	}
	else
	{
		if (pipe(pipeTo) < 0 || pipe(pipeFrom) < 0 || pipe(pipeStderr) < 0)
		{
			errorString = [NSString stringWithFormat:@"%@: %s", PIPE_ERROR, strerror(errno)];
			[self _messageDelegateWith:@selector(subprocess:error:) :self :errorString];
			return nil;
		}
	}
	
	
	switch (childPid = vfork()) {
		case -1:							/* error */
			errorString = [NSString stringWithFormat:@"%@: %s", VFORK_ERROR, strerror(errno)];
			[self _messageDelegateWith:@selector(subprocess:error:) :self :errorString];
			return self;
			break;
		case 0:								/* child */
			if (ptyFlag) {
				dup2(slavePty, 0);
				dup2(slavePty, 1);
				dup2(pipeStderr[1], 2);  //stderr
			}
			else {
				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);		// wait for parent

			[self execChild:aString];

			/* Shouldn't get to here */
			[self _messageDelegateWith:@selector(subprocess:error:) :self :EXEC_ERROR];
			[NSException raise:NSGenericException format:@"vfork (child) returned!"];

			break;

		default:							/* parent */
			if (ptyFlag) {
				close(slavePty);
				close(pipeStderr[1]);
				fpToChild = fdopen(masterPty,"w");
				stdoutFromChild = masterPty;
				stderrFromChild = pipeStderr[0];
			}
			else {
				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 */
			if (!ptyFlag) setbuf(fpFromChild, NULL);

			if ( async )
			{
				// Don't read output now; Tell DPS to notify us when data is ready.
				DPSAddFD(dpsStdoutFromChild = stdoutFromChild,
						(DPSFDProc)stdoutFdHandler, self,
						NX_MODALRESPTHRESHOLD + 1);
				DPSAddFD(dpsStderrFromChild = stderrFromChild,
						(DPSFDProc)stderrFdHandler, self,
						NX_MODALRESPTHRESHOLD + 1);
				running = YES;
				fputs("Run away!  Run away!\n", fpToChild); // tell child to go
			}
			else
			{  //synchronous
				// from Steve Hayman for synchronous processes...
				// modified by Nicolas Droux and Carl Lindberg
				// Tell the subprocess to go right now, we'll read all its
				// output until end-of-file and return when the subprocess
				// has exited.  We still fork, but we won't return to the
				// main thread until the child exits...

				fd_set readfds;
              	int highestFdNum;

				running = YES;				
              	dpsStdoutFromChild = dpsStderrFromChild = -1; //Not using DPSAddFd()

              	highestFdNum = stdoutFromChild > stderrFromChild ?
                  	           stdoutFromChild : stderrFromChild;

              	fputs("Run away!  Run away!\n", fpToChild);

				do {
					// set up the set of fd's we are using
					FD_ZERO(&readfds);
					FD_SET(stdoutFromChild, &readfds);
					FD_SET(stderrFromChild, &readfds);

					// select the ones that nead reading from
					select(highestFdNum+1,&readfds,NULL,NULL,NULL);

					if (FD_ISSET(stdoutFromChild, &readfds))
                     	stdoutFdHandler(stdoutFromChild, self);
                  	if (FD_ISSET(stderrFromChild, &readfds) && [self isRunning])
						stderrFdHandler(stderrFromChild, self);

                } while([self isRunning]);
				// The above condition is the same one that _fdHandler uses to
				// call _childDidExit, so that call has already been done by this point.
			}
			break;
	}

	return self;
}

- setExecArgs:(NSString *)a0 :(NSString *)a1 :(NSString *)a2
{
	[execArg1 autorelease];
	[execArg2 autorelease];
	[execArg3 autorelease];
	execArg1 = [a0 retain];
	execArg2 = [a1 retain];
	execArg3 = [a2 retain];

	return self;
}

- execChild:(NSString *)aString
{
	NSArray		*environKeys = [environment allKeys];
	const char	*envStrings[[environKeys count]+1];
	unsigned	i;
	
	for (i=0;i<[environKeys count]; i++) {
		NSString *currKey    = [environKeys objectAtIndex:i];
		NSString *currString = [NSString stringWithFormat:@"%@=%@", currKey, 
								[environment objectForKey:currKey]];
		envStrings[i] = [currString cString];
	}
	envStrings[[environKeys count]] = NULL;

	/*
	 * we exec a /bin/sh so that cmds are easier to specify for the user
	 * Unlike the old method, we don't automatically
     * use "/bin/sh", "sh", "-c" anymore
     */
    execle([execArg1 cString], [execArg2 cString], [execArg3 cString],
		 [aString cString], NULL, envStrings);
	 
    return self;	// shouldn't get here
}

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

- delegate
{
	return delegate;
}

- (NSMutableDictionary *)environment
{
	return environment;
}


- send:(NSString *)string withNewline:(BOOL)wantNewline
{
	fputs([string cString],fpToChild);
	if (wantNewline) fputc('\n',fpToChild);

	return self;
}

- send:(NSString *)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;
}

- (BOOL)usePtys { return usePtys; }
- setUsePtys:(BOOL)flag { usePtys = flag; return self; }

- (BOOL)asynchronous { return asynchronous; }
- setAsynchronous:(BOOL)flag { asynchronous = flag; return self; }

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

	return self;
}

// *DO NOT* send SIGSTOP or SIGCONT via this method -- use -pause: or -resume:
- sendSignal:(int)signalNum
{
	if (running)
		killpg(childPid,signalNum);
	
	return self;
}

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

- (void)_messageDelegateWith:(SEL)aSelector :(id)obj1 :(id)obj2
{
	if (delegate && [delegate respondsToSelector:aSelector])
	{
		[delegate perform:aSelector withObject:obj1 withObject:obj2];
	}
}

- (void)_addCurrentEnvironment
{
	int i;
	NSRange equalsRange;
	NSString *tempString;

	for (i=0; environ[i]; i++) {
		tempString  = [NSString stringWithCString:environ[i]];
		equalsRange = [tempString rangeOfString:@"="];
		if (equalsRange.length != 0) {
			[environment setObject:[tempString substringFromIndex:NSMaxRange(equalsRange)]
						    forKey:[tempString substringToIndex:equalsRange.location]];
		}
	}
}


/***************************/
/*** CONVENIENCE METHODS ***/
/***************************/

- init
{ return [self init:nil withDelegate:nil keepEnvironment:YES withPtys:NO asynchronously:YES]; }

- init:(NSString *)aString
{ return [self init:aString withDelegate:nil keepEnvironment:YES withPtys:NO asynchronously:YES]; }

- init:(NSString *)aString withDelegate:(id)theDelegate
{ return [self init:aString withDelegate:theDelegate keepEnvironment:YES withPtys:NO asynchronously:YES]; }

- init:(NSString *)aString keepEnvironment:(BOOL)envFlag
{ return [self init:aString withDelegate:nil keepEnvironment:envFlag withPtys:NO asynchronously:YES]; }

- init:(NSString *)aString withDelegate:(id)theDelegate keepEnvironment:(BOOL)envFlag
{ return [self init:aString withDelegate:theDelegate keepEnvironment:envFlag withPtys:NO asynchronously:YES]; }

- init:(NSString *)aString withPtys:(BOOL)ptyFlag
{ return [self init:aString withDelegate:nil keepEnvironment:YES withPtys:ptyFlag asynchronously:YES]; }

- init:(NSString *)aString withDelegate:(id)theDelegate withPtys:(BOOL)ptyFlag
{ return [self init:aString withDelegate:theDelegate keepEnvironment:YES withPtys:ptyFlag asynchronously:YES]; }

- init:(NSString *)aString keepEnvironment:(BOOL)envFlag withPtys:(BOOL)ptyFlag
{ return [self init:aString withDelegate:nil keepEnvironment:envFlag withPtys:ptyFlag asynchronously:YES]; }

- init:(NSString *)aString withDelegate:(id)theDelegate keepEnvironment:(BOOL)envFlag withPtys:(BOOL)ptyFlag
{ return [self init:aString withDelegate:theDelegate keepEnvironment:envFlag withPtys:ptyFlag asynchronously:YES]; }

- init:(NSString *)aString asynchronously:(BOOL)async
{ return [self init:aString withDelegate:nil keepEnvironment:YES withPtys:NO asynchronously:async]; }

- init:(NSString *)aString withDelegate:(id)theDelegate asynchronously:(BOOL)async
{ return [self init:aString withDelegate:theDelegate keepEnvironment:YES withPtys:NO asynchronously:async]; }

- init:(NSString *)aString keepEnvironment:(BOOL)envFlag asynchronously:(BOOL)async
{ return [self init:aString withDelegate:nil keepEnvironment:envFlag withPtys:NO asynchronously:async]; }

- init:(NSString *)aString withDelegate:(id)theDelegate keepEnvironment:(BOOL)envFlag asynchronously:(BOOL)async
{ return [self init:aString withDelegate:theDelegate keepEnvironment:envFlag withPtys:NO asynchronously:async]; }

- init:(NSString *)aString withPtys:(BOOL)ptyFlag asynchronously:(BOOL)async
{ return [self init:aString withDelegate:nil keepEnvironment:YES withPtys:ptyFlag asynchronously:async]; }

- init:(NSString *)aString withDelegate:(id)theDelegate withPtys:(BOOL)ptyFlag asynchronously:(BOOL)async
{ return [self init:aString withDelegate:theDelegate keepEnvironment:YES withPtys:ptyFlag asynchronously:async]; }

- init:(NSString *)aString keepEnvironment:(BOOL)envFlag withPtys:(BOOL)ptyFlag asynchronously:(BOOL)async
{ return [self init:aString withDelegate:nil keepEnvironment:envFlag withPtys:ptyFlag asynchronously:async]; }


- execute:(NSString *)aString
{ return [self execute:aString withPtys:NO asynchronously:YES]; }

- execute:(NSString *)aString withPtys:(BOOL)ptyFlag
{ return [self execute:aString withPtys:ptyFlag asynchronously:YES]; }

- execute:(NSString *)aString asynchronously:(BOOL)async
{ return [self execute:aString withPtys:NO asynchronously:async]; }



@end

/*
@implementation NSObject(MiscSubprocessDelegate)

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

- subprocess:sender output:(NSString *)output
{
	return self;
}

- subprocess:sender outputData:(NSData *)output
{
	return self;
}

- subprocess:sender stderrOutput:(NSString *)output
{
	return self;
}

- subprocess:sender stderrOutputData:(NSData *)output
{
	return self;
}

- subprocess:sender error:(NSString *)errorString
{
	NSLog(@"%@", errorString);
	return self;
}

@end
*/
