Commit 15f82c70 authored by Unai Zalakain's avatar Unai Zalakain Committed by Tim Graham
Browse files

Fixed #9722 - used pyinotify as change detection system when available

Used pyinotify (when available) to replace the "pool-every-one-second"
mechanism in `django.utils.autoreload`.

Thanks Chris Lamb and Pascal Hartig for work on the patch.
parent e9cb333b
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -286,6 +286,7 @@ answer newbie questions, and generally made Django that much better:
    Will Hardy <django@willhardy.com.au>
    Brian Harring <ferringb@gmail.com>
    Brant Harris
    Pascal Hartig <phartig@rdrei.net>
    Ronny Haryanto <http://ronny.haryan.to/>
    Axel Haustant <noirbizarre@gmail.com>
    Hawkeye
@@ -372,6 +373,7 @@ answer newbie questions, and generally made Django that much better:
    Vladimir Kuzma <vladimirkuzma.ch@gmail.com>
    Denis Kuzmichyov <kuzmichyov@gmail.com>
    Panos Laganakos <panos.laganakos@gmail.com>
    Chris Lamb <lamby@debian.org>
    Nick Lane <nick.lane.au@gmail.com>
    Łukasz Langa <lukasz@langa.pl>
    Stuart Langridge <http://www.kryogenix.org/>
@@ -666,6 +668,7 @@ answer newbie questions, and generally made Django that much better:
    Jesse Young <adunar@gmail.com>
    Marc Aymerich Gubern
    Wiktor Kołodziej <wiktor@pykonik.org>
    Unai Zalakain <unai@gisa-elkartea.org>
    Mykola Zamkovoi <nickzam@gmail.com>
    zegor
    Gasper Zejn <zejn@kiberpipa.org>
+63 −11
Original line number Diff line number Diff line
@@ -28,12 +28,14 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import datetime
import os
import signal
import sys
import time
import traceback

from django.core.signals import request_finished
try:
    from django.utils.six.moves import _thread as thread
except ImportError:
@@ -51,6 +53,18 @@ try:
except ImportError:
    termios = None

USE_INOTIFY = False
try:
    # Test whether inotify is enabled and likely to work
    import pyinotify

    fd = pyinotify.INotifyWrapper.create().inotify_init()
    if fd >= 0:
        USE_INOTIFY = True
        os.close(fd)
except ImportError:
    pass

RUN_RELOADER = True

_mtimes = {}
@@ -58,14 +72,13 @@ _win = (sys.platform == "win32")

_error_files = []

def code_changed():
    global _mtimes, _win
    filenames = []
    for m in list(sys.modules.values()):
        try:
            filenames.append(m.__file__)
        except AttributeError:
            pass

def gen_filenames():
    """
    Yields a generator over filenames referenced in sys.modules.
    """
    filenames = [filename.__file__ for filename in sys.modules.values()
                if hasattr(filename, '__file__')]
    for filename in filenames + _error_files:
        if not filename:
            continue
@@ -73,8 +86,42 @@ def code_changed():
            filename = filename[:-1]
        if filename.endswith("$py.class"):
            filename = filename[:-9] + ".py"
        if not os.path.exists(filename):
            continue # File might be in an egg, so it can't be reloaded.
        if os.path.exists(filename):
            yield filename

def inotify_code_changed():
    """
    Checks for changed code using inotify. After being called
    it blocks until a change event has been fired.
    """
    wm = pyinotify.WatchManager()
    notifier = pyinotify.Notifier(wm)

    def update_watch(sender=None, **kwargs):
        mask = (
            pyinotify.IN_MODIFY |
            pyinotify.IN_DELETE |
            pyinotify.IN_ATTRIB |
            pyinotify.IN_MOVED_FROM |
            pyinotify.IN_MOVED_TO |
            pyinotify.IN_CREATE
        )
        for path in gen_filenames():
            wm.add_watch(path, mask)

    request_finished.connect(update_watch)
    update_watch()

    # Block forever
    notifier.check_events(timeout=None)
    notifier.stop()

    # If we are here the code must have changed.
    return True

def code_changed():
    global _mtimes, _win
    for filename in gen_filenames():
        stat = os.stat(filename)
        mtime = stat.st_mtime
        if _win:
@@ -129,11 +176,16 @@ def ensure_echo_on():

def reloader_thread():
    ensure_echo_on()
    if USE_INOTIFY:
        fn = inotify_code_changed
    else:
        fn = code_changed
    while RUN_RELOADER:
        if code_changed():
        if fn():
            sys.exit(3) # force reload
        time.sleep(1)


def restart_with_reloader():
    while True:
        args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] + sys.argv
+12 −0
Original line number Diff line number Diff line
@@ -794,6 +794,18 @@ needed. You don't need to restart the server for code changes to take effect.
However, some actions like adding files or compiling translation files don't
trigger a restart, so you'll have to restart the server in these cases.

If you are using Linux and install `pyinotify`_, kernel signals will be used to
autoreload the server (rather than polling file modification timestamps each
second). This offers better scaling to large projects, reduction in response
time to code modification, more robust change detection, and battery usage
reduction.

.. _pyinotify: https://pypi.python.org/pypi/pyinotify/

.. versionadded:: 1.7

    ``pyinotify`` support was added.

When you start the server, and each time you change Python code while the
server is running, the server will validate all of your installed models. (See
the ``validate`` command below.) If the validator finds errors, it will print
+3 −0
Original line number Diff line number Diff line
@@ -343,6 +343,9 @@ Management Commands
  Django takes this information from your settings file. If you have configured
  multiple caches or multiple databases, all cache tables are created.

* The :djadmin:`runserver` command now uses ``inotify`` Linux kernel signals
  for autoreloading if ``pyinotify`` is installed.

Models
^^^^^^