Source

Webware / WebKit / AppServer.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
#!/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.

"""

import os
import sys
from time import time
from threading import Thread, Event

from MiscUtils import AbstractError, NoDefault
from MiscUtils.Funcs import asclocaltime
from Application import Application
from ImportManager import ImportManager
from PlugIn import PlugIn
from PidFile import PidFile, ProcessRunning
from ConfigurableForServerSidePath import ConfigurableForServerSidePath
import Profiler

defaultConfig = dict(
    PrintConfigAtStartUp = True,
    Verbose = True,
    SilentURIs = None,
    PlugIns = [],
    PlugInDirs = [],
    CheckInterval = 100,
    PidFile = 'appserver.pid')

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


class AppServer(ConfigurableForServerSidePath):
    """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()

        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)
        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')
        if self._verbose:
            self._silentURIs = self.setting('SilentURIs')
            if self._silentURIs:
                import re
                self._silentURIs = re.compile(self._silentURIs)
        else:
            self._silentURIs = None
        self._plugIns = []
        self._requestID = 0

        self.checkForInstall()
        self.config() # cache the config
        self.printStartUpMessage()
        if self.setting('CheckInterval') is not None:
            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()
        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
            del self._pidFile
            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() - 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 dict(
            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-2010 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.serverSidePath(path)
            for path in self.setting('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)
            for filename in sorted(os.listdir(plugInDir)):
                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 stop(*args, **kw):
    """Stop the AppServer (which may be in a different process)."""
    print "Stopping the AppServer..."
    workDir = kw.get('workDir')
    if workDir:
        pidfile = None
    else:
        if globalAppServer:
            pidfile = globalAppServer._pidFile
        else:
            pidfile = None
        if not pidfile:
            workDir = os.path.dirname(__file__)
    if not pidfile:
        pidfile = PidFile(os.path.join(workDir, 'appserver.pid'), create=0)
    try:
        pidfile.kill()
    except Exception:
        from traceback import print_exc
        print_exc(1)
        print "WebKit cannot terminate the running process."

if __name__ == '__main__':
    main()