On September 25, 2002 10:56 am, Jason Hildebrand wrote:
> On Wed, 2002-09-25 at 08:35, Max Ischenko wrote:
> > Could someone point me to the webware code
> > which handles automagic reloading of servlet code?
>
> Reloading servlets is a nontrivial problem, since one must also
> check all modules imported from that servlet. AFAIK, no one has
> implemented a full-scale solution to this problem.
>
> For alternatives, see
> http://webware.colorstudy.net/twiki/bin/view/Webware/OneShot
Actually, I have implemented this in expWebware and it should be possible for
someone to implement in Webware. Unfortunately, I don't have time to do it
myself.
It works like this:
* the class that controls everything (AppServer in Webware's case, MultiServer
in expWebware) spawns an extra thread that maintains a list of all module
files and their mtimes. It sits in a loop and each cycle it checks to see if
any of the module files have been modified. If it finds any modifications it
sets the flag self._shouldRestart.
* the main threadLoop (also managed by AppServer in Webware) keeps an eye on
the self._shouldRestart flag and calls self._restart() if it is set to true.
NB: this method may only be called by the main thread.
* self._restart() shuts down the AppServer and all applications, flushe stdout
and stderr, then uses os.execve to restart the WebKit process with the same
arguments it was started with. The PID remains the same.
This approach is significantly simpler and faster than oneshot and less prone
to strange errors as the AppServer operates normally unless a module has
changed.
Here are some of difficulties/differences that might be encountered in
implementing this in Webware:
* WebKit.ThreadedAppServer.py is not cleanly object-oriented as several
critical parts of this module are handled via module-level functions (run(),
main(), and shutDown()) and global variables rather than being handled by the
ThreadedAppServer class. The code I've attached belongs to a single class in
expWebware, but in Webware it might need to be spread between
ThreadedAppServer and the module-level functions.
* The startup sequence for Webware is significantly different to that in
expWebware so I'm not certain that this os.execve approach will work.
Cheers,
Tavis
-------------------
def restart(self):
"""Tell the main thread to restart the server."""
self._shouldRestart = True
def _restart(self):
"""Restart the server by completely reinitializing the process with
os.execve(). This method can only be called by the main thread. If a
worker thread calls it, the process will freeze up."""
self.shutdown()
sys.stdout.flush()
sys.stderr.flush()
L = [sys.executable]
# shutdown has already completed so 'start' not 'restart'
L.extend([i.replace('restart','start') for i in sys.argv])
os.execve(L[0], L, os.environ)
def mainThreadWaitLoop(self):
"""The main thread waits here while the server is running and the
worker
threads are doing their thing. This function is called at the end of
the start sequence."""
while self.running:
time.sleep(0.2) #@@TR: softcode this later
if self._shouldRestart:
self._restart()
def activateAutoReload(self):
self._autoReload = True
## @@ Temporary
self._fileMonitorThread = t =
Thread(target=self._fileMonitorThreadLoop)
t.start()
def deactivateAutoReload(self):
self._autoReload = False
try:
self._fileMonitorThread.join()
except:
pass
def _fileMonitorThreadLoop(self,
getmtime=os.path.getmtime):
monitoredFiles = self._monitoredFiles
monitoredModules = self._monitoredModules
while self._autoReload:
time.sleep(1) #@@TR: softcode this later
for mod in sys.modules.values():
if not mod or mod in monitoredModules:
continue
monitoredModules.append(mod)
f2 = getattr(mod, '__orig_file__', False) # @@TR might rename
__orig_file__, this is used for cheetah and psp mods
f = getattr(mod, '__file__', False)
if f2 and f2 not in monitoredFiles.keys():
try:
monitoredFiles[f2] = getmtime(f2)
except OSError:
pass
elif f and f not in monitoredFiles.keys():
try:
monitoredFiles[f] = getmtime(f)
except OSError:
pass
for f, mtime in monitoredFiles.items():
try:
if mtime < getmtime(f):
print '*** The file', f, 'has changed. The server is
restarting now.'
sys.stdout.flush()
sys.stderr.flush()
self._autoReload = False
return self.restart()
except OSError:
print '*** The file', f, 'is no longer accesible The
server is restarting now.'
sys.stdout.flush()
sys.stderr.flush()
self._autoReload = False
return self.restart()
#!/usr/bin/env python
"""Provides a multi-port socket server that dispatches connections on multiple
ports to the services that are bound to them.
Its main use is to dispatch connections from Webware adapters to the WebKit
AppServer.
"""
# $Id: MultiServer.py,v 1.5 2002/09/25 18:32:24 tavis Exp $
__author__ = 'Tavis Rudd <[EMAIL PROTECTED]>'
__revision__ = "$Revision: 1.5 $"[11:-2]
##################################################
## DEPENDENCIES ##
import sys # used for printing to sys.stdout
import os # used to check os.name and for creating
# pipes
import os.path
import signal # used to capture interrupts
import select # used for catching select.error exceptions, etc.
import socket # this should be obvious
from threading import Thread # used to create worker threads
# that process the connections
from string import join as strJoin, lower as strLower
from exceptions import KeyboardInterrupt
from time import time as currentTime
import time
import traceback
# intra-packages imports ...
import Webware.Exceptions
from Webware.SettingsManager import SettingsManager
from PortDispatcher import ThreadedDispatcher
from _properties import Version
import _properties
##################################################
## GLOBALS & CONSTANTS ##
True = (1==1)
False = (1==0)
##################################################
## CLASSES ##
class Error(Webware.Exceptions.Error):
pass
class MultiServer(SettingsManager):
"""A TCP/IP socket server that listens to a specified list of ports and
dispatches incoming connections to the services that have been bound to
each port.
Services are instances of a subclass of the Service class. They can be
simple classes that accept the connection and return a string, or they can
be a a full-blown Application Server class that dispatches the request to an
appropriate servlet and returns the servlet's output. This modularized
approach makes it very easy to create custom application or admin servers,
or to debug this MultiServer independently of service.
Services must be thread-safe if you use the ThreadedDispatcher!!
After initializing the MultiServer you must bind at least one service to a
port using the method MultiServer.bindService(service); where 'service' is
an instance of a Service subclass. Internally, each Service will be wrapped
in a _Dispatcher object that will handle communication with the services
main socket in collaboration with the MultiServer.
By default the MultiServer relies on no external configuration or
cache files. However, almost everything about MultiServer is
configurable via subclassing or at runtime using its
updateSettings(newSettings, merge=True) method, so it is very easy to
create an interface for Webware that parses settings out of config files
upon startup and uses these settings to initialize the MultiServer,
an AppServer service, and Applications inside the AppServer.
Also note that MS COM can be used with this framework if the 'EnableCOM'
setting is set to True. When enabled it does the same as Webware's COMKit.
"""
_shouldRestart = False
_autoReload = False
def __init__(self, **kw):
"""Setup the server, but don't start it yet."""
SettingsManager.__init__(self)
self._initializeSettings()
if kw.has_key('settings'):
self.updateSettings(kw['settings'])
if self.setting('EnableCOM',False):
sys.coinit_flags = 0
self._addrToDispatcherMap = {}
self._serviceIDToAddrMap = {}
self._initFuncs = []
self._cleanupFuncs = []
self.running = False
## AutoReload stuff
# @@TR is this the right place for these
self._monitoredFiles = {}
self._monitoredModules = []
def _initializeSettings(self):
self._settings = {
'ServerName':"WebKit MultiServer",
'ServerVersion':Version,
'EventCheckInterval':100,
'EnableCOM':False,
'RecordPID':True,
'PIDFile':'webkit.pid',
'RecordRunSettings':True,
'RunSettingsFile':'webkit_run_settings.txt', # in the current working dir
'StartupNotice':'',
'ConfigFile':'',
'AutoReload':False,
}
def bindService(self, service, manageService=True):
"""Set the service object that will process connections on a given
hostName/port. 'address' should be a tuple in the form (hostName,
port). Note that hostName can be any legal name for the machine this
process is running on.
The 'manageService' arg specifies where this service needs to be
started and stopped by the MultiServer. The default is True,
but if you are binding a single service to multiple addresses you
should set this to True for the first port you address you bind to and
False for every address thereafter.
If returns a reference to the portDispatcher created for the service.
This reference is intended for internal use only!"""
if self._serviceIDToAddrMap.has_key(service.serviceID()):
raise Error(
'The MultiServer already has a service with the ID ' +
service.serviceID())
dispatcherClass = service.setting('DispatcherClass')
dispatcher = dispatcherClass(self, service, manageService=manageService)
hostName, port = service.serviceAddress()
addr = (socket.gethostbyname(hostName), port)
self._addrToDispatcherMap[addr] = dispatcher
self._serviceIDToAddrMap[service.serviceID()] = addr
if self.running:
dispatcher.start()
return dispatcher
def removeService(self, serviceID):
"""Shutdown and remove a service bound to a port."""
addr = self._serviceIDToAddrMap[serviceID]
self._addrToDispatcherMap[ addr ].shutdown()
del self._serviceIDToAddrMap[serviceID]
self._addrToDispatcherMap[ addr ] = None
del self._addrToDispatcherMap[ addr ]
def serviceIDToAddrMap(self):
return self._serviceIDToAddrMap
def connCountForService(self, serviceID):
"""Return the current connection count for service."""
address = self._serviceIDToAddrMap[serviceID]
return self._addrToDispatcherMap[ address ]._connCount
def startupNotice(self):
return self.setting('StartupNotice')
def activateAutoReload(self):
self._autoReload = True
## @@ Temporary
self._fileMonitorThread = t = Thread(target=self._fileMonitorThreadLoop)
t.start()
def deactivateAutoReload(self):
self._autoReload = False
try:
self._fileMonitorThread.join()
except:
pass
def start(self):
"""Start the portDispatchers then start listening on the active
ports."""
try:
self._startTime = currentTime()
## catch system interrupt signals ##
signal.signal(signal.SIGINT, self.shutdown)
signal.signal(signal.SIGTERM, self.shutdown)
self._PID = os.getpid()
## set the thread event check interval ##
sys.setcheckinterval( self.setting('EventCheckInterval') )
## call the custom init funcs
for func in self._initFuncs:
func()
print
print self.startupNotice()
print "="*80
print 'Starting', self.setting('ServerName'), 'at', \
time.asctime(time.localtime(time.time())), 'local time.'
self.printRunSettingsBlurb()
if not self._addrToDispatcherMap:
raise Error(self.setting('ServerName') +
' must have at least on Service to start.')
try:
self.running = True
## hook for storing info on the PID and ports being monitored ##
self.recordRunSettings()
print
for addr, dispatcher in self._addrToDispatcherMap.items():
dispatcher.start(indent=' ')
# note that a _NonThreadedDispatcher will grab control here
sys.stdout.flush()
if self.running:
# check because of _NonThreadedDispatcher's funny shutdown seq.
print
print self.setting('ServerName'), 'has been started'
print "="*80
print
sys.stdout.flush()
self.mainThreadWaitLoop()
except socket.error, e:
if e[0] == 98:
err = sys.stderr
traceback.print_exc(file=sys.stderr)
print >> err
print >> err, "************************* ERROR *************************"
print >> err, "*****"
print >> err, "***** CAN'T START:"
print >> err, "***** One of the network ports we need is still in use."
print >> err, "***** Try again in 15 seconds. "
print >> err, "***** If it still doesn't work, there might be another" \
" process using the port."
print >> err, "*****"
print >> err
err.flush()
self.shutdown()
return
except KeyboardInterrupt:
err = sys.stderr
print
print >> err, "************************* NOTICE *************************"
print >> err, "*****"
print >> err, "***** A Keyboard Interrupt (i.e. CTRL-C) was received."
print >> err, "***** Shutting down now!"
print >> err, "*****"
print
err.flush()
self.shutdown()
return
except:
err = sys.stderr
traceback.print_exc(file=sys.stderr)
print >> err
print >> err, "************************* ERROR *************************"
print >> err, "*****"
print >> err, "***** Attempt to start", self.setting('ServerName'), "failed"
print >> err, "*****"
print >> err
err.flush()
self.shutdown()
def shutdown(self, signalNum=None, currentStackFrame=None):
"""Cleanup and shutdown the dispatcher."""
self.running = False
try: ## @@ Temporary
self.deactivateAutoReload()
except:
pass
print
print "="*80
print 'Shutting down', self.setting('ServerName'), 'at', \
time.asctime(time.localtime(time.time())), 'local time.'
self.printRunSettingsBlurb()
print
## shutdown the portDispatchers
for addr, portDispatcher in self._addrToDispatcherMap.items():
portDispatcher.shutdown(indent=' ')
self._addrToDispatcherMap[addr] = None
del self._addrToDispatcherMap[addr]
## call the custom cleanupFuncs
for func in self._cleanupFuncs:
func()
print
print self.setting('ServerName'), 'has been shutdown'
print "="*80
if self.setting('RecordPID'):
try:
os.remove(self.setting('PIDFile'))
print "Temporary file", self.setting('PIDFile'), "has been removed."
except:
print "Couldn't remove the PIDFile (" + self.setting('PIDFile') + ")."
if self.setting('RecordRunSettings'):
try:
os.remove(self.setting('RunSettingsFile'))
print "Temporary file", self.setting('RunSettingsFile'), \
"has been removed."
except:
print ("Couldn't remove the runSettingsFile (" +
self.setting('RunSettingsFile') + ").")
print
def mainThreadWaitLoop(self):
"""The main thread waits here while the server is running and the worker
threads are doing their thing. This function is called at the end of
the start sequence."""
while self.running:
time.sleep(0.2) #@@TR: softcode this later
if self._shouldRestart:
self._restart()
def _fileMonitorThreadLoop(self,
getmtime=os.path.getmtime):
monitoredFiles = self._monitoredFiles
monitoredModules = self._monitoredModules
while self._autoReload:
time.sleep(1) #@@TR: softcode this later
for mod in sys.modules.values():
if not mod or mod in monitoredModules:
continue
monitoredModules.append(mod)
f2 = getattr(mod, '__orig_file__', False) # @@TR might rename __orig_file__
f = getattr(mod, '__file__', False)
if f2 and f2 not in monitoredFiles.keys():
try:
monitoredFiles[f2] = getmtime(f2)
except OSError:
pass
elif f and f not in monitoredFiles.keys():
try:
monitoredFiles[f] = getmtime(f)
except OSError:
pass
for f, mtime in monitoredFiles.items():
try:
if mtime < getmtime(f):
print '*** The file', f, 'has changed. The server is restarting now.'
sys.stdout.flush()
sys.stderr.flush()
self._autoReload = False
return self.restart()
except OSError:
print '*** The file', f, 'is no longer accesible The server is restarting now.'
sys.stdout.flush()
sys.stderr.flush()
self._autoReload = False
return self.restart()
def restart(self):
"""Tell the main thread to restart the server."""
self._shouldRestart = True
def _restart(self):
"""Restart the server by completely reinitializing the process with
os.execve(). This method can only be called by the main thread. If a
worker thread calls it, the process will freeze up."""
self.shutdown()
sys.stdout.flush()
sys.stderr.flush()
L = [sys.executable]
# shutdown has already completed so 'start' not 'restart'
L.extend([i.replace('restart','start') for i in sys.argv])
os.execve(L[0], L, os.environ)
def getUserInfo(self):
"""Returns the loginName, uid, and gid of the user running WebKit
"""
if os.name == 'posix':
loginName = os.environ['USER']
uid = os.getuid()
gid = os.getgid()
else:
try:
import win32api
loginName = win32api.GetUserName()
uid = 'N/A'
gid = 'N/A'
except:
loginName = 'N/A'
uid = 'N/A'
gid = 'N/A'
return loginName, uid, gid
def printRunSettingsBlurb(self):
loginName, uid, gid = self.getUserInfo()
serverName = self.setting('ServerName')
print ' Version:', Version
print ' Installed in:', os.path.split(_properties.__file__)[0]
if self.setting('ConfigFile'):
print ' Config file:', self.setting('ConfigFile')
print ' Working directory:', str(os.getcwd())
print ' Process ID: ', str(self._PID)
print ' Current user:', loginName, '(uid: '+ str(uid) + ', gid: ' + str(gid) + ')'
print ' Start time (local):', time.asctime(time.localtime(self._startTime))
print ' Start time (UTC):', time.asctime(time.gmtime(self._startTime))
def recordRunSettings(self):
"""A hook for storing a reference to the PID and server settings"""
loginName, uid, gid = self.getUserInfo()
if self.setting('RecordPID'):
PIDFile = self.setting('PIDFile')
PIDFile = self.settings()['PIDFile'] = os.path.abspath(PIDFile)
print ' Process ID recorded in: ', PIDFile
fp = open( self.setting('PIDFile'), 'w' )
fp.write(str(self._PID))
fp.close()
if self.setting('RecordRunSettings'):
# the runSettings file is written so that it is parsable by the
# ConfigParser module. It might make sense to just record this
# information in plain english and then record it again in a pickled
# dictionary that is saved alongside it. The pickled dict could
# also contain a copy of the config_file settings used to initialize
# WebKit.
runSettingsFile = self.setting('RunSettingsFile')
runSettingsFile = self.settings()['RunSettingsFile'] = os.path.abspath(runSettingsFile)
print ' Run settings recorded in: ', runSettingsFile
fp = open( self.setting('RunSettingsFile'), 'w' )
print >> fp, '[' + self.setting('ServerName') + ']'
print >> fp, 'start_time_utc:', time.asctime(time.gmtime(self._startTime))
print >> fp, 'start_time_local:', time.asctime(time.localtime(self._startTime))
print >> fp, 'pid:', str(self._PID)
if self.setting('ConfigFile'):
print >> fp, 'config_file:', self.setting('ConfigFile')
print >> fp, 'user_name:', loginName
print >> fp, 'user_id:', str(uid)
print >> fp, 'group_id:', str(gid)
print >> fp, 'current_working_dir:', os.getcwd()
print >> fp, 'server_version:', self.setting('ServerVersion')
print >> fp, 'python_version:', sys.version.replace('\n',' ')
print >> fp, 'python_path:', ';'.join(sys.path)
print >> fp, 'sys_event_check_interval:', self.setting('EventCheckInterval')
print >> fp, 'com_enabled:', self.setting('EnableCOM')
print >> fp
for serviceID, address in self._serviceIDToAddrMap.items():
dispatcher = self._addrToDispatcherMap[address]
print >> fp, '[' + serviceID + ']'
print >> fp, "address: %s, %s"% (address[0], address[1])
print >> fp, "threads:", dispatcher.setting('Threads')
print >> fp, "listen_queue_limit:", dispatcher.setting('ListenQueueLimit')
print >> fp
fp.close()
class Service(SettingsManager):
"""An abstract base class for MultiServer services.
Each instances settings must define ServiceID, ServiceName, HostName, Port,
DispatcherClass, and DispatcherSettings
"""
def _initializeSettings(self):
SettingsManager._initializeSettings(self)
self.updateSettings({'DispatcherSettings':{},
'DispatcherClass':ThreadedDispatcher,
'HostName':'localhost',
})
def start(self, indent=' '*2):
pass
def shutdown(self, indent=' '*2):
pass
def handleConnection(self, conn):
raise Webware.Exceptions.SubclassResponsibilityError
def serviceID(self):
return self.setting('ServiceID')
def serviceName(self):
return self.setting('ServiceName')
def serviceAddress(self):
return (self.setting('HostName'), self.setting('Port'))
##################################################
## Example Services ##
class HttpHelloWorld(Service):
def handleConnection(conn):
conn.send(
"HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\nHello World!<BR><BR>\r\n")
conn.close()
class HttpHelloWorldLong(Service):
def handleConnection(conn, buffer="Hi There<BR>\n"*1000):
conn.send(
"HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\nHello World!<BR><BR>\r\n")
reslen = len(buffer)
sent = 0
while sent < reslen:
try:
sent = sent + conn.send( buffer[sent:sent+8192] )
except socket.error, e:
break
conn.close()
class WebwareTestService(Service):
def handleConnection(conn, strJoin=''.join, BUFSIZE=8*1024):
"""For use with a Webware adapter. This won't work by itself.
"""
## get the request dict from the webserver adapter ##
recv = conn.recv
data = []
while 1:
chunk = recv(BUFSIZE)
if not chunk:
break
data.append(chunk)
data = strJoin(data)
buffer = "Content-type: text/html\r\n\r\n\r\nThe webware " + \
"adapter sucessfully transmitted the request.<BR><BR>"
conn.shutdown(0)
conn.send(buffer)
conn.close()
class WebwareEchoService(Service):
def handleConnection(conn):
"""A trivial services that echoes the request data.
For use with a Webware adapter. This won't work by itself.
"""
## get the request dict from the webserver adapter ##
recv = conn.recv
BUFSIZE = 8*1024
data = []
#while 1:
chunk = recv(BUFSIZE)
#if not chunk:
# break
data.append(chunk)
data = strJoin(data, '')
conn.shutdown(0)
buffer = "Content-type: text/html\r\n\r\n" + data
## send the response buffer ##
sent = 0
reslen = len(buffer)
while sent < reslen:
try:
sent = sent + conn.send( buffer[sent:sent+8192] )
except socket.error, e:
break
conn.close()
##################################################
def main():
server = MultiServer()
server.bindService(
HttpHelloWorldLong('Hello World!', ('localhost', 8088),
settings={'Threads':15,
'SysCheckInterval':100,
},
)
)
server.start()
if __name__ == '__main__':
main()