// -------------------------------------------------------------------------------------
// ExecMonitor.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 <libc.h>
#import <stdlib.h>
#import <c.h>
#import <errno.h>
#import <ctype.h>
#import <math.h>
#import <sys/param.h>
#import <sys/types.h>
#import <sys/time.h>
#import <sys/wait.h>
#import <sys/resource.h>
#import <dpsclient/dpsNeXT.h>
#import <appkit/Listener.h>
#import <appkit/appkit.h>
#import "PPPMonitor.h"
#import "ExecScrollText.h"
#import "ExecMonitor.h"

#define CHKPPPSOCK
#ifdef CHKPPPSOCK
#import <sys/socket.h>
#import <sys/ioctl.h>
#import <netdb.h>
#import <net/route.h>
#import <net/if.h>
#endif

// -------------------------------------------------------------------------------------
// local vars
static id globalMonitor = (id)nil;	// ExecMonitor instance
static int _testMode_ = 0;			// test mode flag

// -------------------------------------------------------------------------------------
// ppp 'system' commands
#define SYS_PPP_ISLOADED "/usr/etc/kl_util -s | /bin/grep ' ppp Loaded' "
#define SYS_PPP_ISUP     "/usr/etc/ifconfig ppp0 | /bin/grep RUNNING "
#define SYS_PPPD_RUNNING "ps -ax | egrep '(/pppd )' | egrep -v 'egrep' "

// -------------------------------------------------------------------------------------
// play sounds
#define PLAY(S)				{ if (S) [[Sound findSoundFor:(S)] play]; }

// -------------------------------------------------------------------------------------
// network applications (default when "MiniShelf" default-db is not available)
#define MAX_SHELFSIZE		10
static const char *netAppList = (char*)0;
static const char *netApps[MAX_SHELFSIZE + 1] = { (char*)0 };
static int netAppCount = 0;

// -------------------------------------------------------------------------------------
// misc defines
#define LCURL				'{'
#define RCURL				'}'
#define LPREN				'('
#define RPREN				')'
#define SYSTEM(C)			[ExecRunCommand system:(C)]

// -------------------------------------------------------------------------------------
// Mini-Shelf icon cell

@interface TileButtonCell : ButtonCell 
{ BOOL _noDots; }
- (void)setDrawDots:(BOOL)flag;
@end

/* highlight rectangle */
static void _hiliteRect(const NXRect *r)
{
	PSgsave();
	PSsetalpha(0.5);
	PSsetgray(NX_WHITE);
	PScompositerect(r->origin.x,r->origin.y,r->size.width,r->size.height,NX_SOVER);
	PSgrestore();
}

/* load special images used for minishelf */
static NXImage *miniShelfTile = nil, *shelfTile = nil;
static NXImage *miniShelfDots = nil, *shelfDots = nil;
static void _loadMiniShelfIcons(void)
{
	{
	const char *tile = ICO_SHELFTILE;
	BOOL _tile = (tile && !strcasecmp(tile,"yes"))? YES : NO;
	if (!miniShelfTile && _tile) miniShelfTile = [NXImage findImageNamed:"tile"]; 
	shelfTile = _tile? miniShelfTile : nil;
	}
	{
	const char *dots = ICO_SHELFDOTS;
	BOOL _dots = (dots && !strcasecmp(dots,"yes"))? YES : NO;
	if (!miniShelfDots && _dots) miniShelfDots = [NXImage findImageNamed:"dots"]; 
	shelfDots = _dots? miniShelfDots : nil;
	}
}

// -------------------------------------------------------------------------------------
// private functions

/* return interval timer value */
long _intervalTime(void)
{
	struct timeval tp;
	gettimeofday(&tp, NULL);
	return tp.tv_sec;
}

/* return start of command line */
const char *_cmdLine(const char *cmd)
{
	const char *n = cmd;
	while (*n && isspace(*n)) n++;
	if ((*n == LPREN) || (*n == LCURL)) {
		for (n++; *n && ((*n != RPREN) && (*n != RCURL)); n++);
		if (*n) n++;
	}
	while (*n && isspace(*n)) n++;
	return n;
}

/* return command name */
const char *_getCmdFile(char *buff, const char *cmd)
{
	const char *n2, *n1 = _cmdLine(cmd);
	for (n2 = n1; *n2 && !isspace(*n2); n2++); // scan for space
	strncpy(buff, n1, n2 - n1);
	buff[n2 - n1] = 0;
	return buff;
}

/* return command name */
const char *_getCmdName(char *buff, const char *cmd)
{
	const char *n = cmd;
	while (*n && isspace(*n)) n++;
	if ((*n == LPREN) || (*n == LCURL)) {
		char *b = buff;
		for (n++ ; *n && ((*n != RPREN) && (*n != RCURL)); n++) *b++ = *n;
		*b = 0;
	} else {
		char tmp[1024], *name = strrchr((char*)_getCmdFile(tmp,n),'/');
		strcpy(buff, (name?name+1:tmp));
	}
	return buff;
}

/* return true if specified file ends with ".app" */
BOOL _cmdIsApp(const char *cmd)
{
	char tmp[1024], *n = (char*)_getCmdFile(tmp,cmd);
	const char *dot = strrchr(n, '.');
	return (!dot || strcmp(dot,".app"))? NO : YES;
}

// -------------------------------------------------------------------------------------
@implementation ExecMonitor

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

/* show message */
- (void)message:(BOOL)isError:(const char*)fmt, ...
{
	char msg[1024], *m = msg;
	va_list args;
	if (isError) { m += strlen(strcpy(m,"ERROR: ")); }
	va_start(args, fmt);
	vsprintf(m, fmt, args);
	va_end(args);
	if (isError) {
		if ([cmdMessage shouldDrawColor]) [cmdMessage setTextColor:NX_COLORRED];
		else [cmdMessage setTextGray:NX_WHITE];
	} else {
		if ([cmdMessage shouldDrawColor]) [cmdMessage setTextColor:NX_COLORDKGRAY];
		else [cmdMessage setTextGray:NX_DKGRAY];
	}
	[cmdMessage setStringValue:msg];
}

// -------------------------------------------------------------------------------------
// polling/timer support

/* target/action structure */
typedef struct {
	BOOL			abort;
	id				self;
	SEL				meth;
	void			*arg;
	DPSTimedEntry	timerTag;
} _timerHandlerData_t;

/* periodic timer handler */
static void _timerHandler(DPSTimedEntry tag, double now, void *userData)
{
	_timerHandlerData_t *t = (_timerHandlerData_t*)userData;
	if (t->abort || ![t->self perform:t->meth with:(id)(t->arg)]) {
		[t->self perform:@selector(_stopTimer:) with:(id)t];
	}
}

/* start periodic timer */
- (_timerHandlerData_t*)_startTimer:(SEL)theSelector arg:(void*)arg freq:(float)freq
{
	_timerHandlerData_t *t = (_timerHandlerData_t*)malloc(sizeof(_timerHandlerData_t));
	t->abort    = NO;
	t->self     = self;
	t->meth     = theSelector;
	t->arg      = arg;
	t->timerTag = DPSAddTimedEntry((double)freq,_timerHandler,(void*)t,30);
	return t;
}

/* stop timer */
- (void)_stopTimer:(_timerHandlerData_t*)t
{
	if (t) {
		DPSRemoveTimedEntry(t->timerTag);
		free((void*)t);
	}
}

/* abbreviated delayed perform */
- (void)_perform:(SEL)sel:(id)objId delay:(int)delay
{
    [self perform:sel with:objId afterDelay:delay cancelPrevious:YES];
}

// -------------------------------------------------------------------------------------
// fill mini-shelf

/* fill mini-shelf */
- (void)_fillMiniShelf
{
	int i;
	static ButtonCell *tileProto = nil;

	/* reset/load shelf tile icon */
	_loadMiniShelfIcons();
	
	/* clear matrix */
	[appMatrix removeRowAt:0 andFree:YES];
	
	/* parse/count shelf apps */
	if (CMD_MINISHELF) {
		char *n = (char*)(netAppList=STRDUP(CMD_MINISHELF));
		netAppCount = 0;
		while (*n) {
			char *b1, *b2;
			while ((*n == ';') || isspace(*n)) *(n++) = 0;
			if (!*n) break;
			netApps[netAppCount++] = b1 = n;
			while (*n && (*n != ';')) n++;
			b2 = n - 1;
			if (*n) *n++ = 0;
			while ((b2 > b1) && isspace(*b2)) *b2-- = 0;
			if (netAppCount > MAX_SHELFSIZE) break;
		}
		netApps[netAppCount] = (char*)nil;
	}

	/* set prototype */
	if (!tileProto) {
//		ButtonCell *proto = [appMatrix prototype];
		tileProto = [[TileButtonCell alloc] initIconCell:"NXAppTile"];
		[tileProto setHighlightsBy:NX_NONE];
		[appMatrix setPrototype:tileProto];
	}
	
	/* create app buttons */
	[appMatrix renewRows:1 cols:netAppCount];
	for (i = 0; i < netAppCount; i++) {
		id btnCell = [appMatrix cellAt:0:i];
		if (!netApps[i]) { netAppCount = i; break; }
		if (_cmdIsApp(netApps[i])) {
			char buff[1024], *cmdFile = (char*)_getCmdFile(buff,netApps[i]);
			id appIcon = [[Application workspace] getIconForFile:cmdFile];
			[btnCell setIconPosition:NX_ICONONLY];
			[btnCell setImage:appIcon];
		} else {
			char cmdName[128];
			_getCmdName(cmdName,netApps[i]);
			[btnCell setIconPosition:NX_ICONABOVE];
			[btnCell setIcon:"cmdButton"];
			[btnCell setTitle:cmdName];
		}
		[btnCell setTag:i];
	}
	[appMatrix display];
	
}

/* called when new user defaults have been loaded */
+ (void)updateFromDefaults
{
	if (globalMonitor) [globalMonitor updateFromDefaults];
}

/* called when new user defaults have been loaded */
- (void)updateFromDefaults
{
	const char *showSec = FLG_SHOWSECONDS;
	if (showSec) {
		int val = atoi(showSec);
		_showTileTime = ((val >= 0) && (val <= 2))? val : 0;
	}
	[self _fillMiniShelf];
	[self enableConnectButton:nil];
	[self tailLogFile:self];
}

// -------------------------------------------------------------------------------------
// object initialization

/* crete single shared instance */
+ (id)sharedPPPMonitor
{
	if (!globalMonitor) globalMonitor = [[self alloc] init];
	return globalMonitor;
}

/* init */
- init
{

	/* init super */
	[super init];
	exeWindow = (id)nil;
	pppLogExeId = nil;
	connectExeId = nil;
	pingExeId = nil;
	_alreadyConnected = NO;
	_showTileTime = 0;
	_isConnected = NO;
	_isDisconnecting = NO;
	_shutDown = NO;
	_connectTime = 0L;
	
	/* clear timers */
	pppdTimer  = (void*)nil;
	clockTimer = (void*)nil;
	ppingTimer = (void*)nil;
	checkTimer = (void*)nil;

	/* test mode */
	{
	const char *val = [NSApp readDefault:"_TestMode_"];
	_testMode_ = (val && (*val == '1'))? 1 : 0;
	if (_testMode_) fprintf(stderr,"WARNING: PPPMonitor running in TEST mode.\n");
	}
	
	/* load nib */
	if (![NSApp loadNibSection:"ExecMonitor.nib" owner:self]) {
		[NSApp errorPanel:"Could not load nib file 'ExecMonitor.nib'\n"
			"(Check that the nib file exists and try again)"];
		[NSApp delayedFree:self];
		return nil;
	}
	
	/* spot-check nib loading */
	if (!exeWindow || !appMatrix) {
		[NSApp errorPanel:"'ExecMonitor.nib' did not load properly\n"
			"(The nib file has apparently been corrupted)"];
		[NSApp delayedFree:self];
		return nil;
	}
	
	/* initially clear messages */
	[pingMessage setStringValue:""];
	[self message:0:""];
	
	/* set connection state */
	if ([self isReallyConnected] || [self isPPPDRunning]) _alreadyConnected = YES;

	/* set mini-shelf target */
	[appMatrix setTarget:self];
	[appMatrix setDoubleAction:@selector(runApplication:)];
	[appMatrix setAction:(SEL)0];

	/* update default settings */
	[self updateFromDefaults];
	
	/* init panel */
	[exeWindow setFrameAutosaveName:[NSApp appName]];
	[exeWindow setDelegate:self];
	[exeWindow makeKeyAndOrderFront:(id)nil];

	/* already connected */
	if (_alreadyConnected) {
		[self message:1:"Already connected (connection not made by this PPPMonitor)"];
		[NSApp errorPanel:"Already connected:\n"
						"PPPMonitor does not currently support\n"
						"previously established connections."];
	}
	
	return self;
}

/* free */
- _free:(id)sender { return [self free]; }
- free
{
	_shutDown = YES;
	if (pppLogExeId) [pppLogExeId killCommand];
	if (connectExeId || pingExeId) {
		if (connectExeId && (connectExeId != self)) {
			// terminate ppp connections (actually, should already be terminated);
		}
		if (pingExeId) [pingExeId killCommand];
		[self perform:@selector(_free:) with:self afterDelay:500 cancelPrevious:YES];
		return (id)nil;
	}
	[exeWindow free];
	globalMonitor = (id)nil;
	return [super free];
}

// -------------------------------------------------------------------------------------
// shutting down

/* can shut down */
- (BOOL)canShutDown
{
	return (connectExeId || pingExeId)? NO : YES;
}

/* can shut down */
+ (BOOL)canShutDown
{
	return (!globalMonitor || [globalMonitor canShutDown])? YES : NO;
}

/* return shut down state */
- (BOOL)isShuttingDown
{
	return _shutDown;
}

/* return connection flag state */
+ (BOOL)shutDown
{
	if (globalMonitor && ![globalMonitor isShuttingDown]) [globalMonitor free];
	return globalMonitor? NO : YES;
}

// -------------------------------------------------------------------------------------
// connection state checks

/* check for true connection */
- (BOOL)isReallyConnected
{
#ifdef CHKPPPSOCK
	/* open socket to check ppp0 */
	// (Thanks to Erik Doernenburg for his contributions to this section)
    static int sock = -1; // maintain socket connection
    if (sock == -1) sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (sock != -1) {
    	struct ifreq ifr;
    	strcpy(ifr.ifr_name, "ppp0");
    	if (ioctl(sock,SIOCGIFFLAGS,&ifr) != -1) {
			return (ifr.ifr_flags & IFF_UP)? YES : NO;
		}
	}
	// fprintf(stdout,"DEBUG - PPPMonitor: ppp0 socket check failed ...\n");
#endif
	return SYSTEM(SYS_PPP_ISUP)? NO : YES; // fallback if all else fails
}

/* check for true connection */
- (BOOL)isPPPDRunning
{
	return SYSTEM(SYS_PPPD_RUNNING)? NO : YES;
}

/* check for ppp loaded */
- (BOOL)isPPPDriverLoaded
{
	return SYSTEM(SYS_PPP_ISLOADED)? NO : YES;
}

/* enable/disable button options */
- (void)enableConnectButton:(id)sender
{
	if (sender) { // delay
		[btnConnect setEnabled:NO];
		[self _perform:@selector(enableConnectButton:):nil delay:1000];
		return;
	} else
	if (!_alreadyConnected) { // set 'Connect' button enabled state
		if (!connectExeId && !_isConnected) {
			[btnConnect setTitle:"Connect"];
		} else
		if ( connectExeId && !_isConnected) {
			[btnConnect setTitle:"(Stop)"];
		} else {
			[btnConnect setTitle:"Disconnect"];
		}
		[btnConnect setEnabled:YES];
	} else {
		[btnConnect setEnabled:NO];
	}
}

// -------------------------------------------------------------------------------------
// connect time keeper

/* create connect time image */
- (void)setConnectTimeIconImage:(int)hh :(int)mm :(int)ss
{
	if (_isConnected) {
		static NXImage *appImage = (id)nil;
		static NXImage *conImage = (id)nil;
		if (!appImage) appImage = [[NXImage alloc] initFromSection:"appConnect"];
		if (!conImage) conImage = [NXImage findImageNamed:"appConnect"];
		if ([appImage lockFocus]) {
			char tm[16];
			float w, h;
			NXPoint orgPt = { 0.0, 0.0 }, txtPt = { 0.0, 6.0 };
			[conImage composite:NX_SOVER toPoint:&orgPt];
			if ((_showTileTime == 1) || (ss < 0)) sprintf(tm,"%d:%02d", hh, mm);
			else sprintf(tm,"%d:%02d:%02d", hh, mm, ss);
			PSsetgray(NX_BLACK); 
			PSselectfont("Ohlfs",9.0);
			PSstringwidth(tm,&w,&h);
			txtPt.x = ((48.0 - w) / 2.0) + 1.0;
			PSmoveto(txtPt.x,txtPt.y); 
			PSshow(tm);
			[appImage unlockFocus];
			[NSApp setApplicationIconImage:appImage];
		}
	} else {
		[NSApp setApplicationIconImage:[NXImage findImageNamed:"appIcon"]];
	}
}

/* TIMER: update connection timer */
- (id)_updClockTime:(id)sender /* 'nil' sender resets timer */
{
	if (clockTimer && _isConnected) {
		long interval = _intervalTime();
		int hh = 0, mm = 0, ss = 0, tt;
		static int lastMM = -1, lastMode = -1;
		if (!sender || !_connectTime) { _connectTime = interval; }
		tt = interval - _connectTime;
		hh = tt / 3600, mm = (tt % 3600) / 60, ss = tt % 60;
		[self message:0:"Connected  (%2d:%02d:%02d)", hh, mm, ss];
		if (_showTileTime == 2) {
			[self setConnectTimeIconImage:hh:mm:ss];
		} else
		if ((_showTileTime == 1) && ((lastMM != mm) || (lastMode != 1))) {
			[self setConnectTimeIconImage:hh:mm:-1];
			lastMM = mm;
		} else
		if ((_showTileTime == 0) && (lastMode != 0)) {
			[NSApp setApplicationIconImage:[NXImage findImageNamed:"appIcon"]];
		}
		if (lastMode != _showTileTime) lastMode = _showTileTime;
	}
	return clockTimer? self : nil;
}

/* start/stop timer */
- (void)_enableConnectClock:(BOOL)flag
{
	if (clockTimer) {
		[self _stopTimer:(_timerHandlerData_t*)clockTimer];
		clockTimer = (void*)nil;
		[NSApp setApplicationIconImage:[NXImage findImageNamed:"appIcon"]];
	}
	if (flag) {
		_timerHandlerData_t *timer;
		float freq = 0.99; // just shy of a second
		[self _updClockTime:nil]; // reset timer
		timer = [self _startTimer:@selector(_updClockTime:) arg:self freq:freq];
		clockTimer = (void*)timer;
	}
}

// -------------------------------------------------------------------------------------
// periodic command execution

/* TIMER: periodic ping */
- (id)_periodicPing:(id)sender
{
	if (!ppingTimer || !_isConnected || !CMD_PING || _testMode_) {
		ppingTimer = (void*)nil;
	} else
	if (!pingExeId) {
		[pingMessage setStringValue:"Ping"];
		pingExeId = [ExecRunCommand runCommand:CMD_PING output:self];
		if (!pingExeId) [pingMessage setStringValue:"****"];
	}
	return ppingTimer? self : nil;
}

/* start/stop ping */
- (void)_enablePeriodicPing:(BOOL)flag
{
	if (ppingTimer) { // cancel if active
		[self _stopTimer:(_timerHandlerData_t*)ppingTimer];
		ppingTimer = (void*)nil;
		if (pingExeId) [pingExeId killCommand];
	}
	if (flag) {
		_timerHandlerData_t *timer;
	    int sec = CMD_PINGINTERVAL? atoi(CMD_PINGINTERVAL) : 3 * 60;
		if (sec < 60) sec = 60;
		timer = [self _startTimer:@selector(_periodicPing:) arg:self freq:(float)sec];
		ppingTimer = timer;
	}
}

// -------------------------------------------------------------------------------------
// connection monitor

/* TIMER: connection established */
- (id)_checkConnect:(id)sender
{
	if (!connectExeId || _isConnected) { // if no connection in progress
		checkTimer = (void*)nil;
	} else
	if (_testMode_ || [self isReallyConnected]) { // if connected
		PLAY(SND_CONNECT);
		_isConnected = YES;
		[self enableConnectButton:self]; // delayed
		[self _enablePeriodicPing:YES];
		[self _enableConnectClock:YES];
		checkTimer = (void*)nil;
	}
	return checkTimer? self : nil;
}

/* start connection check */
- (void)_enableCheckConnect:(BOOL)flag
{
	if (flag) {
		_timerHandlerData_t *timer;
		timer = [self _startTimer:@selector(_checkConnect:) arg:self freq:5.0];
		checkTimer = timer;
	} else
	if (checkTimer) {
		[self _stopTimer:(_timerHandlerData_t*)checkTimer];
		checkTimer = (void*)nil;
	}
}

// -------------------------------------------------------------------------------------
// pppd monitor

/* monitor '/pppd' command */
- (id)_monitorPPPD:(id)sender
{
	if (![self isPPPDRunning]) {
		pppdTimer = (void*)nil;
		[self commandDidComplete:connectExeId withError:0];
	}
	return pppdTimer? self : nil;
}

/* start monitor */
- (void)_enablePPPDMonitor:(BOOL)flag
{
	if (pppdTimer) {
		[self _stopTimer:(_timerHandlerData_t*)pppdTimer];
		pppdTimer = (void*)nil;
	}
	if (flag && (connectExeId == self)) {
		_timerHandlerData_t *timer;
		float freq = 15.0;
		timer = [self _startTimer:@selector(_monitorPPPD:) arg:self freq:freq];
		pppdTimer = (void*)timer;
	}
}

// -------------------------------------------------------------------------------------
// PPP log file monitor

/* print message to ppplog scroll text */
- (void)logMessage:(BOOL)isError:(const char*)fmt, ...
{
	va_list args;
	
	/* text color (on a white background) */
	if ([[pppLogScroll docView] shouldDrawColor]) {
		if (isError) { [pppLogScroll setTextAttributeColor:NX_COLORRED]; }
		else         { [pppLogScroll setTextAttributeGray:NX_DKGRAY]; }
	} else {
		if (isError) { [pppLogScroll setTextAttributeGray:NX_DKGRAY]; }
		else         { [pppLogScroll setTextAttributeGray:NX_DKGRAY]; } // same gray
	}
	
	/* print message */
	va_start(args, fmt);
	[pppLogScroll textPrintf:fmt args:args];
	va_end(args);
	
	/* reset color to BLACK */
	[pppLogScroll setTextAttributeGray:NX_BLACK];
}

/* tail log file */
- (void)tailLogFile:(id)sender
{

	/* initialize pppLogScroll */
	if (![pppLogScroll isMemberOf:[ExecScrollText class]]) {
		pppLogScroll = [ExecScrollText newExecScrollText:pppLogScroll];
		[pppLogScroll setDelegate:self];
		[pppLogScroll setTab:[textFont getWidthOf:"        "] count:10];
		[pppLogScroll textPrintf:"\n"];
		[pppLogScroll clearScrollText];
		[pppLogScroll setTextAttributeGray:NX_BLACK];
	}
	
	/* start tail command */
	if (!pppLogExeId) {
		[pppLogScroll clearScrollText];
		[pppLogScroll setTextAttributeGray:NX_BLACK];
		if (CMD_TAILLOG) {
			char *ld1 = "/usr/adm/";
			char *ld2 = "/private/adm/";
			char tailBuff[64], *tail = (char*)CMD_TAILLOG;
			if ((strlen(tail) < 32) && !strchr(tail,' ') &&
				(!strncmp(tail,ld1,strlen(ld1)) || !strncmp(tail,ld2,strlen(ld2)))) {
				sprintf(tailBuff,"/usr/ucb/tail -1f %s;", tail);
				tail = tailBuff;
			}
			[self logMessage:0:"(%s)\n",tail];
			pppLogExeId = [pppLogScroll runCommand:tail user:0];
			if (!pppLogExeId) [self logMessage:1:"'tail' command failed to start.\n"];
		} else {
			[self logMessage:1:"'tail' command disabled (not specified).\n"];
		}
	}

}

/* clear text */
- (void)clearLog:(id)sender
{
	[pppLogScroll clearScrollText];
	[pppLogScroll setTextAttributeGray:NX_BLACK];
}

// -------------------------------------------------------------------------------------
// user commands

/* connect */
- (void)connect:(id)sender
{

	/* disconnect */
	if (connectExeId) {
		if (!CMD_DISCONNECT) {
			[self message:1:"'Disconnect' command has not been specified"];
			return;
		}
		[self message:0:"Disconnecting ... "];
		[autoConnect setState:0]; // clear auto-reconnect
		[btnConnect setEnabled:NO];
		[self _enableCheckConnect:NO]; // cancel connection check
		[self _enablePeriodicPing:NO]; // cancel ping
		[self _enableConnectClock:NO]; // cancel connection timer
		_isDisconnecting = YES;
		if (!_testMode_) {
			if (SYSTEM(CMD_DISCONNECT)) {
				[self message:1:"'Disconnect' command failed"];
				_isDisconnecting = NO;
				[self enableConnectButton:self]; // delayed
			}
		} else {
			[self commandDidComplete:self withError:0];
		}
		return;
	}
	_isDisconnecting = NO;
	
	/* ignore connect-request during shutdown */
	if (_shutDown) return;
	
	/* load PPP driver is necessary */
	if (!_testMode_ && ![self isPPPDriverLoaded]) {
		if (CMD_LOADPPPLKS) {
			[self message:0:"Loading PPP driver ... "];
			{ [[appMatrix window] flushWindow]; NXPing(); }
			if (SYSTEM(CMD_LOADPPPLKS)) {
			    [self message:1:"Error encountered while loading PPP driver"];
			} else {
			    [self message:0:"Loading PPP driver ... successful"];
			}
			if (![self isPPPDriverLoaded]) return;
		} else {
			[self message:1:"PPP driver must be loaded before connecting"];
			return;
		}
	}
	
	/* check for 'Connect' command availability */
	if (!CMD_CONNECT) {
		[self message:1:"'Connect' command has not been specified"];
		return;
	}
	
	/* check for already connected */
	if (![self isReallyConnected]) {
		[self message:0:"Connecting ..."];
		_connectStart = _intervalTime();
		if (!_testMode_) {
			connectExeId = [ExecRunCommand runCommand:CMD_CONNECT output:self];
			if (!connectExeId) {
				[self message:1:"Connection failed to start"];
			} else {
				[self _enableCheckConnect:YES];
			}
		} else {
			connectExeId = self;
			[self _enableCheckConnect:YES];
		}
	} else {
		_alreadyConnected = YES;
		[self message:1:"Already connected"];
	}
	[self enableConnectButton:self]; // delayed
	
}

/* run application */
- (void)runApplication:(id)sender
{
	id btnCell = [appMatrix selectedCell];
	int appNdx = [btnCell tag];
	if ((appNdx >= 0) && (appNdx < netAppCount) && netApps[appNdx]) {
		const char *app = netApps[appNdx];
		int r = -1, c = -1;
		NXRect cellFrame;
		[appMatrix getRow:&r andCol:&c ofCell:btnCell];
		[appMatrix getCellFrame:&cellFrame at:r:c];
		[appMatrix lockFocus];
		_hiliteRect(&cellFrame);
		{ [[appMatrix window] flushWindow]; NXPing(); }
		if (_cmdIsApp(app)) {
			char buff[1024], *cmdFile = (char*)_getCmdFile(buff,app);
			NXPortFromName(cmdFile,(char*)nil);
		} else {
			const char *cmdLine = _cmdLine(app);
			char *ext = ";\n", *cmd = (char*)malloc(strlen(cmdLine)+strlen(ext)+1);
			sprintf(cmd,"%s%s",cmdLine,ext);
			[ExecMonitor terminalCommand:cmd title:"Command"];
			free(cmd);
		}
		[btnCell setDrawDots:NO];
		[btnCell drawSelf:&cellFrame inView:appMatrix]; 
		{ [[appMatrix window] flushWindow]; NXPing(); }
		[appMatrix unlockFocus];
	} else NXBeep();
}

// -------------------------------------------------------------------------------------
// command execution/completion
// -------------------------------------------------------------------------------------

/* command output (when we are the delegate) */
- (void)commandOutput:(id)execId buffer:(const char*)buffer len:(int)len
{
	/* ignore anything we get */
}

/* call-back from shell command (main thread) */
- (void)commandDidComplete:execId withError:(int)err
{

	/* check for connection termination */
	if (execId == connectExeId) {
		_isConnected = NO;
		connectExeId = nil;
		[NSApp setApplicationIconImage:[NXImage findImageNamed:"appIcon"]];
		if (_isDisconnecting) { // manual disconnect
			[self message:0:"Connection terminated"];
		} else
		if ((_intervalTime() - _connectStart) <= 7) { // failed in <= X seconds
	    	if ([self isPPPDRunning]) { // pppd still running
				_alreadyConnected = YES;
				[exeWindow makeKeyAndOrderFront:(id)nil];
				[self message:1:"'pppd' still running "
					"('-detach' apparently not specified)"];
#if 1
				[autoConnect setState:0];
#else
				// Assuming that '-detach' was not specified on the 'pppd' command:
				// At this point the 'pppd' command will be monitored to attempt to detect 
				// detect when it is destroyed. We attempt to simulate the same interface 
				// used in 'ExecRunServer' to monitor it's own child processes.
				connectExeId = self;
				[self _enablePPPDMonitor:YES];
				_isConnected = [self isReallyConnected];
				return;
#endif
			} else {
				[self message:1:"Connection did not start properly"];
			}
		} else
		if ([autoConnect state]) { // re-connect
			PLAY(SND_DISCONNECT);
			[self message:1:"Connection terminated (auto-reconnect)"];
			[self _perform:@selector(connect:):nil delay:1000];
		} else {
			PLAY(SND_DISCONNECT);
			[self message:1:"Connection terminated"];
		}
		_isDisconnecting = NO;
		[exeWindow makeKeyAndOrderFront:(id)nil];
		[self enableConnectButton:self];
		return;
	}
		
	/* check for ping */
	if (execId == pingExeId) {
		pingExeId = nil;
		[pingMessage setStringValue:""];
		return;
	}

	/* ppp log scroller */
	if (execId == pppLogExeId) {
		[self logMessage:err:"'tail' command terminated.\n"];
		pppLogExeId = nil;
		return;
	}

	/* error */
	fprintf(stderr,"Unknown execId in call to 'commandDidComplete:withError:'");
	return;
}

// -------------------------------------------------------------------------------------
// output monitor
// -------------------------------------------------------------------------------------

/* same as "strstr()", except with limit check */
const char *strnstr(const char *s1, const char *s2, int n)
{
	int i, n2 = strlen(s2), e = n - n2;
	for (i = 0; i < e; i++) {
		while ((i < e) && (s1[i] != s2[0])) i++;
		if ((i < e) && !strncmp(&s1[i],s2,n2)) return &s1[i];
	}
	return (char*)0;
}

/* monitor scroll text output */
- (void)monitorOutput:scrollId buffer:(const char*)buffer len:(int)len
{
	if (scrollId != pppLogScroll) return;
	// not fully implemented
}

// -------------------------------------------------------------------------------------
// Terminal invoke
// -------------------------------------------------------------------------------------

/* invoke command in Terminal window */
+ terminalCommand:(const char*)cmd title:(const char*)title
{
	Speaker	*speaker;
    port_t	terminalPort;
    
	/* can't find Terminal */
	if (!(terminalPort = NXPortFromName("Terminal", NULL))) return (id)nil;
	
	/* launch Terminal */
	[NSApp deactivateSelf];
	creat("/tmp/.reallyignorethis.term", 0444);
	[[Application workspace] openFile:"/tmp/.reallyignorethis.term"
		fromImage:(id)nil at:(NXPoint*)nil inView:(id)nil];
	
	/* run command */
	speaker = [NSApp appSpeaker];
	[speaker setSendPort: terminalPort];
	[speaker selectorRPC:"runCommand:usingShell:inFolder:windowTitle:closeOnExit:"
		paramTypes: "cccci", cmd, "", "", title, NO];

	return self;
	
}

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

@end

// -------------------------------------------------------------------------------------
// TileButtonCell
// -------------------------------------------------------------------------------------
@implementation TileButtonCell
- drawSelf:(const NXRect *)cellFrame inView:controlView
{
	NXSize imageSize;
	NXPoint imageOrigin, iconOrigin = cellFrame->origin;
	
	/* draw tile */
	iconOrigin.y += cellFrame->size.height; // assume frameHeight == tileHeight
	if (shelfTile) { [shelfTile composite:NX_COPY toPoint:&iconOrigin]; } 
	else { NXDrawButton(cellFrame, cellFrame); }
	
	/* draw image */
	[[self image] getSize:&imageSize];
	imageOrigin.x = iconOrigin.x + (cellFrame->size.width  - imageSize.width )/2.0;
	imageOrigin.y = iconOrigin.y - (cellFrame->size.height - imageSize.height)/2.0;
	[[self image] composite:NX_SOVER toPoint:&imageOrigin];
	
	/* draw text */
	if ([self iconPosition] == NX_ICONABOVE) {
		NXSize titleSize;
		NXRect titleRect = *cellFrame;
		static Cell *titleCell = nil;
		if (!titleCell) {
			titleCell = [[Cell alloc] initTextCell:""];
			[titleCell setAlignment:NX_CENTERED];
			[titleCell setFont:[controlView font]];
		}
		[titleCell setStringValue:[self title]];
		[titleCell calcCellSize:&titleSize inRect:cellFrame];
		titleRect.origin.y += titleRect.size.height - titleSize.height - 7;
		titleRect.origin.x += floor((titleRect.size.width-titleSize.width)/2.0);
		titleRect.size.width  = titleSize.width;
		titleRect.size.height = titleSize.height;
		[titleCell drawSelf:&titleRect inView:controlView];
	}
	
	/* draw dots */
	if (shelfDots && !_noDots) [shelfDots composite:NX_SOVER toPoint:&iconOrigin];
	
	return self;
}
- highlight:(const NXRect*)cellFrame inView:aView lit:(BOOL)flag { return self; }
- (void)setDrawDots:(BOOL)flag { _noDots = flag? NO : YES; }
@end
