Auto reload with Apache, mod_wsgi, and Django on Windows

Posted by james on Nov. 11, 2009

Django has the built-in 'manage.py runserver', which is very convenient since it auto-reloads when code is modified. On unix, you can set WSGI to auto reload when code is modified, but on windows it's trickier. I've figured out how to do this by modifying the code at http://blog.dscpl.com.au/2008/12/using-modwsgi-when-developing-django.html.

Here's the line in my apache httpd.conf file:

WSGIScriptAlias /tracker c:/webpage/tracker/tracker/apache/tracker.wsgi

And in my tracker.wsgi file:

# This causes apache/WSGI to auto-reload django whenever a python file changes.
import monitor
monitor.start(interval=1.0)


This assumes that your app's main folder is in the python path, so that 'import monitor' can find monitor.py. This is my modified monitor.py. Really I just had to add the kill() function (from the python FAQs) and modify the _restart() function to use it. And yay! Now I can run django apps in apache via mod_wsgi and enjoy auto-reload as I develop. The reloading seems faster than the django runserver, which is a nice bonus.

The only downside is that django output is now in the main apache log. I'll have to figure that out later.

# This file monitors a path for changes

import os
import sys
import time
import signal
import threading
import atexit
import Queue

_interval = 1.0
_times = {}
_files = []

_running = False
_queue = Queue.Queue()
_lock = threading.Lock()

def kill(pid):
    """kill function for Win32"""
    import win32api
    handle = win32api.OpenProcess(1, 0, pid)
    return (0 != win32api.TerminateProcess(handle, 0))

def _restart(path):
    _queue.put(True)
    prefix = 'monitor (pid=%d):' % os.getpid()
    print >> sys.stderr, '%s Change detected to \'%s\'.' % (prefix, path)
    print >> sys.stderr, '%s Triggering process restart.' % prefix
    if hasattr(os, 'kill'):
        os.kill(os.getpid(), signal.SIGINT)
    else:
        # Windows
        kill(os.getpid())

def _modified(path):
    try:
        # If path doesn't denote a file and were previously
        # tracking it, then it has been removed or the file type
        # has changed so force a restart. If not previously
        # tracking the file then we can ignore it as probably
        # pseudo reference such as when file extracted from a
        # collection of modules contained in a zip file.

        if not os.path.isfile(path):
            return path in _times

        # Check for when file last modified.

        mtime = os.stat(path).st_mtime
        if path not in _times:
            _times[path] = mtime

        # Force restart when modification time has changed, even
        # if time now older, as that could indicate older file
        # has been restored.

        if mtime != _times[path]:
            return True
    except:
        # If any exception occured, likely that file has been
        # been removed just before stat(), so force a restart.

        return True

    return False

def _monitor():
    while 1:
        # Check modification times on all files in sys.modules.

        for module in sys.modules.values():
            if not hasattr(module, '__file__'):
                continue
            path = getattr(module, '__file__')
            if not path:
                continue
            if os.path.splitext(path)[1] in ['.pyc', '.pyo', '.pyd']:
                path = path[:-1]
            if _modified(path):
                return _restart(path)

        # Check modification times on files which have
        # specifically been registered for monitoring.

        for path in _files:
            if _modified(path):
                return _restart(path)

        # Go to sleep for specified interval.

        try:
            return _queue.get(timeout=_interval)
        except:
            pass

_thread = threading.Thread(target=_monitor)
_thread.setDaemon(True)

def _exiting():
    try:
        _queue.put(True)
    except:
        pass
    _thread.join()

atexit.register(_exiting)

def track(path):
    if not path in _files:
        _files.append(path)

def start(interval=1.0):
    global _interval
    if interval < _interval:
        _interval = interval

    global _running
    _lock.acquire()
    if not _running:
        prefix = 'monitor (pid=%d):' % os.getpid()
        print >> sys.stderr, '%s Starting change monitor.' % prefix
        _running = True
        _thread.start()
    _lock.release()