import os, sys, stat
from time import sleep, strftime
from tiny import hide_env, say
from signal import SIGKILL

mode_rw = 384			# u+rw
mode_w  = 128			# u+w

#+
# NAME:
#	skyd_claim
# PURPOSE:
#	Reads the specified file 'filepath' and adds its content to the
#	returned status dictionary.
#	If claim=True then the file is 'claimed' by enabling read protection
#	This makes the file unavailable until read protection is switched off
# CALLING SEQUENCE:
#	status = skyd_claim(claim,status,filepath)
# INPUTS:
#	claim		bool	
#	status		dictionary	status['number'] should be 0
#	filepath	string		name of configuration file or orbit catalogue
# OUTPUTS:
#	status		dictionary	on success:
#							status['number'] and status['message'] are not modified
#								(should be 0 and blank string, respectively)
#							status['contents'] is the file contents
#								as a string array
#							status['chmod'] is the rwx-mode of filepath
#								(claim=TRUE only) 
#							on failure
#							status['number' ] is set to 1
#							status['message'] contains error message
#							status['contents'] and status['chmod'] will not be present.
# CALLS:
#	skyd_status
# SEE ALSO:
#	skyd_release
# PROCEDURE:
#	Used to control updates to the daemon configuration file and to the
#	catalogue of orbits on which the daemon operates.
#
#	claim=TRUE:
#		A while loop runs for 5 seconds until is is readable.
#		On succes the file is read, read protection is switched on. This 'claims' the
#		conf file until it is 'released' by skyd_release switching read
#		protection off again. Failure means either that the file was not readable
#		(presumably because a previous call already claimed it, and the read
#		protection is still on) or because there was a read error.
#	claim=FALSE:
#		One attempt is made to read the file.
#		Failure means that a read error occurred.
# MODIFICATION HISTORY:
#		DEC-2005, Paul Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_claim(claim,status,filepath):

	csay = 'skyd_claim'

	hidepath = hide_env(filepath)

	if not os.path.isfile(filepath):
		return skyd_status(status,1,hidepath+' does not exist')

	# If there is no read access (in use by another skyd_orbit run) then
	# wait try for 24x5 seconds (2 minutes) for read access to appear

	count = 0
	count_max = 30
	naptime = 5

	while count < count_max:

		try:
			contents = open(filepath,'r').read()
		except IOError, (errno, strerror):
			count += 1
			say(csay,'W','<time>','%s (%d), wait for %s'%(strerror,count,hidepath))
			sleep(naptime)
			if count%10 == 0:
				os.chmod(filepath,mode_rw)
				say(csay,'W','<time>','force read access on '+hidepath)
		except:
			count += 1
			print sys.exc_info()
			say(csay,'W','<time>','unexpected error (%d), wait for %s'%(count,hidepath))
			sleep(naptime)
		else:
			break

	if count == count_max:
		return skyd_status(status,1,hidepath+	\
			' open-and-read failed @ '+strftime('%Y/%m/%d %H:%M:%S'))

	#count = 0
	#while not os.access(filepath,os.R_OK):
	#	count += 1
	#	if count > 24:
	#		os.chmod(filepath,mode_rw)
	#		say(csay,'W','<time>','force read access on '+hidepath)
	#		break
	#	say(csay,'W','<time>','waiting for '+hidepath)
	#	sleep(5)

	# We now know that filepath is readable; read it

	#try:
	#	contents = open(filepath,'r').read()
	#except:
	#	print 'file access:',os.access(filepath,os.R_OK)
	#	return skyd_status(status,1,hidepath+	\
	#		' open-and-read failed @ '+strftime('%Y/%m/%d %H:%M:%S'))

	if claim:
		# Switch read-access off for everybody (user,group and other).
		# Note that write-access stays on.

		os.chmod(filepath,mode_w)
		say(csay,'W','<time>',hidepath+' claimed')

	contents = contents.split('\n')
	contents.pop(-1)				# Drop last (empty) entry
	status['contents'] = contents

	return status

#+
# NAME:
#	skyd_release
# PURPOSE:
#	Update and release the configuration file of the SMEI indexing daemon.
#	Only used when daemon is run on multiple machines on the SMEI subnet,
#	i.e. if the indexing is done on machines other than 'boss'.
# CALLING SEQUENCE:
#	status = skyd_release(status,filepath)
# INPUTS:
#	status		dictionary	status['chmod'] is rwx-mode of original of conf
#							status['contents'] is content of updated conf file
#							to be written into 'filepath'
#	filepath	string		name of configuration file
# OUTPUTS:
#	status		dictionary	on success:
#							status['number'] and status['message'] are not modified
#								(should be 0 and blank string, respectively)
#							on failure:
#							status['number' ] is set to 1
#							status['message'] contains error message
#							status['chmod'] and status['contents'] are
#							always removed from the dictionary
# CALLS:
#	skyd_status
# SEE ALSO:
#	skyd_claim
# SIDE EFFECTS:
#	Conf file 'filepath' is put back where it belongs (is 'released').
#	Temporary files are cleaned up.
# PROCEDURE:
#	The original mode of conf file status['chmod'] and the updated configuration
#	status['contents'] are popped from the status dictionary.
#	Then the updated content is written back to the conf file, and
#	the original rwx-mode is set (which 'releases' the file by making it
#	readable again.
# MODIFICATION HISTORY:
#	DEC-2005, Paul Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_release(status,filepath):

	csay = 'skyd_release'
	hidepath = hide_env(filepath)

	if status['number'] == 0:

		# Pop the name of the temporary conf file set up by skyd_claim
		# and the conf file contents from the status dictionary.

		contents = status.pop('contents')
		contents = '\n'.join(contents)+'\n'

		# 'filepath' still has read access switched off, but is writable.

		try:
			open(filepath,'w').write(contents)
		except:
			status = skyd_status(status,1,hidepath+	\
				' write failed @ '+strftime('%Y/%m/%d %H:%M:%S'))

	# Switch read access back on

	os.chmod(filepath,mode_rw)
	say(csay,'W','<time>',hidepath+' released')

	return status

#+
# NAME:
#	skyd_read_conf
# PURPOSE:
#	Read configuration file for SMEI indexing daemon
# CALLING SEQUENCE:
#	status = skyd_read_conf(cffile,claim,status)
# INPUTS:
#	cffile		string		name of configuration file
#	claim		bool		passed to skyd_claim
#	status		dictionary	status dictionary
# OUTPUTS:
#	status		dictionary	on success:
#							status['number'] and status['message'] are not modified
#								(should be 0 and blank string, respectively)
#							status['conf'] is a dictionary with all the conf entries.
#							status['chmod'] is the rwx-mode of the original conf file
#								(claim=TRUE only; added by skyd_claim)
#							on failure:
#							status['number' ] is set to 1
#							status['message'] contains error message
# CALLS:
#	skyd_claim
# SEE ALSO:
#	skyd_write_conf
# PROCEDURE:
#	Only called if smei_orbit is controlled by indexing daemon skyd_wait.
#	Not called if skyd_orbit is called directly.
# MODIFICATION HISTORY:
#	DEC-2005, Paul Hick (UCSD/CASS)
#	FEB-2006, Paul Hick (UCSD/CASS; pphick@ucsd.edu)
#		Modifified to skip empty lines and comments (lines
#		with # character at the beginnin).
#-

def skyd_read_conf(cffile,claim,status):

	# Adds key 'contents' to status
	# If claim=TRUE then key 'chmod' is also added and 'filepath'
	# is read-protected.

	status = skyd_claim(claim,status,cffile)
	if status['number'] != 0:
		return status

	# Pop conf file contents

	contents = status.pop('contents')

	# Analyze conf file contents and fill 'conf' dictionary with
	# separate configuration items.

	conf  = dict()
	names = []

	for line in contents:
		if len(line) == 0:					# Skip empty lines
			continue
		if line.find('#') == 0:				# Skip comments
			continue

		if line.find('=') != -1:			# Found equal sign
			name,value = line.split('=')
			if len(names) == 0:				# Global cf items (e.g. max_proc, grunts)
				conf[name] = value
			else:							# Subitems in group_0, group_1, etc.
				name = name[1:]				# Drop leading underscore
				conf[names[-1]][name] = value
		elif line.find(':') != -1:			# Found colon, i.e. group_0:, group_1:, etc.
			names.append(line.split(':')[0])
			conf[names[-1]] = dict()

	# Attach the config dictionary to the status dictionary
	# Note that all entries in conf are strings, even integers like max_proc.
	# These will need to be explicitly converted to integers when necessary.

	status['conf'] = conf

	return status
#+
# NAME:
#	skyd_write_conf
# PURPOSE:
#	Write configuration file for SMEI indexing daemon
# CALLING SEQUENCE:
#	status = skyd_write_conf(cffile,status)
# INPUTS:
#	cffile		string		name of configuration file
#	status		dictionary	status dictionary
#							Should have keys 'conf' and 'chmod'
# OUTPUTS:
#	status		dictionary	the return status of skyd_release
# CALLS:
#	skyd_release
# SEE ALSO:
#	skyd_read_conf
# PROCEDURE:
#	Only called if smei_orbit is controlled by indexing daemon skyd_wait.
#	Not called if skyd_orbit is called directly.
# MODIFICATION HISTORY:
#	DEC-2005, Paul Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_write_conf(cffile,status):

	# Pop configuration from status dictionary

	conf = status.pop('conf')

	# Convert dictionary to array of strings

	contents = []

	n = 0
	for key in conf:
		if key.find('group_') != 0:
			contents.append(key+'='+conf[key])
		else:						# Create key=<value> lines
			n += 1					# Number of groups group_<i>

	for i in range(n):
		name = 'group_%d'%i
		contents.append(name+':')	# Create line count_<n>:
		for key in conf[name]:		# Add underscore prefix
			contents.append('_'+key+'='+conf[name][key])

	# Put array of strings in key 'contents' and call
	# skyd_release to write the configuration file

	status['contents'] = contents
	status = skyd_release(status,cffile)

	return status

#+
# NAME:
#	skyd_empty_run
# PURPOSE:
#	Set up an empty run with status 'dead'
# CALLING SEQUENCE:
#	run = skyd_empty_run()
# OUTPUTS:
#	run		dictionary with fields set for 'dead' process
# PROCEDURE:
# MODIFICATION HISTORY:
#	DEC-2005, Paul  Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_empty_run():

	run = dict({'report'	: '' 	,
				'wmark'		: '' 	,
				'status'	: 'dead',
				'pid'		: 0	 	,
				'time'		: strftime('%Y_%j_%H%M%S')})

	return run

#+
# NAME:
#	skyd_not_running
# PURPOSE:
#	Find run marked 'dead'
# CALLING SEQUENCE:
#	run = skyd_not_running(lst_runs)
# INPUTS:
#	lst_runs	dictionary with info about processes
#			running on grunts
# PROCEDURE:
#	Return run number for first 'dead' process found.
#	If no 'dead' processes are left then return -1.
# MODIFICATION HISTORY:
#	DEC-2005, Paul  Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_not_running(all_runs):

	runs = range(len(all_runs))
	for run in runs:
		status = all_runs[run]['status']
		if all_runs[run]['status'] == 'dead':
			break
	else:
		run = -1

	return run

#+
# NAME:
#	skyd_count_runs
# PURPOSE:
#	Count processes that haven't finished yet
# CALLING SEQUENCE:
#	count = skyd_count_runs(lst_runs)
# INPUTS:
#	lst_runs	dictionary with info about processes
#			running on grunts
# PROCEDURE:
# MODIFICATION HISTORY:
#	DEC-2005, Paul  Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_count_runs(lst_runs):

	count = 0

	if len(lst_runs) > 0:

		for grunt in lst_runs.keys():
			for run in range(len(lst_runs[grunt])):
				if lst_runs[grunt][run]['status'] != 'dead':
					count += 1

	return count

#+
# NAME:
#	skyd_kill_runs
# PURPOSE:
#	Kill processes running on grunts by sending them
#	a SIGKILL signal
# CALLING SEQUENCE:
#	skyd_kill_runs(lst_runs,boss)
# INPUTS:
#	lst_runs	dictionary with info about processes
#			running on grunts
# PROCEDURE:
#	Sending a keyboard interrupt will cause an exception to occur
#	in skyd_orbit allowing skyd_orbit to update the user catalogue.
#	However this will not kill the smeidb_skyd program.
#	A SIGKILL signal will kill the program but without a catalogue
#	update (leaving orbits with status 'busy').
#	For now we stick with SIGKILL.
# MODIFICATION HISTORY:
#	DEC-2005, Paul  Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_kill_runs(lst_runs,boss):

	csay = 'skyd_kill_runs'

	if len(lst_runs) > 0:

		print
		print

		for grunt in lst_runs.keys():
			for run in range(len(lst_runs[grunt])):
				grunt_pid = lst_runs[grunt][run]['pid']
				if grunt_pid != -1:
					say(csay,'I',grunt,'kill run %d (pid %d)'%(run,grunt_pid))
					if grunt == boss:
						os.kill(grunt_pid,SIGKILL)
					else:
						print (os.popen('ssh '+grunt+' "kill -%d %d"'%(SIGKILL,grunt_pid))).read()			

	return

#+
# NAME:
#	skyd_show_runs
# PURPOSE:
#	Show summary of processes running on grunts
# CALLING SEQUENCE:
#	skyd_show_runs(lst_runs,grunt_,boss)
# INPUTS:
#	lst_runs	dictionary with info about processes
#			running on grunts
# PROCEDURE:
#	Prints dictionary content
# MODIFICATION HISTORY:
#	DEC-2005, Paul  Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_show_runs(lst_runs,grunt_,boss):

	if len(lst_runs) > 0:

		#if grunt_ == '':
		#	grunts = lst_runs.keys()
		#else:
		#	grunts = [grunt_]
		grunts = [[grunt_], lst_runs.keys()][grunt_ == '']

		#max_len = 0
		#for grunt in grunts:
		#	max_len = max([max_len,len(lst_runs[grunt])])
		max_len = 2

		hdr = '%-8s'%boss
		div = '--------'
		for run in range(max_len):
			hdr += ' %-7s'%'status'+'%-6s'%'wmark'+'%7s'%'pid'+'%16s'%strftime('%Y_%j_%H%M%S')
			div += '--------'      +'------'      +'-------'  +'----------------'

		line = '\n\n'+hdr+'\n'+div+'\n'

		for grunt in grunts:

			for run in range(len(lst_runs[grunt])):

				if run == 0:
					line += '%-8s'%grunt
				elif run%max_len == 0:
					line += '\n'+' '*8

				tmp = lst_runs[grunt][run]
				line += ' %-7s'%tmp['status']+'%-6s'%tmp['wmark']+'%7d'%tmp['pid']+'%16s'%tmp['time']

			line += '\n'

		print line

	return

#+
# NAME:
#	skyd_status
# PURPOSE:
#	Updates status of SMEI indexing daemon
# CALLING SEQUENCE:
#	status = skyd_status(status, istat, message)
# INPUTS:
#	status		status dictionary
#	istat		integer		status number
#	message		string		message string
# OUTPUTS:
#	status		updated status dictionary
# CALLS:
#	tiny.say
# PROCEDURE:
#	Two entries in status are updated:
#	status['number' ] = istat
#	status['message'] = message
#	The message is printed to standard output if the length
#	is non-zero.
# MODIFICATION HISTORY:
#		DEC-2005, Paul Hick (UCSD/CASS; pphick@ucsd.edu)
#-

def skyd_status(status, istat, message):

	csay = 'skyd_status'

	status['number' ] = istat
	status['message'] = message

	if len(message) > 0:
		say(csay,'I','<time>',message)

	return status
