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()