from AppServer import AppServer import os from threading import Thread import time import sys import select ''' The purpose of this module is to notice changes to source files, including servlets, PSPs, templates or changes to the Webware source file themselves, and reload the server as necessary to pick up the changes. The server will also be restarted if a file which Webware _tried_ to import is modified. This is so that changes to a file containing a syntax error (which would have prevented it from being imported) will also cause the server to restart. ''' # Attempt to use python-fam (fam = File Alteration Monitor) instead of polling # to see if files have changed. If python-fam is not installed, we fall back # to polling. try: import _fam haveFam = 1 except: haveFam = 0 from ImportSpy import modloader standardLibraryPrefix = '%s/lib/python%i.%i' % \ (sys.prefix, sys.version_info[0], sys.version_info[1]) DefaultConfig = { 'AutoReload': 0, 'AutoReloadPollInterval': 1, # in seconds } class AutoReloadingAppServer(AppServer): ## AppServer methods which this class overrides def __init__(self,path=None): AppServer.__init__(self,path) self._autoReload = 0 self._shouldRestart = 0 self._requests = [] self._pipe = None if self.isPersistent(): if self.setting('AutoReload'): self.activateAutoReload() def defaultConfig(self): conf = AppServer.defaultConfig(self) conf.update(DefaultConfig) return conf def shutDown(self): print 'Stopping AutoReload Monitor' sys.stdout.flush() self._shuttingDown = 1 self.deactivateAutoReload() AppServer.shutDown(self) ## Activatation of AutoReload def activateAutoReload(self): """Start the monitor thread""" modloader.activate() if not self._autoReload: if haveFam: print 'AutoReload Monitor started, using FAM' self._fileMonitorThread = t = Thread(target=self.fileMonitorThreadLoopFAM) else: print 'AutoReload Monitor started, polling every %d seconds' % self.setting('AutoReloadPollInterval') self._fileMonitorThread = t = Thread(target=self.fileMonitorThreadLoop) self._autoReload = 1 t.setName('AutoReloadMonitor') t.start() def deactivateAutoReload(self): """Tell the monitor thread to stop.""" self._autoReload = 0 if haveFam: # send a message down the pipe to wake up the monitor # thread and tell him to quit self._pipe[1].write('close') self._pipe[1].flush() try: self._fileMonitorThread.join() except: pass ## Restart methods def restartIfNecessary(self): """ This should be called regularly to see if a restart is required. Tavis Rudd claims: "this method can only be called by the main thread. If a worker thread calls it, the process will freeze up." I've implemented it so that the ThreadedAppServer's control thread calls this. That thread is _not_ the MainThread (the initial thread created by the Python interpreter), but I've never encountered any problems. Most likely Tavis meant a freeze would occur if a _worker_ called this. """ if self._shouldRestart: self.restart() def restart(self): """Do the actual restart.""" self.initiateShutdown() self._closeThread.join() sys.stdout.flush() sys.stderr.flush() # calling execve() is problematic, since the file descriptors don't # get closed by the OS. This can result in leaked database connections. # Instead, we exit with a special return code which is recognized by # the AppServer script, which will restart us upon receiving that code. sys.exit(3) ## Callbacks def monitorNewModule(self, filepath, mtime): """ This is a callback which ImportSpy invokes to notify us of new files to monitor. This is only used when we are using FAM. """ self._requests.append(self._fc.monitorFile(filepath, filepath) ) ## Internal methods def shouldRestart(self): """Tell the main thread to restart the server.""" self._shouldRestart = 1 def fileMonitorThreadLoop(self, getmtime=os.path.getmtime): pollInterval = self.setting('AutoReloadPollInterval') while self._autoReload: time.sleep(pollInterval) for f, mtime in modloader.fileList().items(): try: if mtime < getmtime(f): print '*** The file', f, 'has changed. The server is restarting now.' self._autoReload = 0 return self.shouldRestart() except OSError: print '*** The file', f, 'is no longer accessible. The server is restarting now.' self._autoReload = 0 return self.shouldRestart() print 'Autoreload Monitor stopped' sys.stdout.flush() def fileMonitorThreadLoopFAM(self, getmtime=os.path.getmtime): modloader.notifyOfNewFiles(self.monitorNewModule) self._fc = fc = _fam.open() # for all of the modules which have _already_ been loaded, we check # to see if they've already been modified or deleted for f, mtime in modloader.fileList().items(): if mtime < getmtime(f): try: if mtime < getmtime(f): print '*** The file', f, 'has changed. The server is restarting now.' self._autoReload = 0 return self.shouldRestart() except OSError: print '*** The file', f, 'is no longer accessible The server is restarting now.' self._autoReload = 0 return self.shouldRestart() # request that this file be monitored for changes self._requests.append( fc.monitorFile(f, f) ) # create a pipe so that this thread can be notified when the # server is shutdown. We use a pipe because it needs to be an object # which will wake up the call to 'select' r,w = os.pipe() r = os.fdopen(r,'r') w = os.fdopen(w,'w') self._pipe = pipe = (r,w) while self._autoReload: try: # we block here until a file has been changed, or until # we receive word that we should shutdown (via the pipe) ri, ro, re = select.select([fc,pipe[0]], [], []) except select.error, er: errnumber, strerr = er if errnumber == errno.EINTR: continue else: print strerr sys.exit(1) while fc.pending(): fe = fc.nextEvent() if fe.code2str() in ['changed','deleted','created']: print '*** The file %s has changed. The server is restarting now.' % fe.userData self._autoReload = 0 return self.shouldRestart() for req in self._requests: req.cancelMonitor() fc.close() print 'Autoreload Monitor stopped' sys.stdout.flush()