From 15f82c701161b327cef54b469c00b6cbe01534db Mon Sep 17 00:00:00 2001 From: Unai Zalakain Date: Mon, 14 Oct 2013 07:33:45 +0200 Subject: [PATCH] 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. --- AUTHORS | 3 ++ django/utils/autoreload.py | 74 ++++++++++++++++++++++++++++++++------ docs/ref/django-admin.txt | 12 +++++++ docs/releases/1.7.txt | 3 ++ 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index c7490182bf..6e208cd369 100644 --- a/AUTHORS +++ b/AUTHORS @@ -286,6 +286,7 @@ answer newbie questions, and generally made Django that much better: Will Hardy Brian Harring Brant Harris + Pascal Hartig Ronny Haryanto Axel Haustant Hawkeye @@ -372,6 +373,7 @@ answer newbie questions, and generally made Django that much better: Vladimir Kuzma Denis Kuzmichyov Panos Laganakos + Chris Lamb Nick Lane Łukasz Langa Stuart Langridge @@ -666,6 +668,7 @@ answer newbie questions, and generally made Django that much better: Jesse Young Marc Aymerich Gubern Wiktor Kołodziej + Unai Zalakain Mykola Zamkovoi zegor Gasper Zejn diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index e1627f4954..ba98530f1b 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -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 diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 2d9dcd9920..6bb03347c9 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -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 diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 784ee86493..9a7ff41cf7 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -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 ^^^^^^