2005-07-21 12:08:54 +08:00
|
|
|
# Autoreloading launcher.
|
|
|
|
# Borrowed from Peter Hunt and the CherryPy project (http://www.cherrypy.org).
|
|
|
|
# Some taken from Ian Bicking's Paste (http://pythonpaste.org/).
|
2005-07-21 12:10:47 +08:00
|
|
|
#
|
|
|
|
# Portions copyright (c) 2004, CherryPy Team (team@cherrypy.org)
|
|
|
|
# All rights reserved.
|
|
|
|
#
|
|
|
|
# Redistribution and use in source and binary forms, with or without modification,
|
|
|
|
# are permitted provided that the following conditions are met:
|
|
|
|
#
|
|
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
|
|
# this list of conditions and the following disclaimer.
|
|
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
|
|
# and/or other materials provided with the distribution.
|
|
|
|
# * Neither the name of the CherryPy Team nor the names of its contributors
|
|
|
|
# may be used to endorse or promote products derived from this software
|
|
|
|
# without specific prior written permission.
|
|
|
|
#
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
|
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
|
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
|
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
|
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
|
|
# 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.
|
2005-07-21 12:08:54 +08:00
|
|
|
|
2013-11-24 23:43:24 +08:00
|
|
|
from __future__ import absolute_import # Avoid importing `importlib` from this package.
|
|
|
|
|
2013-10-11 19:25:14 +08:00
|
|
|
import os
|
|
|
|
import signal
|
|
|
|
import sys
|
2013-11-05 04:55:52 +08:00
|
|
|
import tempfile
|
2013-10-11 19:25:14 +08:00
|
|
|
import time
|
|
|
|
import traceback
|
2006-05-31 22:53:23 +08:00
|
|
|
|
2013-11-02 17:28:22 +08:00
|
|
|
from django.conf import settings
|
2013-10-14 13:33:45 +08:00
|
|
|
from django.core.signals import request_finished
|
2013-11-05 21:30:31 +08:00
|
|
|
from django.utils._os import upath
|
2013-11-24 23:43:24 +08:00
|
|
|
from importlib import import_module
|
2013-11-11 17:46:24 +08:00
|
|
|
from django.utils import six
|
2006-05-31 22:53:23 +08:00
|
|
|
try:
|
2012-08-18 16:56:56 +08:00
|
|
|
from django.utils.six.moves import _thread as thread
|
2006-05-31 22:53:23 +08:00
|
|
|
except ImportError:
|
2012-07-20 22:16:57 +08:00
|
|
|
from django.utils.six.moves import _dummy_thread as thread
|
2005-07-21 12:08:54 +08:00
|
|
|
|
2006-07-29 06:13:34 +08:00
|
|
|
# This import does nothing, but it's necessary to avoid some race conditions
|
|
|
|
# in the threading module. See http://code.djangoproject.com/ticket/2330 .
|
|
|
|
try:
|
2013-10-18 19:25:30 +08:00
|
|
|
import threading # NOQA
|
2006-07-29 06:13:34 +08:00
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
|
2011-03-20 06:09:38 +08:00
|
|
|
try:
|
|
|
|
import termios
|
|
|
|
except ImportError:
|
|
|
|
termios = None
|
2006-07-29 06:13:34 +08:00
|
|
|
|
2013-10-14 13:33:45 +08:00
|
|
|
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
|
|
|
|
|
2013-10-31 06:08:12 +08:00
|
|
|
try:
|
|
|
|
import select
|
|
|
|
select.kevent, select.kqueue
|
|
|
|
USE_KQUEUE = True
|
|
|
|
|
|
|
|
import resource
|
|
|
|
NOFILES_SOFT, NOFILES_HARD = resource.getrlimit(resource.RLIMIT_NOFILE)
|
2013-11-01 17:19:37 +08:00
|
|
|
|
|
|
|
import subprocess
|
|
|
|
command = ["sysctl", "-n", "kern.maxfilesperproc"]
|
|
|
|
NOFILES_KERN = int(subprocess.check_output(command).strip())
|
2013-12-04 17:11:42 +08:00
|
|
|
except Exception:
|
2013-10-31 06:08:12 +08:00
|
|
|
USE_KQUEUE = False
|
|
|
|
|
2005-07-21 12:08:54 +08:00
|
|
|
RUN_RELOADER = True
|
|
|
|
|
2008-08-09 01:54:35 +08:00
|
|
|
_mtimes = {}
|
|
|
|
_win = (sys.platform == "win32")
|
|
|
|
|
2012-12-12 11:33:03 +08:00
|
|
|
_error_files = []
|
|
|
|
|
2013-11-03 04:12:09 +08:00
|
|
|
|
2013-10-14 13:33:45 +08:00
|
|
|
def gen_filenames():
|
|
|
|
"""
|
2013-11-02 17:28:22 +08:00
|
|
|
Yields a generator over filenames referenced in sys.modules and translation
|
|
|
|
files.
|
2013-10-14 13:33:45 +08:00
|
|
|
"""
|
|
|
|
filenames = [filename.__file__ for filename in sys.modules.values()
|
|
|
|
if hasattr(filename, '__file__')]
|
2013-11-02 17:28:22 +08:00
|
|
|
|
2013-11-05 21:30:31 +08:00
|
|
|
if settings.USE_I18N:
|
|
|
|
# Add the names of the .mo files that can be generated
|
|
|
|
# by compilemessages management command to the list of files watched.
|
|
|
|
basedirs = [os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
|
|
|
'conf', 'locale'),
|
|
|
|
'locale']
|
|
|
|
for appname in reversed(settings.INSTALLED_APPS):
|
|
|
|
app = import_module(appname)
|
|
|
|
basedirs.append(os.path.join(os.path.dirname(upath(app.__file__)),
|
|
|
|
'locale'))
|
|
|
|
basedirs.extend(settings.LOCALE_PATHS)
|
|
|
|
basedirs = [os.path.abspath(basedir) for basedir in basedirs
|
|
|
|
if os.path.isdir(basedir)]
|
|
|
|
for basedir in basedirs:
|
|
|
|
for dirpath, dirnames, locale_filenames in os.walk(basedir):
|
|
|
|
for filename in locale_filenames:
|
|
|
|
if filename.endswith('.mo'):
|
|
|
|
filenames.append(os.path.join(dirpath, filename))
|
2013-11-02 17:28:22 +08:00
|
|
|
|
2012-12-17 16:46:26 +08:00
|
|
|
for filename in filenames + _error_files:
|
2012-12-26 09:37:39 +08:00
|
|
|
if not filename:
|
|
|
|
continue
|
2008-08-09 01:54:35 +08:00
|
|
|
if filename.endswith(".pyc") or filename.endswith(".pyo"):
|
|
|
|
filename = filename[:-1]
|
2011-08-15 16:29:08 +08:00
|
|
|
if filename.endswith("$py.class"):
|
|
|
|
filename = filename[:-9] + ".py"
|
2013-10-14 13:33:45 +08:00
|
|
|
if os.path.exists(filename):
|
|
|
|
yield filename
|
|
|
|
|
2013-11-03 07:53:29 +08:00
|
|
|
|
2013-10-14 13:33:45 +08:00
|
|
|
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)
|
|
|
|
|
2013-11-05 04:55:52 +08:00
|
|
|
# New modules may get imported when a request is processed.
|
2013-10-14 13:33:45 +08:00
|
|
|
request_finished.connect(update_watch)
|
|
|
|
|
2013-11-05 04:55:52 +08:00
|
|
|
# Block until an event happens.
|
|
|
|
update_watch()
|
2013-10-14 13:33:45 +08:00
|
|
|
notifier.check_events(timeout=None)
|
|
|
|
notifier.stop()
|
|
|
|
|
|
|
|
# If we are here the code must have changed.
|
|
|
|
return True
|
|
|
|
|
2013-11-03 07:53:29 +08:00
|
|
|
|
2013-10-31 06:08:12 +08:00
|
|
|
def kqueue_code_changed():
|
|
|
|
"""
|
|
|
|
Checks for changed code using kqueue. After being called
|
|
|
|
it blocks until a change event has been fired.
|
|
|
|
"""
|
|
|
|
kqueue = select.kqueue()
|
|
|
|
|
2013-11-05 04:55:52 +08:00
|
|
|
# Utility function to create kevents.
|
2013-10-31 06:08:12 +08:00
|
|
|
_filter = select.KQ_FILTER_VNODE
|
2013-12-03 01:12:04 +08:00
|
|
|
flags = select.KQ_EV_ADD | select.KQ_EV_CLEAR
|
2013-10-31 17:15:42 +08:00
|
|
|
fflags = select.KQ_NOTE_DELETE | select.KQ_NOTE_WRITE | select.KQ_NOTE_RENAME
|
2013-10-31 06:08:12 +08:00
|
|
|
|
2013-11-05 04:55:52 +08:00
|
|
|
def make_kevent(descriptor):
|
|
|
|
return select.kevent(descriptor, _filter, flags, fflags)
|
|
|
|
|
|
|
|
# New modules may get imported when a request is processed. We add a file
|
|
|
|
# descriptor to the kqueue to exit the kqueue.control after each request.
|
2013-11-11 17:46:24 +08:00
|
|
|
buf_kwargs = {'buffering' if six.PY3 else 'bufsize': 0}
|
|
|
|
watcher = tempfile.TemporaryFile(**buf_kwargs)
|
2013-11-05 04:55:52 +08:00
|
|
|
kqueue.control([make_kevent(watcher)], 0)
|
|
|
|
|
|
|
|
def update_watch(sender=None, **kwargs):
|
2013-11-11 22:48:33 +08:00
|
|
|
watcher.write(b'.')
|
2013-11-05 04:55:52 +08:00
|
|
|
|
|
|
|
request_finished.connect(update_watch)
|
|
|
|
|
|
|
|
# We have to manage a set of descriptors to avoid the overhead of opening
|
|
|
|
# and closing every files whenever we reload the set of files to watch.
|
|
|
|
filenames = set()
|
|
|
|
descriptors = set()
|
|
|
|
|
|
|
|
while True:
|
|
|
|
old_filenames = filenames
|
|
|
|
filenames = set(gen_filenames())
|
|
|
|
new_filenames = filenames - old_filenames
|
|
|
|
|
|
|
|
# If new files were added since the last time we went through the loop,
|
|
|
|
# add them to the kqueue.
|
|
|
|
if new_filenames:
|
|
|
|
|
|
|
|
# We must increase the maximum number of open file descriptors
|
|
|
|
# because each kevent uses one file descriptor and resource limits
|
|
|
|
# are too low by default.
|
|
|
|
#
|
|
|
|
# In fact there are two limits:
|
|
|
|
# - kernel limit: `sysctl kern.maxfilesperproc` -> 10240 on OS X.9
|
|
|
|
# - resource limit: `launchctl limit maxfiles` -> 256 on OS X.9
|
|
|
|
#
|
|
|
|
# The latter can be changed with Python's resource module, but it
|
|
|
|
# can never exceed the former. Unfortunately, getrlimit(3) -- used
|
|
|
|
# by both launchctl and the resource module -- reports no "hard
|
|
|
|
# limit", even though the kernel sets one.
|
|
|
|
|
|
|
|
# If project is too large or kernel limits are too tight, use polling.
|
|
|
|
if len(filenames) >= NOFILES_KERN:
|
|
|
|
return code_changed()
|
|
|
|
|
|
|
|
# Add the number of file descriptors we're going to use to the current
|
|
|
|
# resource limit, while staying within the kernel limit.
|
|
|
|
nofiles_target = min(len(filenames) + NOFILES_SOFT, NOFILES_KERN)
|
|
|
|
resource.setrlimit(resource.RLIMIT_NOFILE, (nofiles_target, NOFILES_HARD))
|
|
|
|
|
|
|
|
new_descriptors = set(open(filename) for filename in new_filenames)
|
|
|
|
descriptors |= new_descriptors
|
|
|
|
|
|
|
|
kqueue.control([make_kevent(descriptor) for descriptor in new_descriptors], 0)
|
|
|
|
|
|
|
|
events = kqueue.control([], 1)
|
|
|
|
|
|
|
|
# After a request, reload the set of watched files.
|
|
|
|
if len(events) == 1 and events[0].ident == watcher.fileno():
|
|
|
|
continue
|
|
|
|
|
|
|
|
# If the change affected another file, clean up and exit.
|
|
|
|
for descriptor in descriptors:
|
|
|
|
descriptor.close()
|
|
|
|
watcher.close()
|
|
|
|
kqueue.close()
|
|
|
|
|
|
|
|
return True
|
2013-10-31 06:08:12 +08:00
|
|
|
|
|
|
|
|
2013-10-14 13:33:45 +08:00
|
|
|
def code_changed():
|
|
|
|
global _mtimes, _win
|
|
|
|
for filename in gen_filenames():
|
2008-08-09 01:54:35 +08:00
|
|
|
stat = os.stat(filename)
|
|
|
|
mtime = stat.st_mtime
|
|
|
|
if _win:
|
|
|
|
mtime -= stat.st_ctime
|
|
|
|
if filename not in _mtimes:
|
|
|
|
_mtimes[filename] = mtime
|
|
|
|
continue
|
|
|
|
if mtime != _mtimes[filename]:
|
|
|
|
_mtimes = {}
|
2012-12-12 11:33:03 +08:00
|
|
|
try:
|
|
|
|
del _error_files[_error_files.index(filename)]
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2008-08-09 01:54:35 +08:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2013-11-03 07:53:29 +08:00
|
|
|
|
2012-12-12 11:33:03 +08:00
|
|
|
def check_errors(fn):
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
try:
|
|
|
|
fn(*args, **kwargs)
|
|
|
|
except (ImportError, IndentationError, NameError, SyntaxError,
|
|
|
|
TypeError, AttributeError):
|
|
|
|
et, ev, tb = sys.exc_info()
|
|
|
|
|
|
|
|
if getattr(ev, 'filename', None) is None:
|
|
|
|
# get the filename from the last item in the stack
|
|
|
|
filename = traceback.extract_tb(tb)[-1][0]
|
|
|
|
else:
|
|
|
|
filename = ev.filename
|
|
|
|
|
|
|
|
if filename not in _error_files:
|
|
|
|
_error_files.append(filename)
|
|
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
2013-11-03 07:53:29 +08:00
|
|
|
|
2011-03-20 06:09:38 +08:00
|
|
|
def ensure_echo_on():
|
|
|
|
if termios:
|
2011-03-24 20:36:33 +08:00
|
|
|
fd = sys.stdin
|
|
|
|
if fd.isatty():
|
|
|
|
attr_list = termios.tcgetattr(fd)
|
|
|
|
if not attr_list[3] & termios.ECHO:
|
|
|
|
attr_list[3] |= termios.ECHO
|
2011-06-04 23:29:11 +08:00
|
|
|
if hasattr(signal, 'SIGTTOU'):
|
|
|
|
old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
|
|
|
else:
|
|
|
|
old_handler = None
|
2011-03-24 20:36:33 +08:00
|
|
|
termios.tcsetattr(fd, termios.TCSANOW, attr_list)
|
2011-06-04 23:29:11 +08:00
|
|
|
if old_handler is not None:
|
|
|
|
signal.signal(signal.SIGTTOU, old_handler)
|
2011-03-20 06:09:38 +08:00
|
|
|
|
2013-11-03 07:53:29 +08:00
|
|
|
|
2005-07-21 12:08:54 +08:00
|
|
|
def reloader_thread():
|
2011-03-20 06:09:38 +08:00
|
|
|
ensure_echo_on()
|
2013-10-14 13:33:45 +08:00
|
|
|
if USE_INOTIFY:
|
|
|
|
fn = inotify_code_changed
|
2013-10-31 06:08:12 +08:00
|
|
|
elif USE_KQUEUE:
|
|
|
|
fn = kqueue_code_changed
|
2013-10-14 13:33:45 +08:00
|
|
|
else:
|
|
|
|
fn = code_changed
|
2005-07-21 12:08:54 +08:00
|
|
|
while RUN_RELOADER:
|
2013-10-14 13:33:45 +08:00
|
|
|
if fn():
|
2013-11-03 03:27:47 +08:00
|
|
|
sys.exit(3) # force reload
|
2005-07-21 12:08:54 +08:00
|
|
|
time.sleep(1)
|
|
|
|
|
2013-10-14 13:33:45 +08:00
|
|
|
|
2005-07-21 12:08:54 +08:00
|
|
|
def restart_with_reloader():
|
|
|
|
while True:
|
2011-01-17 21:15:08 +08:00
|
|
|
args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] + sys.argv
|
2005-07-21 12:08:54 +08:00
|
|
|
if sys.platform == "win32":
|
|
|
|
args = ['"%s"' % arg for arg in args]
|
|
|
|
new_environ = os.environ.copy()
|
|
|
|
new_environ["RUN_MAIN"] = 'true'
|
|
|
|
exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)
|
|
|
|
if exit_code != 3:
|
|
|
|
return exit_code
|
|
|
|
|
2013-11-03 07:53:29 +08:00
|
|
|
|
2008-08-09 01:54:35 +08:00
|
|
|
def python_reloader(main_func, args, kwargs):
|
2005-07-21 12:08:54 +08:00
|
|
|
if os.environ.get("RUN_MAIN") == "true":
|
|
|
|
thread.start_new_thread(main_func, args, kwargs)
|
|
|
|
try:
|
|
|
|
reloader_thread()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
try:
|
2011-12-30 22:12:31 +08:00
|
|
|
exit_code = restart_with_reloader()
|
|
|
|
if exit_code < 0:
|
|
|
|
os.kill(os.getpid(), -exit_code)
|
|
|
|
else:
|
|
|
|
sys.exit(exit_code)
|
2005-07-21 12:08:54 +08:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
2008-08-09 01:54:35 +08:00
|
|
|
|
2013-11-03 07:53:29 +08:00
|
|
|
|
2008-08-09 01:54:35 +08:00
|
|
|
def jython_reloader(main_func, args, kwargs):
|
|
|
|
from _systemrestart import SystemRestart
|
|
|
|
thread.start_new_thread(main_func, args)
|
|
|
|
while True:
|
|
|
|
if code_changed():
|
|
|
|
raise SystemRestart
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
def main(main_func, args=None, kwargs=None):
|
|
|
|
if args is None:
|
|
|
|
args = ()
|
|
|
|
if kwargs is None:
|
|
|
|
kwargs = {}
|
|
|
|
if sys.platform.startswith('java'):
|
|
|
|
reloader = jython_reloader
|
|
|
|
else:
|
|
|
|
reloader = python_reloader
|
2012-12-12 11:33:03 +08:00
|
|
|
|
|
|
|
wrapped_main_func = check_errors(main_func)
|
|
|
|
reloader(wrapped_main_func, args, kwargs)
|