#!/usr/bin/python
# -*- coding: utf-8 -*-

import 	os
import 	sys
import 	getopt
import 	string
import 	re
from   	stat 		import *
import 	thread
import 	time
from   	threading	import *
#import 	popen2
from    subprocess      import *
import 	SocketServer
from	dgmsocket	import *
import  time

# Some global defaults
Gstr_sourceScript	= '-x'
Gstr_sourceCmd		= '-x'
Gstr_optionsFile	= 'options.txt'
Gstr_host		= 'localhost'
Gstr_hostType           = ''
Gstr_thisHost           = 'localhost'
Glstr_uname             = os.uname()
if len(Glstr_uname):
    Gstr_thisHost       = Glstr_uname[1];
    Gstr_hostType       = "%s-%s" % (Glstr_uname[0], Glstr_uname[4])
Gstr_port           	= '1701'
Gstr_portResult		= '1835'
Gstr_portSys		= '1834'

G_socket		= C_dgmsocket(Gstr_host, string.atoi(Gstr_port))

Gb_term                 = 0
Gb_quiet                = 0
Gb_threadTERM           = 0
Gb_threadREADY          = 0
Gb_timerReset		= 0
Gb_syncSend             = 1
Gtimer_READY		= 0
Gb_threadTimerStarted 	= 0
Gb_allTERM              = 0
Gb_nonInteractive	= 0
Gstr_exec               = 'mris_pmake'

G_SYNOPSIS = """

 NAME

        dsh

 SYNOPSIS

        dsh		[-s <sourceScript>]     \\
                        [-e <execName>]         \\
			[-c <cmdString>]        \\
			[-o <optionsFile>]      \\			
			[-h <host>]             \\
			[-p <portRemote>]       \\
			[-R <portResult>]       \\
			[-S <portSys>]          \\
			[-v <level>]            \\
			[-q]                    \\
			[-t]			\\
			[-V]


 DESCRIPTION
 
 	'dsh' is a simple "shell" that intermediates between 'mris_pmake'
	and a user. It can operate either interactively, or in batch mode.
	
	In interactive operation, it presents the user with a simple prompt, 
	waits for input, and transmits this input to 'mris_pmake'. Any data
	transmitted from 'mris_pmake' to the result log server socket will
	be echoed back to the user. Also, if this instance of the shell 
	actually started the 'mris_pmake' engine, any output that the
	engine dumps to stdout will also be echoed back to the user.
	
	This shell is multi-threaded. One thread spawns the 'mris_pmake'
	and captures any output it generates. Each time user input is 
	captured, another thread is started that transmits this input
	to 'mris_pmake'. Additional threads are created for the result and
	system log server sockets.
	
 OPTIONS
 
	-s <sourceScript>
	A simple text file of control commands, one per line, that are
	transmitted to 'mris_pmake'.
        
        -e <execName>
        The name of the executable to run. By default 'mris_pmake' on the
        current path is used. Specify a different executable with this
        flag. Useful only when debugging against different versions of
        'mris_pmake' although in theory any executable could be used.
	
	-c <cmdString>
	Instead of reading a file for control commands, the commands can
	be specified directly with 'c'. Group all commands together within
	double quotes, and separate individual commands with ';'
 
	-o <optionsFile>
	The options file that 'mris_pmake' should parse.
	
	Defaults to "./options.txt".
			
	-h <host>
	The host on which the 'mris_pmake' process is running. This can
	be a remote host, or a local one.
	
	Defaults to "localhost".
	
	-p <portRemote>
	The port on which the remote 'mris_pmake' listens. This script will
	transmit UDP data to this port.
	
	Defaults to "1701".	
	
	-R <portResult>
	The port on which 'dsh' itself will listen for result transmissions
	from 'mris_pmake'.
	
	Defaults to "1835".
	
	-S <portSys>
	The port on which to listen for UDP system log transmissions from 
	'mris_pmake'.
	
	Defaults to "1834".
	
	-v <level>
        Verbose. Useful for debugging. Higher levels show more info - currently
        only 0, 1, and 2 are valid levels. Note that level 0 effectively turns
        OFF verbosity.

	-q
	Quiet. If specified, redirect stdout > /dev/null.
	
	-t
	If specified, send a TERM string to 'mris_pmake' when finishing.

	-V
	Print the version string.	
		
 PRECONDITIONS

	o This "shell" will start a 'mris_pmake' process if one is not already
	  running, or connect to an existing one (assuming standard port).	
		  	  
 POSTCONDITIONS
 
 	o On termination with "quit", the spawned 'mris_pmake' is killed.
	
	o On termination with "exit", the spawned 'mris_pmake' is left
	  running.
		      	 
 HISTORY
 28 February 2005
 o Initial design and coding.
 
 24 March 2005
 o Back-ported enhancements from dijk_dscript.py, specifically
   the ability to connect to already running engine.
 
 07 April 2005
 o Added resultlog port server.
 
 11 April 2005
 o Finally sorted out issues related to resultlog port.
 o Designed replacement for fifo processing via UDP socket.
 
 13 April 2005
 o Synchronised with 'dijk_dscript'. This shell now incorporates all of
   dijk_dscript's functionality - consequently, 'dijk_dscript' is 
   DEPRECIATED.

 09 June 2005
 o Added "source" command - evaluate a script from an interactive session.
 
 04 October 2005
 o Added transmit synchronisation pulses - if 'dsh' somehow misses a 
   "Ready" from the engine, and a certain timeout has elapsed, it will
   send a "SYNC" string to trigger the engine to return another "Ready".
   
 09 December 2005
 o Changes for Mac OS X.
 
 10 December 2005
 o HOSTNAME / HOSTTYPE environment variables *must* be set correctly
   and be accessible to the python interpreter.
   
 20 December 2005
 o Added handler code for unexpected death of 'mris_pmake' while processing
   dsh script.
   
 19 April 2006
 o Finally removed dependency on 'SSocket_client'; now uses internal socket
   class <dgmsocket>.  
   
 28 October 2009
 o Resurrection. Replaced popen(...) with subprocess.

"""

Gstr_SELF	= sys.argv[0]
Gstr_VERSION	= "$Id: dsh,v 1.3 2009/11/30 20:09:29 rudolph Exp $"
G_dieNow	= 0

class dijkResultLogHandler(SocketServer.BaseRequestHandler) :
	def handle(self):
		print self.request[0]
			
class dijkSysLogHandler(SocketServer.BaseRequestHandler) :
	def handle(self):
		global Gb_threadREADY
		Gb_threadREADY = 0
		str_target = "Ready"
		str_incoming = self.request[0]
		if re.search(str_target, str_incoming): 
			Gb_threadREADY = 1
def vprint(str_msg):
	global verbose
	if(verbose): print str_msg

def synopsis_show(void=0):
	print G_SYNOPSIS
	
def shutdown(exitCode):
	global verbose
	if(verbose):
		print Gstr_SELF
		print "\n\tNormal termination to system with code %d.\n" % exitCode
	sys.exit(exitCode)

def error_exit(str_action, str_msg, code):
	print Gstr_SELF
	print "\n"
	print "\tSorry, but some error occurred."
	print "\tWhile " + str_action + ','
	print "\t" + str_msg
	print "\n"
	print "Exiting to system with code %d.\n" % code
	os._exit(code)
	sys.exit(code)
	
def transmit(str_msg):
    	#
	# ARGS
	#	str_msg		in		String message to transmit
	#
	# DESC
	#	Sends <str_msg> to <Gstr_host> on <Gstr_port>.
	#
	# DEPENDS
	#	Global variables:
	#		o Gstr_host
	#		o Gstr_port
	#
	#	'SSocket_client'
	#
	
	global Gstr_host, Gstr_port, G_socket
	global verbose
		
	if(verbose): 
		print "\n(%d): Transmitting <%s>" % (thread.get_ident(), str_msg)
	str_transmit =  'SSocket_client --silent --host ' + Gstr_host
	str_transmit += ' --port ' + Gstr_port
	str_transmit += ' --msg "' + str_msg + '"'
	str_transmit += ' >/dev/null &'
	
	os.system(str_transmit)
	#G_socket.tx(str_msg)

def dijkstra_spawn(str_port):
	#
	# ARGS
	#	port		in		the port on which the mris_pmake
	#					process will listen
	#
	# DESC
	#	Spawn the external mris_pmake process, and listen on a pipe
	#	for any communication.
	#
	
	global verbose
	
	str_start	= '%s --listen' % Gstr_exec
        str_cmd         = '%s'          % Gstr_exec
        str_arg         = '--listen'
	if(verbose): print "Starting " + str_start
	#(dijkstra_stdout, dijkstra_stdin, dijkstra_stderr)	= popen2.popen3(str_start)
        p       = Popen([str_cmd, str_arg], bufsize=-1,                 \
                    stdin=PIPE, stdout=PIPE, close_fds=True)
        (dijkstra_stdout, dijkstra_stdin, dijkstra_stderr)      =       \
                    (p.stdout, p.stdin, p.stderr)
	while(1):
		line		= dijkstra_stdout.readline()
		#err_line	= dijkstra_stderr.readline()
		#if err_line:
			#print err_line,
		#else:
			#break
		if line:
			print line,
		else:
			break
	print "process died."
 	#print "stdout:\t" + dijkstra_stdout.read() + "\n"
 	#print "stderr:\t" + dijkstra_stderr.read() + "\n"
	error_exit(	"attempting dijkstra_spawn()", 
			"the process died unexpectedly",
			10)
	return 1

def thread_dijkstra(str_port):
	#
	# ARGS
	#	port		in		the port on which the mris_pmake
	#					process will listen
	#
	# DESC
	#	Thread wrapper for 'dijsktra_spawn'
	#
	
	global verbose
	
	if(verbose): print "thread dijkstra start"
	thread.start_new_thread(dijkstra_spawn, (str_port,))
	if(verbose): print "thread dijkstra return"
	
		
def thread_transmit(str_msg):
    	#
	# ARGS
	#	str_msg		in		String message to transmit
	#
	# DESC
	#	Thread wrapper for 'transmit();
	#
	
	global verbose, Gb_timerReset
	
	if(verbose): print "thread transmit start"
	thread.start_new_thread(transmit, (str_msg,))
	if(verbose): print "thread transmit return"
	Gb_timerReset = 1

def sysLog_listenFor(port, str_ready):
	#
	# ARGS
	#	port		in		port on which to listen
	#	str_ready	in		string transmitted from back
	#						end process that signals
	#						it is ready to process
	#						comms.
	#
	# DESC
	#	Creates a threaded UDP socket server, listening on port 
	#	<port>.
	#
	# PRECONDITIONS
	#	o Valid requestHandler method, defined in sub-class.
	#
	# POSTCONDITIONS
	#	o The server socket is itself threaded.
	#
	
	sysUDPs_result	= SocketServer.UDPServer(('', port), dijkSysLogHandler)
	sysUDPs_result.serve_forever()
	
def thread_sysLog_listenFor(str_port, str_ready):
    	#
	# ARGS
	#	str_port	in		port on which to create server
	#	str_ready	in		string transmitted from back
	#						end process that signal
	#						it is ready to process
	#						comms.
	#
	# DESC
	#	Thread wrapper for 'sysLog_listenFor();
	#
	
	global verbose
	
	if(verbose): print "thread sysLog_listenFor start"
	thread.start_new_thread(sysLog_listenFor, (string.atoi(str_port), str_ready))
	if(verbose): print "thread sysLog_listenFor return"
	
		
def resultLog_listenFor(port):
	#
	# ARGS
	#	port		in		port on which to listen
	#
	# DESC
	#	Creates a threaded UDP socket server, listening on port 
	#	<port>.
	#
	# PRECONDITIONS
	#	o Valid requestHandler method, defined in sub-class.
	#
	# POSTCONDITIONS
	#	o The server socket is itself threaded.
	#
	
	resultUDPs_result	= SocketServer.UDPServer(('', port), dijkResultLogHandler)
	resultUDPs_result.serve_forever()
	
def thread_resultLog_listenFor(str_port):
    	#
	# ARGS
	#	str_port	in		port on which to create server
	#
	# DESC
	#	Thread wrapper for 'resultLog_listenFor();
	#
	
	global verbose
	
	if(verbose): print "thread resultLog_listenFor start"
	thread.start_new_thread(resultLog_listenFor, (string.atoi(str_port),))
	if(verbose): print "thread resultLog_listenFor return"

def line_send(str_line, b_synchronise = 1):
	#
	# ARGS
	#	str_line	in		string line to transmit
	#	b_synchronise	in		boolean flag. If true,
	#						then wait on theads,
	#						else simply transmit
	#						and return.
	#
	# DESC
	#	Main entry point to handshaking / transmitting a string
	#	to the 'mris_pmake' process.
	#
	
	str_lineCleanedr = string.rstrip(str_line)
	str_lineCleaned  = string.lstrip(str_lineCleanedr)
	
	thread_transmit(str_lineCleaned)
	if(b_synchronise): thread_waitREADY()	
	
def thread_waitTERM():
	#
	# DESC
	#	Simple funciton that just waits until a global variable
	#	Gb_threadTERM is TRUE.
	#
	
	global	Gb_threadTERM, verbose
	
	if(verbose): print "Waiting for thread termination..."
	while(not Gb_threadTERM):
		pass
	Gb_threadTERM	= 0
	
def SYNCsend():
	global Gb_timerReset, Gb_threadTimerStarted
	global Gtimer_READY
        if Gb_syncSend:
            print "%s %s pulsing engine with 'SYNC'... " % \
                (time.asctime(time.localtime()), Gstr_thisHost)
            thread_transmit("SYNC")
            Gtimer_READY.cancel()
            Gb_timerReset 		= 1
            Gb_threadTimerStarted 	= 0
            dijkstraRunning		= dijkstra_isRunning()
            if(dijkstraRunning == 256):
                    print "(Engine failure detected - code %s)" % dijkstraRunning
                    error_exit("sending 'SYNC'",
                            "it appears as though the engine has died.",
                            10)
            #else:
                    #print "(Engine still running - code %s)" % dijkstraRunning
		
def thread_waitREADY():
	#
	# DESC
	#	Simple funciton that just waits until a global variable
	#	Gb_threadREADY is TRUE. This is set by the syslog socket
	#	handler.
	#
	
	global	Gb_threadREADY, verbose, Gb_timerReset, Gb_threadTimerStarted
	global	Gtimer_READY
	timeout	= 20.0 # seconds
	
	Gb_timerReset 		= 1
	Gb_threadTimerStarted	= 0
	
	if(verbose): print "Waiting for Gb_threadREADY to become TRUE..."
	while(not Gb_threadREADY):
		if(Gb_timerReset):
			if(Gb_threadTimerStarted):
				vprint ("Timer already running...")
			else:
				Gtimer_READY = Timer(timeout, SYNCsend)
				vprint ("Starting timer...")
				Gtimer_READY.start()
				Gb_threadTimerStarted = 1
			Gb_timerReset = 0
		pass
	Gb_threadREADY		= 0
	Gb_threadTimerStarted	= 0
	vprint ("Stopping timer")
	Gtimer_READY.cancel()

def userInput_count():
    count = 0
    while True:
        count += 1
        yield count

staticUserInputCount    = userInput_count().next

def userInput_get():
	#
	# DESC
	#	Presents the user with a prompt char '>' and waits
	#	for input. This input is returned to the caller.

        count = staticUserInputCount()
        if count == 1:
            print "Engine successfully started and environment parsed."
            print "Ready for user input. Type 'HELP' for help."
	str_userInput = raw_input("%d > " % count)
	return str_userInput	
	
def dijkstra_isRunning():
	#
	# DESC 
	#	Checks the system process list for any instances of 'dijkstra'
	#	or '--listen'.
	#
	
	#if(Gstr_hostType == 'powerpc'):
            #alreadyRunningListen	= os.system("ps -A | grep -e --listen | grep -v grep >/dev/null")
            #alreadyRunning		= os.system("ps -A|grep mris_pmake|grep -v python|grep -v grep >/dev/null")
	#else:
        alreadyRunningListen	= os.system("ps -Af | grep -e --listen | grep -v grep >/dev/null")
        alreadyRunning		= os.system("ps -Af|grep mris_pmake|grep -v python|grep -v grep>/dev/null")
	dijkstraRunning		= alreadyRunningListen & alreadyRunning
	return dijkstraRunning
	
# main 

try:
	opts, remargs	= getopt.getopt(sys.argv[1:], 'xqtc:o:s:e:h:p:S:R:v:V')
except getopt.GetoptError:
	synopsis_show()
	sys.exit(1) 

verbose		= 0
for o, a in opts:
	if(o == '-c'):
		Gstr_sourceCmd		= a
		Gb_nonInteractive	= 1
	if(o == '-s'):
		Gstr_sourceScript	= a
		Gb_nonInteractive	= 1
        if(o == '-e'):
                Gstr_exec               = a
	if(o == '-o'):
		Gstr_optionsFile 	= a
	if(o == '-h'):
		Gstr_host		= a
		print "Warning! I cannot start a '%s' process" % Gstr_exec
		print "on any host other than localhost! The <host>"
		print "option is currently only useful in controlling"
		print "a '%s' process that has already been"    % Gstr_exec
		print "started."
	if(o == '-p'):
		Gstr_port		= a
	if(o == '-R'):
		Gstr_portResult		= a
	if(o == '-S'):
		Gstr_portSys		= a
	if(o == '-v'):
		str_verbose		= a
		verbose			= string.atoi(str_verbose)
	if(o == '-V'):
		print Gstr_VERSION
		sys.exit(0)
	if(o == '-x'):
		synopsis_show()
		sys.exit(1)
	if(o == '-t'):
		Gb_term			= 1 and Gb_nonInteractive
	if(o == '-q'):
		Gb_quiet		= 1
		F_devnull		= open('/dev/null', 'w')
		sys.stdout		= F_devnull

#
# Startup messages
#
if(not Gb_nonInteractive):
	print "Welcome to the mris_pmake shell, 'dsh', running on '%s'" % Gstr_thisHost
	print "CVS version " + Gstr_VERSION
	print "\nType 'quit' to return gracefully to system."
	print "Note that this will terminate any running 'mris_pmake' engine.\n"
	print "Type 'exit' to terminate this shell."
	print "Note that any 'mris_pmake' engine active will remain running.\n"
															
# Check if dijsktra_p1 has already been started on 'localhost'
if Gstr_host == 'localhost':
	dijkstraRunning = dijkstra_isRunning()
	if(dijkstraRunning == 256):
            str_msg  = "\n\tIt seems as though 'mris_pmake' is not already running.\n"
            str_msg += "\tI will attempt to start it as 'mris_pmake --listen'.\n"
            str_msg += "\tThere might be a delay of a few seconds as the program\n"
            str_msg += "\tstarts and parses its initial environment.\n\n"
            str_msg += "\tPlease be patient..."
            if(not Gb_nonInteractive): print str_msg

            thread_dijkstra(Gstr_port)

            if(G_dieNow):
                sys.exit(G_dieNow)

            if(not Gb_nonInteractive):
                print "\tBack-end engine has been successfully started!"
                print "\tAny console output from this 'mris_pmake' process"
                print "\twill be captured by this shell.\n"
                print "\tWaiting on threads while environment is parsed...\n"
        else:
            if(not Gb_nonInteractive):
                print "\n\t'mris_pmake' is already running on ",
                print "host '" + Gstr_thisHost + "'."
                print "\n\tI will connect to this process."
                print "\tPlease note that I cannot capture console output"
                print "\tfrom an already running 'mris_pmake' process. Any."
                print "\tdata transmitted to my resultLog server, however, will"
                print "\tbe captured and displayed.\n"
                print "\tIf you want a fully interactive session, please type"
                print "\t'quit' at the prompt and re-run 'dsh'.\n"
else:
	if(not Gb_nonInteractive):
            print "\tYou have specified a host other than <localhost>. I cannot"
            print "\tstart 'mris_pmake' on any host other than <localhost>, and"
            print "\twill assume that this process is running on <"+Gstr_host+">."
            print "\tIf the back end engine is not currently started on that host"
            print "\tplease login and start. Note that this shell will block until"
            print "\tit receives a transmission from the engine. Also note that"
            print "\tthis engine needs to at the very least direct its syslog"
            print "\tto this shell - use 'ENV syslog set <hostname>:<port>.'"
	
# Start the result and system log server threads
thread_resultLog_listenFor(Gstr_portResult)
thread_sysLog_listenFor(Gstr_portSys, "Ready")
					
# hello
str_hello 	= "SYSECHO Hello from " + Gstr_SELF
str_hello  += ", PID: " + Gstr_thisHost + ":" + str(os.getpid())
str_userInput	= str_hello
line_send(str_userInput)

if Gb_nonInteractive:
	if(Gstr_sourceCmd == '-x'):
		if(Gstr_sourceScript == '-x'):
			str_action = "checking command line parameters"
			str_error  = "No 'sourceScript' file was specified."
			error_exit( str_action, str_error, 1)
			# Check if <sourceScript> is a regular file
			mode = os.stat(Gstr_sourceScript)[ST_MODE]
			if(not S_ISREG(mode)): 
				str_error  = "it seems that the file is not a regular text file.\n\t"
				str_error += "Please use an ASCII text file for the <sourceScript>."
				error_exit('checking on ' + Gstr_sourceScript, str_error, 1)	
	# process the sourceScript
	if(Gstr_sourceCmd != '-x'):
		lineCmd	= string.split(Gstr_sourceCmd, ';')
		for line in lineCmd:
			if(line == "TERM"):
				line 	= "EXIT"
				Gb_term = 1
			line_send(line)
	else:
		F_sourceScript	= open(Gstr_sourceScript, 'r')
		for line in F_sourceScript.readlines():
			line_send(line)
		F_sourceScript.close()
				
else:
	while (str_userInput != "exit" and str_userInput != "quit"):
		str_userInput = userInput_get()
		if str_userInput == "help": print G_SYNOPSIS
		if re.search("source", str_userInput):
			words = string.split(str_userInput)
			if len(words) >= 2:
				str_sourceScript = words[1]
				if len(str_sourceScript):
					print "Sourcing " + str_sourceScript + "..."
					F_sourceScript	= open(str_sourceScript, 'r')
					for line in F_sourceScript.readlines():
						line_send(line)
					F_sourceScript.close()
                elif str_userInput == "noSync":
                        print "Disabling re-sync pulsing"
                        Gb_syncSend     = 0
		elif str_userInput == "version":
			print Gstr_VERSION
		else:
			line_send(str_userInput)
		time.sleep(0.2)
	if(str_userInput == "quit"):	
		print "Quitting (and turning off any running engines)..."
		thread_transmit("TERM")
	if(str_userInput == "exit"):
		print "Exiting from shell. Note that the engine is still running."

# goodbye
str_cheers	= "SYSECHO Goodbye from " + Gstr_SELF
str_cheers  +=", PID: " + Gstr_thisHost + ":" + str(os.getpid())
line_send(str_cheers)

if(Gb_term):
	line_send("TERM")	
	
os.system("rm lock 2>/dev/null")
shutdown(0)
	
