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