#!/usr/bin/env python

#FIXME make a check that checks after 2 seconds of having actually started services up, to see if they are still up, and if they are not, shutdown the whole thing safely

import signal
import os

import logging

#FIXME make these imports dynamic, perhaps by making the components themselves load dynamically on request as a plugin fashion
# Machinery imports
from Index import Index
from Databases import ZODBDatabase
from pgtools.Catalog import PGCatalog
from pgtools.DatabaseCluster import PGDatabaseCluster
from AdaptiveLoadController import AdaptiveLoadController
from InfospaceAgents.Filesystem.FilesystemAgent import FilesystemAgent
from InterfaceServers.SocketXMLRPC.SocketXMLRPCInterfaceServer import SocketXMLRPCInterfaceServer
from InterfaceServers.XMLRPC.XMLRPCInterfaceServer import XMLRPCInterfaceServer
from Component import Component
from PublicInterface import PublicInterface
import ConfigParser
import Config
import stat

class TerminateRequest(Exception):
	"""A dummy class to use whenever we catch a signal"""

# startup process: setup logging, initialize index(with database), initialize load controller, initialize infospace agents(index) and register them with the index, initialize interface servers ([load controller] + [index] + infospace_agents), start them in that order
# shutdown process: stop interface servers, stop infospace agents, stop load controller, close index, end logging.
class MasterService(Component):
	config = None
	index = None
	dbcluster = None
	load_controller = None
	infospace_agents = []
	interface_servers = []
# 	log = None
	
	def __init__(self,config_filename=None):
		Component.__init__(self,public_interface=MetadataServicePublicInterface("metadataService",self))
		self.set_component_name("MetadataService")
		self.config = Config.get(config_filename)

	def setup_logging(self):
		log = logging.getLogger()
		log.setLevel(self.config.getint("logging","log level"))

		stderr_handler = logging.StreamHandler()
		stderr_format = "%(message)s"
		stderr_formatter = logging.Formatter(stderr_format)
		stderr_handler.setFormatter(stderr_formatter)
		log.addHandler(stderr_handler)

		logfile = None
		try: logfile = self.config.get("logging","log file")
		except ConfigParser.NoOptionError: pass

		if logfile:
			format = "%(levelname)s:%(name)s: %(message)s"
			formatter = logging.Formatter(format)
			if logfile == "stderr":
				handler = stderr_handler
				handler.setFormatter(formatter)
			else:
				handler = logging.FileHandler(logfile)
				handler.setFormatter(formatter)
				log.addHandler(handler)


		# if the user didn't configure a log file, or the log file is not stderr
		# we just set the default stderr handler to ERROR
		if not logfile or logfile != "stderr":
			stderr_handler.setLevel(logging.ERROR)

		syslog = self.config.getboolean("logging","use syslog")
		if syslog:
			facility = self.config.getint("logging","log facility")
			handler = logging.handlers.SysLogHandler(address="/dev/log",facility=facility)
			format = "%(name)s: %(message)s"
			formatter = logging.Formatter(format)
			handler.setFormatter(formatter)
			log.addHandler(handler)

		log.debug("Using config file %s",self.config.get_filename())
		
	def install_sighandlers(self):
		signal.signal(signal.SIGINT,self.__signal_handler)
		signal.signal(signal.SIGTERM,self.__signal_handler)
		signal.signal(signal.SIGHUP,self.__signal_handler)
	
	def end_logging(self):
		logging.shutdown()
	
	def initialize_dbcluster(self):
		indexpath = self.config.get("index","path")
		if not os.path.isdir(indexpath):
			os.makedirs(indexpath)
			os.chmod(indexpath,stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
		self.log.debug("Starting database cluster")
		self.dbcluster = PGDatabaseCluster(os.path.join(indexpath,"catalog"))
		self.dbcluster.start()
		self.log.debug("Database cluster started")
	
	def initialize_index(self):
		# fixme make modular
		assert self.dbcluster is not None
		
		indexpath = self.config.get("index","path")
		self.log.debug("Starting index")
		database = ZODBDatabase(os.path.join(indexpath,"database"))
		catalog_arguments = self.dbcluster.get_database("catalog")
		catalog = PGCatalog(**catalog_arguments)
		self.index = Index(database=database,catalog=catalog)
		self.log.debug("Index started")
	
	def close_index(self):
		if self.index:
			self.log.debug("Closing index")
			self.index.close()
			self.log.debug("Index closed")
			
	def close_dbcluster(self):
		if self.dbcluster:
			self.log.debug("Stopping database cluster")
			try:
				self.dbcluster.stop_if_started()
				self.log.debug("Database cluster stopped")
			except: self.log.error("Could not stop database cluster",exc_info=True)
	
	def initialize_load_controller(self):
		load = self.config.getfloat("load controller","target load")
		self.load_controller = AdaptiveLoadController(target_load=load)
	
	def initialize_infospace_agents(self):
		# fixme make modular
		assert isinstance(self.index,Index)
		i = FilesystemAgent(index=self.index,config=self.config)
		self.infospace_agents.append(i)
		self.index.register_infospace_agent(i)
	
	def initialize_interface_servers(self):
		# fixme make modular
		components = self.get_components()
		assert len(components) > 0 #fixme make modular and organize interface servers folder into reasonable packages
		
		interfaces = [ c.public_interface for c in self.get_components() if c.public_interface is not None ]
		
		self.interface_servers.append(SocketXMLRPCInterfaceServer(interfaces=interfaces,config=self.config))
		
		self.interface_servers.append(XMLRPCInterfaceServer(interfaces=interfaces,config=self.config))
	
	def get_services(self): return [self.load_controller] + self.infospace_agents + self.interface_servers

	def get_components(self): return [self,self.index] + self.get_services()

	def start_services(self):
		self.log.debug("Starting services")
		services = self.get_services()
		
		try:
			for service in services:
				self.log.debug("Starting service %s",service.get_component_name())
				service.start()
				self.log.debug("Service %s started",service.get_component_name())
		except TerminateRequest: raise # This will be caught in the run() method
		except:
			self.log.critical("Failed to start service %s, startup aborted",service.get_component_name(),exc_info=True)
			raise
		
		self.log.debug("Services started")
	
	def stop_services(self):
		self.log.debug("Stopping services")
		
		services = self.get_services()
		services.remove(self.load_controller)
		services.append(self.load_controller)
		services.reverse()
		for service in services:
			try:
				self.log.debug("Stopping %s",service.get_component_name())
				if not service.isAlive():
					self.log.error("Service %s had died prematurely",service.get_component_name())
				else:
					service.kill()
					self.log.debug("Service %s stopped",service.get_component_name())
			except AttributeError: pass
			except:
				self.log.error("Cannot stop service %s",service,exc_info=True)

		self.log.debug("Services stopped")

	def run(self):
		# the setup phase
		self.setup_logging()
		self.install_sighandlers()
		self.log.info("Metadata service starting")
		retvalue = 0
		
		# the start phase
		errmsg = None
		try:
			self.initialize_dbcluster()
			try: self.initialize_index()
			except (IOError,OSError), e:
				errmsg = "Metadata service cannot access the database directory %s.  Please check your configuration and the directory permissions."%self.config.get("index","path")
				raise
			self.initialize_load_controller()
			self.initialize_infospace_agents()
			self.initialize_interface_servers()
			self.start_services()
		except TerminateRequest,signum:
			self.log.warning("Metadata service interrupted by signal %s while starting sub-services",signum)
			retvalue = 8
		except:
			if errmsg: self.log.critical(errmsg,exc_info=True)
			else: self.log.critical("Metadata service failed to start one of the sub-services, cannot continue",exc_info=True)
			retvalue = 4 # an error initializing sub-services


		
		# the run phase
		if retvalue == 0:
			self.log.info("Metadata service started")
			try:
				while True: signal.pause() # this should rather wait for events on a queue instead of staying here with signal.pause() -  events like TerminateRequest, ReloadConfigRequest, and, of course, the signal handlers (and the public interface for the metadata service!) should feed the queues with events.
			except TerminateRequest,sig: self.log.info("Metadata service ending on signal %s",sig)
			except:
				self.log.error("Anomalous exception in signal.pause()",exc_info=True)
				retvalue = 16
		
		# the stop phase
		self.stop_services()
		self.close_index()
		self.close_dbcluster()
		self.log.info("Metadata service ended")
		self.end_logging()
		return retvalue
	
	def request_exit(self,signum=signal.SIGTERM): # these actually do stuff
		self.__block_sighandlers() # this to avoid signals to the process while exiting
		raise TerminateRequest, signum
	
# 	def reload_config(self): # these actually do stuff
# 		self.block_sighandlers() # this to avoid signals to the process while exiting
# 		raise NotImplemented # implement!
# 		self.unblock_sighandlers() # restore previous condition
	
	__signal_block = False
	def __block_sighandlers(self): self.__signal_block = True
	def __unblock_sighandlers(self): self.__signal_block = False
	
	def __signal_handler(self,signum,arg2):
		if self.__signal_block: return
# 		if signum == signal.SIGHUP: self.reload_config()
		if signum in (signal.SIGTERM, signal.SIGINT): self.request_exit(signum)

# 	def request_exit(self): os.kill(os.getpid(),signal.SIGTERM) # we need to do it thsi way because if we raise a terminate request it will be raised in the context of the calling process.
# 	def request_reload_config(self): os.kill(os.getpid(),signal.SIGHUP)  #ditto

#Write the interface for this metadata service, and migrate to Component semantics


class MetadataServicePublicInterface(PublicInterface):
	def shutDown(self):
		os.kill(os.getpid(),signal.SIGTERM)
		return True
