#!/usr/bin/env python

"""The AppServer singleton.

The `AppServer` singleton is the controlling object/process/thread.
`AppServer` receives requests and dispatches them to `Application`
(via `Application.dispatchRawRequest`).

There is only one instance of AppServer, `globalAppServer` contains
that instance. Use it like:

    from WebKit.AppServer import globalAppServer

`ThreadedAppServer` completes the implementation, dispatching
these requests to separate threads. `AppServer`, at least in the
abstract, could support different execution models and environments,
but that support is not yet realized (Will it ever be realized?).

The distinction between `AppServer` and `Application` is somewhat
vague -- both are global singletons and both handle dispatching requests.
`AppServer` works on a lower level, handling sockets and threads.

"""

from threading import Thread, Event

from Common import *
from Object import Object
from Application import Application
from ImportManager import ImportManager
from PlugIn import PlugIn
from PidFile import PidFile, ProcessRunning
from ConfigurableForServerSidePath import ConfigurableForServerSidePath
import Profiler

defaultConfig = {
	'PrintConfigAtStartUp': True,
	'Verbose': True,
	'PlugIns': [],
	'PlugInDirs': [],
	'CheckInterval': 100,
	'PidFile': 'appserver.pid',
}

# This actually gets set inside AppServer.__init__
globalAppServer = None


class AppServer(ConfigurableForServerSidePath, Object):
	"""The AppServer singleton.

	Purpose and usage are explained in the module docstring.

	"""


	## Init ##

	def __init__(self, path=None):
		"""Sets up and starts the `AppServer`.

		`path` is the working directory for the AppServer
		(directory in which AppServer is contained, by default)

		This method loads plugins, creates the Application object,
		and starts the request handling loop.

		"""
		self._running = 0
		self._startTime = time.time()

		global globalAppServer
		if globalAppServer:
			raise ProcessRunning('More than one AppServer'
				' or __init__() invoked more than once.')
		globalAppServer = self

		# Set up the import manager:
		self._imp = ImportManager()

		ConfigurableForServerSidePath.__init__(self)
		Object.__init__(self)
		if path is None:
			path = os.path.dirname(__file__) # os.getcwd()
		self._serverSidePath = os.path.abspath(path)
		self._webKitPath = os.path.abspath(os.path.dirname(__file__))
		self._webwarePath = os.path.dirname(self._webKitPath)

		self.recordPID()

		self._verbose = self.setting('Verbose')
		self._plugIns = []
		self._requestID = 0

		self.checkForInstall()
		self.config() # cache the config
		self.printStartUpMessage()
		sys.setcheckinterval(self.setting('CheckInterval'))
		self._app = self.createApplication()
		self.loadPlugIns()

		# @@ 2003-03 ib: shouldn't this just be in a subclass's __init__?
		if self.isPersistent():
			self._closeEvent = Event()
			self._closeThread = Thread(target=self.closeThread,
				name="CloseThread")
			# self._closeThread.setDaemon(1)
			self._closeThread.start()
		self._running = 1

	def checkForInstall(self):
		"""Check whether Webware was installed.

		Exits with an error message if Webware was not installed.
		Called from `__init__`.

		"""
		if not os.path.exists(os.path.join(self._webwarePath, 'install.log')):
			sys.stdout = sys.stderr
			print 'ERROR: You have not installed Webware.'
			print 'Please run install.py from inside the Webware directory.'
			print 'For example:'
			print '> cd ..'
			print '> python install.py'
			print
			sys.exit(0)

	def readyForRequests(self):
		"""Declare ready for getting requests.

		Should be invoked by subclasses when they are finally ready to
		accept requests. Records some stats and prints a message.

		"""
		if Profiler.startTime is None:
			Profiler.startTime = self._startTime
		Profiler.readyTime = time.time()
		Profiler.readyDuration = Profiler.readyTime - Profiler.startTime
		print "Ready (%.2f seconds after launch)." % Profiler.readyDuration
		print
		sys.stdout.flush()
		sys.stderr.flush()

	def closeThread(self):
		"""This method is called when the shutdown sequence is initiated."""
		if self.isPersistent():
			self._closeEvent.wait()
		self.shutDown()

	def initiateShutdown(self):
		"""Ask the master thread to begin the shutdown."""
		if self.isPersistent():
			self._closeEvent.set()

	def recordPID(self):
		"""Save the pid of the AppServer to a file."""
		if self.setting('PidFile') is None:
			self._pidFile = None
			return
		pidpath = self.serverSidePath(self.setting('PidFile'))
		try:
			self._pidFile = PidFile(pidpath)
		except ProcessRunning:
			raise ProcessRunning('The file ' + pidpath + ' exists\n'
				'and contains a process id corresponding to a running process.\n'
				'This indicates that there is an AppServer already running.\n'
				'If this is not the case, delete this file and restart the AppServer.')

	def shutDown(self):
		"""Shut down the AppServer.

		Subclasses may override and normally follow this sequence:
			1. set self._running = 1 (request to shut down)
			2. class specific statements for shutting down
			3. Invoke super's shutDown() e.g., `AppServer.shutDown(self)`
			4. set self._running = 0 (server is completely down)

		"""
		if self._running:
			print "AppServer is shutting down..."
			sys.stdout.flush()
			self._running = 1
			self._app.shutDown()
			del self._plugIns
			del self._app
			if self._pidFile:
				self._pidFile.remove() # remove the pid file
			if Profiler.profiler:
				# The profile stats will be dumped by Launch.py.
				# You might also considering having a page/servlet
				# that lets you dump the stats on demand.
				print 'AppServer ran for %0.2f seconds.' % (
					time.time() - Profiler.startTime)
			print "AppServer has been shutdown."
			sys.stdout.flush()
			sys.stderr.flush()
			self._running = 0


	## Configuration ##

	def defaultConfig(self):
		"""The default AppServer.config."""
		return defaultConfig # defined on the module level

	def configFilename(self):
		"""Return the name of the AppServer configuration file."""
		return self.serverSidePath('Configs/AppServer.config')

	def configReplacementValues(self):
		"""Get config values that need to be escaped."""
		# Since these strings may be eval'ed as ordinary strings,
		# we need to use forward slashes instead of backslashes.
		# Note: This is only needed for old style config files.
		# In new style config files, they are note eval'ed, but used
		# directly, so double escaping would be a bad idea here.
		return {
			'WebwarePath': self._webwarePath.replace('\\', '/'),
			'WebKitPath': self._webKitPath.replace('\\', '/'),
			'serverSidePath': self._serverSidePath.replace('\\', '/'),
			}


	## Network Server ##

	def createApplication(self):
		"""Create and return an application object. Invoked by __init__."""
		return Application(server=self)

	def printStartUpMessage(self):
		"""Invoked by __init__, prints a little intro."""
		print 'WebKit AppServer', self.version()
		print 'Part of Webware for Python.'
		print 'Copyright 1999-2009 by Chuck Esterbrook. All Rights Reserved.'
		print 'WebKit and Webware are open source.'
		print 'Please visit: http://www.webwareforpython.org'
		print
		print 'Process id is', os.getpid()
		print 'Date/time is', asclocaltime()
		print 'Python is', sys.version.replace(') [', ')\n[')
		print
		if self.setting('PrintConfigAtStartUp'):
			self.printConfig()


	## Plug-in loading ##

	def plugIns(self):
		"""Return a list of the plug-ins loaded by the app server.

		Each plug-in is a Python package.

		"""
		return self._plugIns

	def plugIn(self, name, default=NoDefault):
		""" Return the plug-in with the given name. """
		# @@ 2001-04-25 ce: linear search. yuck.
		# Plus we should guarantee plug-in name uniqueness anyway
		for plugin in self._plugIns:
			if plugin.name() == name:
				return plugin
		if default is NoDefault:
			raise KeyError, name
		else:
			return default

	def loadPlugIn(self, path):
		"""Load and return the given plug-in.

		May return None if loading was unsuccessful (in which case this method
		prints a message saying so). Used by `loadPlugIns` (note the **s**).

		"""
		plugIn = None
		path = self.serverSidePath(path)
		try:
			plugIn = PlugIn(self, path)
			willNotLoadReason = plugIn.load()
			if willNotLoadReason:
				print '    Plug-in %s cannot be loaded because:\n' \
					'    %s' % (path, willNotLoadReason)
				return None
			plugIn.install()
		except Exception:
			print
			print 'Plug-in', path, 'raised exception.'
			raise
		return plugIn

	def loadPlugIns(self):
		"""Load all plug-ins.

		A plug-in allows you to extend the functionality of WebKit without
		necessarily having to modify its source. Plug-ins are loaded by
		AppServer at startup time, just before listening for requests.
		See the docs in `WebKit.PlugIn` for more info.

		"""
		plugIns = self.setting('PlugIns')
		plugIns = map(lambda path, ssp=self.serverSidePath: ssp(path), plugIns)

		# Scan each directory named in the PlugInDirs list.
		# If those directories contain Python packages (that don't have
		# a "dontload" file) then add them to the plugs in list.
		for plugInDir in self.setting('PlugInDirs'):
			plugInDir = self.serverSidePath(plugInDir)
			fileNames = os.listdir(plugInDir)
			fileNames.sort()
			for filename in fileNames:
				filename = os.path.normpath(os.path.join(plugInDir, filename))
				if (os.path.isdir(filename)
					and os.path.exists(os.path.join(filename, '__init__.py'))
					and os.path.exists(os.path.join(filename, 'Properties.py'))
					and not os.path.exists(os.path.join(filename, 'dontload'))
					and os.path.basename(filename) != 'WebKit'
					and filename not in plugIns):
					plugIns.append(filename)

		print 'Plug-ins list:', ', '.join(plugIns) or 'empty'

		# Now that we have our plug-in list, load them...
		for plugInPath in plugIns:
			plugIn = self.loadPlugIn(plugInPath)
			if plugIn:
				self._plugIns.append(plugIn)
		print


	## Accessors ##

	def version(self):
		"""Return WebKit version."""
		if not hasattr(self, '_webKitVersionString'):
			from MiscUtils.PropertiesObject import PropertiesObject
			props = PropertiesObject(os.path.join(self.webKitPath(), 'Properties.py'))
			self._webKitVersionString = props['versionString']
		return self._webKitVersionString

	def application(self):
		"""Return the Application singleton."""
		return self._app

	def startTime(self):
		"""Return the time the app server was started.

		The time is given as seconds, like time().

		"""
		return self._startTime

	def numRequests(self):
		"""Return the number of requests.

		Returns the number of requests received by this app server
		since it was launched.

		"""
		return self._requestID

	def isPersistent(self):
		"""Check whether the AppServer is persistent.

		When using `OneShot`, the AppServer will exist only for a single
		request, otherwise it will stay around indefinitely.

		"""
		raise AbstractError, self.__class__

	def serverSidePath(self, path=None):
		"""Return the absolute server-side path of the WebKit app server.

		If the optional path is passed in, then it is joined with the
		server side directory to form a path relative to the app server.

		"""
		if path:
			return os.path.normpath(os.path.join(self._serverSidePath, path))
		else:
			return self._serverSidePath

	def webwarePath(self):
		"""Return the Webware path."""
		return self._webwarePath

	def webKitPath(self):
		"""Return teh WebKit path."""
		return self._webKitPath


## Main ##

def main():
	"""Start the Appserver."""
	try:
		server = AppServer()
		print "Ready."
		print
		print "WARNING: There is nothing to do here with the abstract AppServer."
		print "Use one of the adapters such as WebKit.cgi (with ThreadedAppServer)"
		print "or OneShot.cgi"
		server.shutDown()
	except Exception, exc: # Need to kill the sweeper thread somehow
		print "Caught exception:", exc
		print "Exiting AppServer..."
		server.shutDown()
		del server
		sys.exit()

def kill(pid):
	"""Kill a process."""
	try:
		from signal import SIGTERM
		os.kill(pid, SIGTERM)
	except Exception:
		if os.name == 'nt':
			import win32api
			handle = win32api.OpenProcess(1, 0, pid)
			win32api.TerminateProcess(handle, 0)
		else:
			raise

def stop(*args, **kw):
	"""Stop the AppServer (which may be in a different process)."""
	print "Stopping the AppServer..."
	if kw.has_key('workDir'):
		# app directory
		pidfile = os.path.join(kw['workDir'], "appserver.pid")
	else:
		# pidfile is in WebKit directory
		pidfile = os.path.join(os.path.dirname(__file__), "appserver.pid")
	try:
		pid = int(open(pidfile).read())
	except Exception:
		print "Cannot read process id from pidfile."
	else:
		try:
			kill(pid)
		except Exception:
			from traceback import print_exc
			print_exc(1)
			print "WebKit cannot terminate the running process."

if __name__ == '__main__':
	main()
