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

import os
import shutil
import subprocess
import pws
import threading
import signal
import csv
import socket
import ast
import time


class Initializer(object):

	STATE_UNKNOWN		= 0
	STATE_NOT_READY		= 1
	STATE_READY			= 2
	STATE_FAILED		= 3

	def __init__(self, approot):
		self.approot = approot
		self.init_state = Initializer.STATE_UNKNOWN
		self.init_message = ''
		
		if not os.path.isdir(self.approot):
			raise OSError("Application root '%s' is not a valid directory" % self.approot)
		for subdir in ['apache','confs','pgsql','php','pianos','www-default']:
			if not os.path.isdir(os.path.join(self.approot,subdir)):
				raise OSError("Application root directory '%s' is not valid (missing subdirectory '%s')" % (self.approot, subdir))
		print "Using application root directory '%s'" % self.approot
		
		self._check_state()
		
	def is_ready(self):
		return self.init_state == Initializer.STATE_READY
		
	def get_state(self):
		return self.init_state, self.init_message
		
	def _check_state(self):
		assert self.init_state == Initializer.STATE_UNKNOWN
		
		lockfile = os.path.join(self.approot, 'init.lock')
		if os.path.isfile(lockfile):
			self.init_state = Initializer.STATE_FAILED
			self.init_message = 'Lock file is still present. This indicates a crash in the initialization process.'
			return
		
		cluster_dir = os.path.join(self.approot, 'pgsql', 'data')
		if not os.path.isdir(cluster_dir):
			self.init_state = Initializer.STATE_NOT_READY
			self.init_message = 'Database cluster is not initialized.'
			return
			
		pgsql_conf = os.path.join(cluster_dir, 'postgresql.conf')
		setting_not_found_or_wrong = True
		try:
			with file(pgsql_conf, 'r') as f:
				for line in (x.strip() for x in f):
					if 'custom_variable_classes' in line:
						if not 'unil' in line.split('=')[1].strip():
							break
						setting_not_found_or_wrong = False
						break
		except IOError, e:
			self.init_state = Initializer.STATE_FAILED
			self.init_message = "Could not open PostgreSQL configuration file '%s' (%s)" % (pgsql_conf, str(e))
			return

		if setting_not_found_or_wrong:
			self.init_state = Initializer.STATE_FAILED
			self.init_message = 'PostgreSQL configuration file is invalid.'
			return
			
			
		self.init_state = Initializer.STATE_READY
		self.init_message = 'OK'
		
	
	def cleanup(self):
		assert self.init_state == Initializer.STATE_FAILED
		print "Cleaning up initialization"
		
		try:
			lockfile = os.path.join(self.approot, 'init.lock')
			if os.path.isfile(lockfile):
				os.unlink(lockfile)
			cluster_dir = os.path.join(self.approot, 'pgsql', 'data')
			if os.path.isdir(cluster_dir):
				shutil.rmtree(cluster_dir)
		
		except Exception,e:
			self.init_state = Initializer.STATE_FAILED
			self.init_message = str(e)
			return

		self.init_state = Initializer.STATE_UNKNOWN
		self._check_state()
		assert self.init_state == Initializer.STATE_NOT_READY
		
		
	def run(self):
		assert self.init_state == Initializer.STATE_NOT_READY
		
		try:
			lockfile = os.path.join(self.approot, 'init.lock')
			with file(lockfile,'wb') as f:
				f.write('%s' % str(os.getpid()))
				
			workdir = os.path.join(self.approot, 'pgsql', 'bin')
			with pws.CwdChanger(workdir):
				cluster_rel_dir = os.path.join('..', 'data')
				executable = 'initdb.exe'
				args = [executable, '--auth', 'trust', '--pgdata', cluster_rel_dir, '--encoding', 'UTF8', '--username', 'postgres']
				try:
					output = subprocess.check_output(args)
					print output
				except subprocess.CalledProcessError,e:
					self.init_state = Initializer.STATE_FAILED
					self.init_message = str(e)
					return
					
			conf_src = os.path.join(self.approot, 'confs', 'postgresql.conf')		
			conf_dst = os.path.join(self.approot, 'pgsql', 'data')
			shutil.copy(conf_src, conf_dst)
			
			os.unlink(lockfile)
		
		except Exception,e:
			self.init_state = Initializer.STATE_FAILED
			self.init_message = str(e)
			return
			
		self.init_state = Initializer.STATE_UNKNOWN
		self._check_state()
		assert self.init_state == Initializer.STATE_READY
		
	
class Service(object):
	
	def __init__(self, id, name, pid_file, process_name):
		self.server = None
		self.id = id
		self.name = name
		self.pid_file = pid_file
		self.process_name = process_name
		
	def bind(self, server):
		self.server = server
		
	def start(self):
		assert self.server is not None
		if self.is_running():
			raise Exception("Process is already running")
		print "Starting %s service ..." % self.name
		self.do_start()
		print "  %s started" % self.name
		
	def do_start(self):
		raise Exception("Pure virtual function call")
		
	def stop(self):
		assert self.server is not None
		if not self.is_running():
			raise Exception("Process is not running")
		print "Stopping %s service ..." % self.name
		self.do_stop()
		print "  %s stopped" % self.name
		
	def do_stop(self):
		raise Exception("Pure virtual function call")

	def is_running(self):
		assert self.server is not None
		
		processes_by_pid, processes_by_name = self.get_running_processes()
		
		pid = self.read_pid_file()
		if pid is None or pid not in processes_by_pid:
			#print "is_running("+self.name+"): pid",pid,"not running"
			return False
		
		if self.process_name not in processes_by_name:
			#print "is_running("+self.name+"): process name",self.process_name,"not running"
			return False
			
		if not self.check_port():
			#print "is_running("+self.name+"): TCP port",self.server.config[self.id]['port'],"not listening"
			return False
			
		if not self.do_is_running():
			#print "is_running("+self.name+"): custom running status verification failed"
			return False
		
		#print "is_running("+self.name+"): YES"
		return True
	
	def do_is_running(self):
		return True
		
	def read_pid_file(self):
		filename = os.path.join(self.server.approot, self.pid_file)
		#print "is_running("+self.name+"): checking PID file", filename
		try:
			with file(filename,'rb') as f:
				line = f.readline().strip()
			pid = int(line)
			return pid
		except (IOError,ValueError),err:
			#print "is_running("+self.name+"): error while checking PID file:", err
			return None
	
	def get_running_processes(self):
		output = subprocess.check_output(['tasklist','/FO','csv'])
		lines = output.splitlines()[3:]
		by_pid = {}
		by_name = {}
		for p in csv.reader(lines):
			name = p[0]
			pid = int(p[1])
			# _type = p[2]
			# own = p[3]
			# size = p[4]
			by_pid[pid] = name
			if name not in by_name:
				by_name[name] = []
			by_name[name].append(pid)
		return by_pid, by_name
	
	def check_port(self):
		s = socket.socket()
		try:
			s.connect(('localhost', self.server.config[self.id]['port']))
			return True
		except socket.error, e:
			return False	
	
class PostgreSQLService(Service):
	
	def __init__(self):
		super(PostgreSQLService,self).__init__('pgsql', 'PostgreSQL', os.path.join('pgsql','data','postmaster.pid'), 'postgres.exe')
		
	def do_start(self):
		pgdir = os.path.join(self.server.approot, 'pgsql', 'bin')
		with pws.CwdChanger(pgdir):
			cluster_rel_dir = os.path.join('..', 'data')
			logfile = os.path.join('..', '..', 'var', 'postgres.log')
			executable = 'pg_ctl.exe'
			args = [executable, '-D', cluster_rel_dir, '-l', logfile, 'start']
			subprocess.check_call(args, shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE)
	
	def do_stop(self):
		pgdir = os.path.join(self.server.approot, 'pgsql', 'bin')
		with pws.CwdChanger(pgdir):
			cluster_rel_dir = os.path.join('..', 'data')
			executable = 'pg_ctl.exe'
			args = [executable, '-D', cluster_rel_dir, 'stop']
			subprocess.check_call(args)
	
	def do_is_running(self):
		pgdir = os.path.join(self.server.approot, 'pgsql', 'bin')
		with pws.CwdChanger(pgdir):
			cluster_rel_dir = os.path.join('..', 'data')
			executable = 'pg_ctl.exe'
			args = [executable, '-D', cluster_rel_dir, 'status']
			try:
				output = subprocess.check_output(args)
			except subprocess.CalledProcessError,err:
				return False
			return True
	
		
class ApacheService(Service):

	def __init__(self):
		super(ApacheService,self).__init__('apache', 'Apache', os.path.join('apache','Apache24','logs','httpd.pid'), 'pianos_apache_helper.exe')
		self.process = None
		
	def do_start(self):
		if self.process is not None:
			# May happen if we start/stop the server too rapidly, then the is_running() check
			# differs from the reality, as we're using a proxy application to run apache. This
			# proxy may get out of sync with the actual apache app on rare occasions
			self.process.wait() # Wait on the proxy (should be instantaneous)
			self.process = None
		apachedir = os.path.join(self.server.approot, 'apache', 'Apache24', 'bin')
		with pws.CwdChanger(apachedir):
			executable = 'pianos_apache_helper.exe'
			logfile = os.path.join('..', '..', 'var', 'apache.log')
			args = [executable, '.', '-E logfile']
			self.process = subprocess.Popen(args, shell=True, creationflags=subprocess.CREATE_NEW_CONSOLE)

	def do_stop(self):
		apachedir = os.path.join(self.server.approot, 'apache', 'Apache24', 'bin')
		with pws.CwdChanger(apachedir):
			executable = 'trigger_apache_stop_event.exe'
			args = [executable]
			subprocess.check_call(args)
		if self.process is not None:
			self.process.wait()
			self.process = None

		
class Server(object):
	
	def __init__(self, approot, services=[PostgreSQLService, ApacheService]):
		self.approot = approot
		
		config_filename = os.path.join(self.approot, 'pws.conf')
		print "Loading configuration file '%s'" % config_filename
		with file(config_filename,'r') as f:
			self.config = ast.literal_eval(f.read())
		
		self.services = []
		for srvclass in services:
			service = srvclass()
			service.bind(self)
			self.services.append(service)
		
		print "Checking initial services states"
		for service in self.services:
			if service.is_running():
				print "  %s service is already running" % service.name
			else:
				print "  %s service is not running" % service.name
	
	def smart_start_services(self, ids=None, delay=2.0):
		if ids is None:
			ids = set([x.id for x in self.services])
		else:
			ids = set(ids)

		to_check = []
		for service in self.services:
			if not service.id in ids:
				continue
			if service.is_running():
				continue
			service.start()
			to_check.append(service)

		# Order has been sent, now wait 2 (or 'delay') seconds to know if the services are really started
		start = time.clock()
		while len(to_check):
			for service in to_check:
				if service.is_running():
					to_check.remove(service)
					break
			now = time.clock()
			if now-start > delay:
				break
		
		if len(to_check):
			print "WARNING: not all services have started correctly"
			return False
		return True
			
	def smart_stop_services(self, ids=None, delay=2.0):
		if ids is None:
			ids = set([x.id for x in self.services])
		else:
			ids = set(ids)

		to_check = []
		for service in self.services:
			if not service.id in ids:
				continue
			if not service.is_running():
				continue
			service.stop()
			to_check.append(service)

		# Order has been sent, now wait 2 (or 'delay') seconds to know if the services are really stopped
		start = time.clock()
		while len(to_check):
			for service in to_check:
				if not service.is_running():
					to_check.remove(service)
					break
			now = time.clock()
			if now-start > delay:
				break
		
		if len(to_check):
			print "WARNING: not all services have stopped correctly"
			return False
		return True
