#!/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()