#!/usr/bin/env python

"""Launch.py

DESCRIPTION

Python launch script for the WebKit application server.

This launch script will run in its standard location in the Webware/WebKit
directory as well as in a WebKit work directory outside of the Webware tree.

USAGE

Launch.py [StartOptions] [AppServer [AppServerOptions]]

StartOptions:
  -d, --work-dir=...     Set the path to the app server working directory.
                         By default this is the directory containing Lauch.py.
  -w, --webware-dir=...  Set the path to the Webware root directory.
                         By default this is the parent directory.
  -l, --library=...      Other directories to be included in the search path.
                         You may specify this option multiple times.
  -p, --run-profile      Set this to get profiling going (see Profiler.py).
  -o, --log-file=...     Redirect standard output and error to this file.
  -i, --pid-file=...     Set the file path to hold the app server process id.
                         This option is fully supported under Unix only.
  -u, --user=...         The name or uid of the user to run the app server.
                         This option is supported under Unix only.
  -g, --group=...        The name or gid of the group to run the app server.
                         This option is supported under Unix only.

AppServer:
  The name of the application server module.
  By default, the ThreadedAppServer will be used.

AppServerOptions:
  Options that shall be passed to the application server.
  For instance, the ThreadedAppServer accepts: start, stop, daemon
  You can also change configuration settings here by passing
  arguments of the form ClassName.SettingName=value

Please note that the default values for the StartOptions and the AppServer
can be easily changed at the top of the Launch.py script.
"""

# FUTURE
# * This shares a lot of code with ThreadedAppServer.py and Launch.py.
#   Try to consolidate these things. The default settings below in the
#   global variables could go completely into AppServer.config.
# CREDITS
# * Contributed to Webware for Python by Chuck Esterbrook
# * Improved by Ian Bicking
# * Improved by Christoph Zwerschke


## Default options ##

# You can change the following default values:

# The path to the app server working directory, if you do not
# want to use the directory containing this script:
workDir = None

# The path to the Webware root directory; by default this will
# be the parent directory of the directory containing this script:
webwareDir = None

# A list of additional directories (usually some libraries)
# that you want to include into the search path for modules:
libraryDirs = []

# To get profiling going, set runProfile = 1 (see also
# the description in the docstring of Profiler.py):
runProfile = 0

# The path to the log file, if you want to redirect the
# standard output and error to a log file:
logFile = None

# The pass to the pid file, if you want to check and terminate
# a running server by storing its server process id:
pidFile = None

# The name or uid of the server process user, if you want
# to run the server under a different user:
user = None

# The name or uid of the server process group, if you want
# to run the server under a different group:
group = None

# The default app server to be used:
appServer = 'ThreadedAppServer'


## Launch app server ##

import os, sys

def usage():
	"""Print the docstring and exit with error."""
	sys.stdout.write(__doc__)
	sys.exit(2)

def launchWebKit(appServer=appServer, workDir=None, args=None):
	"""Import and launch the specified WebKit app server.

	appServer  -- the name of the WebKit app server module
	workDir -- the server-side work directory of the app server
	args -- other options that will be given to the app server

	"""
	# Set up the arguments for the app server:
	if args is None:
		args = []
	if workDir:
		args.append('workdir=' + workDir)
	# Allow for a .py on the server name:
	if appServer[-3:] == '.py':
		appServer = appServer[:-3]
	# Import the app server's main() function:
	try:
		appServerMain = __import__('WebKit.' + appServer, None, None, 'main').main
	except ImportError:
		print 'Error: Cannot import the app server module.'
		sys.exit(1)
	# Set Profiler.startTime if this has not been done already:
	from WebKit import Profiler
	if Profiler.startTime is None:
		from time import time
		Profiler.startTime = time()
	# Run the app server:
	return appServerMain(args) # go!


## Main ##

def main(args=None):
	"""Evaluate the command line arguments and call launchWebKit."""
	global workDir, webwareDir, libraryDirs, runProfile, \
		logFile, pidFile, user, group, appServer
	if args is None:
		args = sys.argv[1:]
	# Accept AppServer even if placed before StartOptions:
	if args and not args[0].startswith('-'):
		arg2 = args.pop(0)
	else:
		arg2 = None
	# Accept AppServerOptions that look like StartOptions:
	args1 = []; args2 = []
	while args:
		arg = args.pop(0)
		if arg.startswith('--') and \
			2 < arg.find('.', 2) < arg.find('=', 5):
			args2.append(arg)
		else:
			args1.append(arg)
	# Get all launch options:
	from getopt import getopt, GetoptError
	try:
		opts, args1 = getopt(args1, 'd:w:l:po:i:u:g:', [
			'work-dir=', 'webware-dir=', 'library=', 'run-profile',
			'log-file=', 'pid-file=', 'user=', 'group='])
	except GetoptError, error:
		print str(error)
		print
		usage()
	for opt, arg in opts:
		if opt in ('-d', '--work-dir'):
			workDir = arg
		elif opt in ('-w', '--webware-dir'):
			webwareDir = arg
		elif opt in ('-l', '--library'):
			libraryDirs.append(arg)
		elif opt in ('-p', '--run-profile'):
			runProfile = 1
		elif opt in ('-o', '--log-file'):
			logFile = arg
		elif opt in ('-i', '--pid-file'):
			pidFile = arg
		elif opt in ('-u', '--user'):
			user = arg
		elif opt in ('-g', '--group'):
			group = arg
	if arg2:
		appServer = arg2
	elif args1 and not args1[0].startswith('-') \
		and args1[0].find('=') < 0:
		appServer = args1.pop(0)
	args = args2 + args1
	# Figure out the group id:
	gid = group
	if gid is not None:
		try:
			gid = int(gid)
		except ValueError:
			try:
				import grp
				entry = grp.getgrnam(gid)
			except KeyError:
				print 'Error: Group %r does not exist.' % gid
				sys.exit(2)
			except ImportError:
				print 'Error: Group names are not supported.'
				sys.exit(2)
			gid = entry[2]
	# Figure out the user id:
	uid = user
	if uid is not None:
		try:
			uid = int(uid)
		except ValueError:
			try:
				import pwd
				entry = pwd.getpwnam(uid)
			except KeyError:
				print 'Error: User %r does not exist.' % uid
				sys.exit(2)
			except ImportError:
				print 'Error: User names are not supported.'
				sys.exit(2)
			if not gid:
				gid = entry[3]
			uid = entry[2]
	# Figure out the work directory and make it the current directory:
	if workDir:
		workDir = os.path.expanduser(workDir)
	else:
		scriptName = sys.argv and sys.argv[0]
		if not scriptName or scriptName == '-c':
			scriptName = 'Launch.py'
		workDir = os.path.dirname(os.path.abspath(scriptName))
	try:
		os.chdir(workDir)
	except OSError, error:
		print 'Error: Could not set working directory.'
		print 'The path %r cannot be used.' % workDir
		print error.strerror
		print 'Check the --work-dir option.'
		sys.exit(1)
	workDir = os.curdir
	# Expand user components in directories:
	if webwareDir:
		webwareDir = os.path.expanduser(webwareDir)
	else:
		webwareDir = os.pardir
	if libraryDirs:
		libraryDirs = map(os.path.expanduser, libraryDirs)
	# Remove the package component in the name of this module,
	# because otherwise the package path would be used for imports, too:
	global __name__
	name = __name__.split('.')[-1]
	if name != __name__:
		sys.modules[name] = sys.modules[__name__]
		del sys.modules[__name__]
		__name__ = name
	# Check the validity of the Webware directory:
	sysPath = sys.path # memorize the standard Python search path
	sys.path = [webwareDir] # now include only the Webware directory
	try: # check whether Webware is really located here
		from Properties import name as webwareName
		from WebKit.Properties import name as webKitName
	except ImportError:
		webwareName = None
	if webwareName != 'Webware for Python' or webKitName != 'WebKit':
		print 'Error: Cannot find the Webware directory.'
		print 'The path %r seems to be wrong.' % webwareDir
		print 'Check the --webware-dir option.'
		sys.exit(1)
	if not os.path.exists(os.path.join(webwareDir, 'install.log')):
		print 'Error: Webware has not been installed.'
		print 'Please run install.py in the Webware directory:'
		print '> cd', os.path.abspath(webwareDir)
		print '> python install.py'
		sys.exit(1)
	# Now assemble a new clean Python search path:
	path = [] # the new search path will be collected here
	webKitDir = os.path.abspath(os.path.join(webwareDir, 'WebKit'))
	for p in [workDir, webwareDir] + libraryDirs + sysPath:
		if not p:
			continue # do not include the empty ("current") directory
		p = os.path.abspath(p)
		if p == webKitDir or p in path or not os.path.exists(p):
			continue # do not include WebKit and duplicates
		path.append(p)
	sys.path = path # set the new search path
	# Prepare the arguments for launchWebKit:
	args = (appServer, workDir, args)
	# Handle special case where app server shall be stopped:
	if 'stop' in args[2]:
		print 'Stopping WebKit.%s...' % appServer
		errorlevel = launchWebKit(*args)
		if not errorlevel:
			if pidFile and os.path.exists(pidFile):
				try:
					os.remove(pidFile)
				except:
					print 'The pid file could not be removed.'
					print
		sys.exit(errorlevel)
	# Handle the pid file:
	if pidFile:
		pidFile = os.path.expanduser(pidFile)
		# Read the old pid file:
		try:
			pid = int(open(pidFile).read())
		except:
			pid = None
		if pid is not None:
			print 'According to the pid file, the server is still running.'
			# Try to kill an already running server:
			killed = 0
			try:
				from signal import SIGTERM, SIGKILL
				print 'Trying to terminate the server with pid %d...' % pid
				os.kill(pid, SIGTERM)
			except OSError, error:
				from errno import ESRCH
				if error.errno == ESRCH: # no such process
					print 'The pid file was stale, continuing with startup...'
					killed = 1
				else:
					print 'Cannot terminate server with pid %d.' % pid
					print error.strerror
					sys.exit(1)
			except (ImportError, AttributeError):
				print 'Cannot check or terminate server with pid %d.' % pid
				sys.exit(1)
			if not killed:
				from time import sleep
				try:
					for i in range(100):
						sleep(0.1)
						os.kill(pid, SIGTERM)
				except OSError, error:
					from errno import ESRCH
					if error.errno == ESRCH:
						print 'Server with pid %d has been terminated.' % pid
						killed = 1
			if not killed:
				try:
					for i in range(100):
						sleep(0.1)
						os.kill(pid, SIGKILL)
				except OSError, error:
					from errno import ESRCH
					if error.errno == ESRCH:
						print 'Server with pid %d has been killed by force.' % pid
						killed = 1
			if not killed:
				print 'Server with pid %d cannot be terminated.' % pid
				sys.exit(1)
		# Write a new pid file:
		try:
			open(pidFile, 'w').write(str(os.getpid()))
		except:
			print 'The pid file %r could not be written.' % pidFile
			sys.exit(1)
	olduid = oldgid = stdout = stderr = log = None
	errorlevel = 1
	try:
		# Change server process group:
		if gid is not None:
			try:
				oldgid = os.getgid()
				if gid != oldgid:
					os.setgid(gid)
					if group:
						print 'Changed server process group to %r.' % group
				else:
					oldgid = None
			except:
				if group:
					print 'Could not set server process group to %r.' % group
					raise
					oldgid = None
					sys.exit(1)
		# Change server process user:
		if uid is not None:
			try:
				olduid = os.getuid()
				if uid != olduid:
					os.setuid(uid)
					print 'Changed server process user to %r.' % user
				else:
					olduid = None
			except:
				print 'Could not change server process user to %r.' % user
				olduid = None
				sys.exit(1)
		msg = 'WebKit.' + appServer
		if args[2]:
			msg = '%s %s' % (msg, ' '.join(args[2]))
		else:
			msg = 'Starting %s...' % msg
		print msg
		# Handle the log file:
		if logFile:
			logFile = os.path.expanduser(logFile)
			try:
				log = open(logFile, 'a', 1) # append, line buffered mode
				print 'Output has been redirected to %r...' % logFile
				stdout, stderr = sys.stdout, sys.stderr
				sys.stdout = sys.stderr = log
			except IOError, error:
				print 'Cannot redirect output to %r.' % logFile
				print error.strerror
				log = None
				sys.exit(1)
		else:
			print
		# Set up a reference to our profiler so apps can import and use it:
		from WebKit import Profiler
		if Profiler.startTime is None:
			from time import time
			Profiler.startTime = time()
		# Now start the app server:
		if runProfile:
			print 'Profiling is on.', \
				'See docstring in Profiler.py for more info.'
			print
			from profile import Profile
			profiler = Profile()
			Profiler.profiler = profiler
			errorlevel = Profiler.runCall(launchWebKit, *args)
			print
			print 'Writing profile stats to %s...' % Profiler.statsFilename
			Profiler.dumpStats()
			print 'WARNING: Applications run much slower when profiled,'
			print 'so turn off profiling in Launch.py when you are finished.'
		else:
			errorlevel = launchWebKit(*args)
	finally:
		print
		# Close the log file properly:
		if log:
			sys.stdout, sys.stderr = stdout, stderr
			log.close()
		# Restore server process group and user.
		# Note that because we changed the real group and
		# user id of the process (most secure thing), we
		# cannot change them back now, but we try anyway:
		if oldgid is not None:
			try:
				os.setgid(oldgid)
			except:
				pass
			else:
				oldgid = None
		if olduid is not None:
			try:
				os.setuid(olduid)
			except:
				pass
			else:
				olduid = None
		# Remove the pid file again.
		# Note that this may fail when the group or user id
		# has been changed, but we try anyway:
		if pidFile and os.path.exists(pidFile):
			try:
				os.remove(pidFile)
			except:
				if oldgid is None and olduid is None:
					print 'The pid file could not be removed.'
					print
	sys.exit(errorlevel)

if __name__ == '__main__':
	main()
