// -------------------------------------------------------------------------------------
// ExecRunCommand.m
// (Indent:4, Tabs:4)
// -------------------------------------------------------------------------------------
// Copyright 1996 Persistent Technologies, Inc. - all rights reserved
// -------------------------------------------------------------------------------------
// This source code comes with no warranty of any kind, and the user assumes all 
// responsibility for its use.
// -------------------------------------------------------------------------------------
#import <appkit/appkit.h>
#import <libc.h>
#import <mach/cthreads.h>
#import <stdlib.h>
#import <stdarg.h>
#import <string.h>
#import <pwd.h>
#import <sys/types.h>
#import <sys/wait.h>
#import "ExecRunCommand.h"

// -------------------------------------------------------------------------------------
// pending pid list - keep track of child process creation and termination

/* pid list */
#define MAX_PIDLIST_SIZE	64	/* maximum concurrent child processes */
typedef struct {
	int			pid;
	int			didExit;
	union wait	status;
} pidStatus_t;
static pidStatus_t pidList[MAX_PIDLIST_SIZE] = { 0 };
static int pidCount = 0;

/* return pidList index of specified pid number */
static int _pid_index(int pid)
{
	if (pid > 0) {
		int n;
		for (n = 0; n < pidCount; n++) { if (pid == pidList[n].pid) return n; }
	}
	return -1; // not found
}

/* add pid to list (does not check to see if it already exists!) */
static int _pid_add(int thePid)
{
	int ndx;
	for (ndx = 0; (ndx < pidCount) && pidList[ndx].pid; ndx++);
	pidList[ndx].pid = thePid;
	pidList[ndx].didExit = NO;
	if (ndx == pidCount) pidCount++; // upper bounds (MAX_PIDLIST_SIZE) not checked
	return ndx;
}

/* wait for pid to exit (keep track of all pid exit reports) */
static BOOL _pid_wait(int thePid, union wait *rtnStat)
{
	int pidNdx = _pid_index(thePid);
	if (pidNdx < 0) pidNdx = _pid_add(thePid); //(ERROR) NOT FOUND - add anyway
	
	/* wait for pid to exit */
	if (!(pidList[pidNdx].didExit)) {
		for (;;) {
			union wait status;
			int pid = wait(&status);
			if (pid == -1) { pidNdx = 0; break; };
			pidNdx = _pid_index(pid);
			if (pidNdx < 0) pidNdx = _pid_add(pid); //(ERROR) NOT FOUND - add anyway
			pidList[pidNdx].didExit = YES;
			pidList[pidNdx].status = status;
			if (thePid == pid) break;
		}
	}
	
	/* return reported status */
	if (pidNdx >= 0) {
		*rtnStat = pidList[pidNdx].status;
		pidList[pidNdx].pid = 0;
		return YES;
	}
	return NO;

}

// -------------------------------------------------------------------------------------
// ExecRunCommand private methods
@interface ExecRunCommand(Private)
+ (struct passwd*)_getpwnam:(const char*)user;
+ (BOOL)_isRoot;
+ (int)_setenv:(char**)ep :(char*)eVal :(char*)fmt, ...;
- (BOOL)_runCommand:(const char*)command user:(const char*)user;
- (void)_sendSignal:(int)sigval;
- (void)_stopCommand;
- (int)_popen:(const char*)cmd user:(const char*)user;
- (int)_pclose;
- (void)_gotData;
static void gotData(int fd, void *self);
- (void)_commandOutput:(const char*)buf len:(int)len;
@end

// --------------------------------------------------------------------------------
@implementation ExecRunCommand 

// --------------------------------------------------------------------------------

/* initialize */
- init
{
	[super init];
	cmdChild = 0;
	inputDescriptor = 0;
	delegate = nil;
	tag = 0;
	return self;
}

/* free */
- _free:sender { return [self free]; }
- free
{
	if (cmdChild > 0) {
		fprintf(stderr,"cannot free yet, killing command ...\n");
		[self killCommand];
		[self perform:@selector(_free:) with:self afterDelay:500 cancelPrevious:YES];
		return (id)nil;
	}
	[super free];
	return (id)nil;
}

// --------------------------------------------------------------------------------

/* run command */
+ runCommand:(const char*)command user:(const char*)user output:(id)theDelegate
{
	BOOL didStart;
	ExecRunCommand *mySelf = [[self alloc] init];
	[mySelf setDelegate:theDelegate];
	didStart = [mySelf _runCommand:command user:user];
	if (!didStart) { [mySelf free]; mySelf = (id)nil; }
	return mySelf;
}

/* run command */
+ runCommand:(const char*)command output:(id)theDelegate
{
	return [ExecRunCommand runCommand:command user:(char*)nil output:theDelegate];
}

/* similar to "system(...)" */
+ (int)system:(const char*)command user:(const char*)user output:(id)theDelegate
{
	int err, infd;
	ExecRunCommand *mySelf = [[self alloc] init];
	[mySelf setDelegate:theDelegate];
	infd = [mySelf _popen:command user:user];
	if (infd >= 0) {
		int cnt;
		char buffer[1025];
		do {
			cnt = read(infd, buffer, sizeof(buffer));
			if (cnt != -1) [mySelf _commandOutput:buffer len:cnt];
		} while (cnt > 0);
		err = [mySelf _pclose];
		close(infd);
	} else err = RUNCMD_EXEC; // could not execute
	mySelf->cmdChild = 0;
	[mySelf free];
	return err;
}

/* see "system:user:output:" */
+ (int)system:(const char*)command user:(const char*)user
{
	return [self system:command user:user output:nil];
}

/* see "system:user:output:" */
+ (int)system:(const char*)command
{
	return [self system:command user:(char*)nil output:nil];
}

/* set tag */
- setTag:(int)theTag
{
	tag = theTag;
	return self;
}

/* return tag */
- (int)tag
{
	return tag;
}

/* return true if command is still active */
- (BOOL)isActive
{
	return (cmdChild > 0)? YES : NO;
}

/* set delegate */
- setDelegate:(id)theDelegate
{
	delegate = theDelegate;
	return self;
}

/* return delegate */
- (id)delegate
{
	return delegate;
}

/* interrupt command */
- interruptCommand
{
    [self _sendSignal:SIGINT];
	return self;
}

/* terminate command */
- terminateCommand
{
    [self _sendSignal:SIGTERM];
	return self;
}
  
/* kill command */
- killCommand
{
    [self _sendSignal:SIGKILL];
	return self;
}

/* return true if running with effective user root */
+ (BOOL)isRunningAsRoot
{
	return [self _isRoot];
}

// --------------------------------------------------------------------------------

/* wrapper for getpwnam() */
+ (struct passwd*)_getpwnam:(const char*)user
{
	extern void _lu_setport(port_t);
	extern port_t _lookupd_port(int);
	_lu_setport(_lookupd_port(0)); // may not be necessary post v3.0
	return user? getpwnam(user) : getpwuid(getuid());
}

/* remote: return 'root' flag */
+ (BOOL)_isRoot
{
	return ([ExecRunCommand _getpwnam:"root"]->pw_uid == geteuid())? YES : NO;
}

/* set environment variable */
+ (int)_setenv:(char**)ep :(char*)eVal :(char*)fmt, ...
{
	va_list			args;
	register char	*cp, *dp;
	va_start(args, fmt);
	vsprintf(eVal, fmt, args);
	va_end(args);
	for (;dp = *ep; ep++) {
		for (cp = eVal; (*cp == *dp) && (*cp != '=') && *cp; cp++, dp++) continue;
		if (((*cp == '=') || !*cp) && ((*dp == '=') || !*dp)) { *ep = eVal; return 0; }
	}
	return -1;
}

// --------------------------------------------------------------------------------

/* execute a command */
- (BOOL)_runCommand:(const char*)command user:(const char*)user
{
	inputDescriptor = [self _popen:command user:user];
	if (inputDescriptor >= 0) {
		DPSAddFD(inputDescriptor, gotData, self, NX_BASETHRESHOLD);
		return YES;
	}
	return NO;
}

/* send signal */
- (void)_sendSignal:(int)sigval
{
	if (cmdChild > 0) {
		killpg(cmdChild, sigval);
		kill(cmdChild, sigval);
	}
}

/* stop command */
- (void)_stopCommand
{
	int	error;
	DPSRemoveFD(inputDescriptor);
	error = [self _pclose];
	close(inputDescriptor);
	[self commandDidCompleteWithError:error];
}

/* open pipe to shell and execute command */
- (int)_popen:(const char*)cmd user:(const char*)user
{
	int			inputP[2], hisOutput, myInput;
	const char	**locEnv = environ;

	/* only "root" can specify a user */
	if (user && ![[self class] _isRoot]) return -1;
	
	/* "root" user commands must be preceeded with "{root}..." */
	if (user && !strcmp(user,"root")) {
		const char *match = "{root}";
		int matchLen = strlen(match);
		if (strncmp(cmd,match,matchLen)) return -1;
		cmd += matchLen;
	}

	/* open pipe */
	pipe(inputP);
	myInput = inputP[0];
	hisOutput = inputP[1];

	/* fork and execute shell */
	if ((cmdChild = vfork()) == 0) {
		int i;
		char **env;
		setpgrp(0, getpid());

		/* set up pipe handles */
		close(myInput);
		if (hisOutput != 1) dup2(hisOutput, 1);
		if (hisOutput != 2) dup2(hisOutput, 2);
		if ((hisOutput != 1) && (hisOutput != 2)) close(hisOutput);

		/* make local copy of environment table */
		for (i = 0; locEnv[i]; i++);
		env = (char**)alloca(sizeof(char*) * (i + 2 + 1)); // allocate on stack
		memset(env, 0,       sizeof(char*) * (i + 2 + 1));
		memcpy(env, locEnv,  sizeof(char*) * i);

		/* switch to user */
		if ([ExecRunCommand _isRoot]) {
			struct passwd *pw = [ExecRunCommand _getpwnam:user];
			if ((setgid(pw->pw_gid) < 0)             ||
				(initgroups(pw->pw_name,pw->pw_gid)) ||
				(setuid(pw->pw_uid) < 0))              {
				_exit(RUNCMD_USER); // cannot switch to user
			}
			[ExecRunCommand _setenv:env:(char*)alloca(strlen(pw->pw_dir)+7)
				:"HOME=%s",pw->pw_dir];
			[ExecRunCommand _setenv:env:(char*)alloca(strlen(pw->pw_name)+7)
				:"USER=%s",pw->pw_name];
		}

		/* execute command */
		// execle("/bin/csh","csh","-f","-c",cmd,(char*)nil,env);
		execle("/bin/sh","sh","-c",cmd,(char*)nil,env);
		_exit(RUNCMD_EXEC);

	}
	
	/* set io */
	if (cmdChild == -1) { close(myInput); myInput = -1; } else _pid_add(cmdChild);
	close(hisOutput);
	
	return myInput;
}

/* close shell command pipe */
- (int)_pclose
{
	union wait status;
	int omask = sigblock(sigmask(SIGINT)|sigmask(SIGQUIT)|sigmask(SIGHUP));
	BOOL flag = _pid_wait(cmdChild,&status);
	(void)sigsetmask(omask);
	if (!flag) return 0; // no children
	if (status.w_status & 0xFF) return RUNCMD_STOPPED; // process was terminated
	return (status.w_status >> 8) & 0xFF;
}

/* filter for data piped from command shell */
- (void)_gotData
{
	char	data[1024];
	int		n, cnt = 0;

	/* read available text */
	do {
		if ((n = read(inputDescriptor,data,sizeof(data))) >= 0) {
			cnt += n;
			if (n) [self _commandOutput:data len:n];
		}
    } while (n == sizeof(data));

	/* stop command when done */
	if (!cnt) [self _stopCommand];
	
}

/* fd routine for receiving data */
static void gotData(int fd, void *self)
{
	[(ExecRunCommand*)self _gotData];
}

// --------------------------------------------------------------------------------
// support for remote shell server output

/* copy text to scrollView (MAIN THREAD ONLY!) */
- (void)_commandOutput:(const char*)buf len:(int)len
{
	if (len && buf) {
		if (delegate && [delegate respondsTo:@selector(commandOutput:buffer:len:)]) {
			[delegate commandOutput:self buffer:buf len:len];
		}
	}
}

/* copy text to scrollView (MAIN THREAD ONLY!) */
- (oneway void)commandOutput:(const char*)buf len:(int)len
{
	if (len && buf) {
		[self _commandOutput:buf len:len];
		free((char*)buf);
	}
}

/* indicate that the shell has completed */
- (oneway void)commandDidCompleteWithError:(int)errorCode;
{
	if (delegate && [delegate respondsTo:@selector(commandDidComplete:withError:)]) {
		[delegate commandDidComplete:self withError:errorCode];
	}
	cmdChild = 0;
	[self free];
}

@end
