autoreload.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # Autoreloading launcher.
  2. # Borrowed from Peter Hunt and the CherryPy project (http://www.cherrypy.org).
  3. # Some taken from Ian Bicking's Paste (http://pythonpaste.org/).
  4. #
  5. # Portions copyright (c) 2004, CherryPy Team (team@cherrypy.org)
  6. # All rights reserved.
  7. #
  8. # Redistribution and use in source and binary forms, with or without modification,
  9. # are permitted provided that the following conditions are met:
  10. #
  11. # * Redistributions of source code must retain the above copyright notice,
  12. # this list of conditions and the following disclaimer.
  13. # * Redistributions in binary form must reproduce the above copyright notice,
  14. # this list of conditions and the following disclaimer in the documentation
  15. # and/or other materials provided with the distribution.
  16. # * Neither the name of the CherryPy Team nor the names of its contributors
  17. # may be used to endorse or promote products derived from this software
  18. # without specific prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  21. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
  24. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  25. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  26. # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  27. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  28. # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  29. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. from __future__ import absolute_import # Avoid importing `importlib` from this package.
  31. import os
  32. import signal
  33. import sys
  34. import time
  35. import traceback
  36. from django.apps import apps
  37. from django.conf import settings
  38. from django.core.signals import request_finished
  39. try:
  40. from django.utils.six.moves import _thread as thread
  41. except ImportError:
  42. from django.utils.six.moves import _dummy_thread as thread
  43. # This import does nothing, but it's necessary to avoid some race conditions
  44. # in the threading module. See http://code.djangoproject.com/ticket/2330 .
  45. try:
  46. import threading # NOQA
  47. except ImportError:
  48. pass
  49. try:
  50. import termios
  51. except ImportError:
  52. termios = None
  53. USE_INOTIFY = False
  54. try:
  55. # Test whether inotify is enabled and likely to work
  56. import pyinotify
  57. fd = pyinotify.INotifyWrapper.create().inotify_init()
  58. if fd >= 0:
  59. USE_INOTIFY = True
  60. os.close(fd)
  61. except ImportError:
  62. pass
  63. RUN_RELOADER = True
  64. FILE_MODIFIED = 1
  65. I18N_MODIFIED = 2
  66. _mtimes = {}
  67. _win = (sys.platform == "win32")
  68. _error_files = []
  69. _cached_modules = set()
  70. _cached_filenames = []
  71. def gen_filenames(only_new=False):
  72. """
  73. Returns a list of filenames referenced in sys.modules and translation
  74. files.
  75. """
  76. global _cached_modules, _cached_filenames
  77. module_values = set(sys.modules.values())
  78. _cached_filenames = clean_files(_cached_filenames)
  79. if _cached_modules == module_values:
  80. # No changes in module list, short-circuit the function
  81. if only_new:
  82. return []
  83. else:
  84. return _cached_filenames
  85. new_modules = module_values - _cached_modules
  86. new_filenames = clean_files(
  87. [filename.__file__ for filename in new_modules
  88. if hasattr(filename, '__file__')])
  89. if not _cached_filenames and settings.USE_I18N:
  90. # Add the names of the .mo files that can be generated
  91. # by compilemessages management command to the list of files watched.
  92. basedirs = [os.path.join(os.path.dirname(os.path.dirname(__file__)),
  93. 'conf', 'locale'),
  94. 'locale']
  95. for app_config in reversed(list(apps.get_app_configs())):
  96. basedirs.append(os.path.join(app_config.path, 'locale'))
  97. basedirs.extend(settings.LOCALE_PATHS)
  98. basedirs = [os.path.abspath(basedir) for basedir in basedirs
  99. if os.path.isdir(basedir)]
  100. for basedir in basedirs:
  101. for dirpath, dirnames, locale_filenames in os.walk(basedir):
  102. for filename in locale_filenames:
  103. if filename.endswith('.mo'):
  104. new_filenames.append(os.path.join(dirpath, filename))
  105. _cached_modules = _cached_modules.union(new_modules)
  106. _cached_filenames += new_filenames
  107. if only_new:
  108. return new_filenames
  109. else:
  110. return _cached_filenames + clean_files(_error_files)
  111. def clean_files(filelist):
  112. filenames = []
  113. for filename in filelist:
  114. if not filename:
  115. continue
  116. if filename.endswith(".pyc") or filename.endswith(".pyo"):
  117. filename = filename[:-1]
  118. if filename.endswith("$py.class"):
  119. filename = filename[:-9] + ".py"
  120. if os.path.exists(filename):
  121. filenames.append(filename)
  122. return filenames
  123. def reset_translations():
  124. import gettext
  125. from django.utils.translation import trans_real
  126. gettext._translations = {}
  127. trans_real._translations = {}
  128. trans_real._default = None
  129. trans_real._active = threading.local()
  130. def inotify_code_changed():
  131. """
  132. Checks for changed code using inotify. After being called
  133. it blocks until a change event has been fired.
  134. """
  135. class EventHandler(pyinotify.ProcessEvent):
  136. modified_code = None
  137. def process_default(self, event):
  138. if event.path.endswith('.mo'):
  139. EventHandler.modified_code = I18N_MODIFIED
  140. else:
  141. EventHandler.modified_code = FILE_MODIFIED
  142. wm = pyinotify.WatchManager()
  143. notifier = pyinotify.Notifier(wm, EventHandler())
  144. def update_watch(sender=None, **kwargs):
  145. if sender and getattr(sender, 'handles_files', False):
  146. # No need to update watches when request serves files.
  147. # (sender is supposed to be a django.core.handlers.BaseHandler subclass)
  148. return
  149. mask = (
  150. pyinotify.IN_MODIFY |
  151. pyinotify.IN_DELETE |
  152. pyinotify.IN_ATTRIB |
  153. pyinotify.IN_MOVED_FROM |
  154. pyinotify.IN_MOVED_TO |
  155. pyinotify.IN_CREATE
  156. )
  157. for path in gen_filenames(only_new=True):
  158. wm.add_watch(path, mask)
  159. # New modules may get imported when a request is processed.
  160. request_finished.connect(update_watch)
  161. # Block until an event happens.
  162. update_watch()
  163. notifier.check_events(timeout=None)
  164. notifier.read_events()
  165. notifier.process_events()
  166. notifier.stop()
  167. # If we are here the code must have changed.
  168. return EventHandler.modified_code
  169. def code_changed():
  170. global _mtimes, _win
  171. for filename in gen_filenames():
  172. stat = os.stat(filename)
  173. mtime = stat.st_mtime
  174. if _win:
  175. mtime -= stat.st_ctime
  176. if filename not in _mtimes:
  177. _mtimes[filename] = mtime
  178. continue
  179. if mtime != _mtimes[filename]:
  180. _mtimes = {}
  181. try:
  182. del _error_files[_error_files.index(filename)]
  183. except ValueError:
  184. pass
  185. return I18N_MODIFIED if filename.endswith('.mo') else FILE_MODIFIED
  186. return False
  187. def check_errors(fn):
  188. def wrapper(*args, **kwargs):
  189. try:
  190. fn(*args, **kwargs)
  191. except (ImportError, IndentationError, NameError, SyntaxError,
  192. TypeError, AttributeError):
  193. et, ev, tb = sys.exc_info()
  194. if getattr(ev, 'filename', None) is None:
  195. # get the filename from the last item in the stack
  196. filename = traceback.extract_tb(tb)[-1][0]
  197. else:
  198. filename = ev.filename
  199. if filename not in _error_files:
  200. _error_files.append(filename)
  201. raise
  202. return wrapper
  203. def ensure_echo_on():
  204. if termios:
  205. fd = sys.stdin
  206. if fd.isatty():
  207. attr_list = termios.tcgetattr(fd)
  208. if not attr_list[3] & termios.ECHO:
  209. attr_list[3] |= termios.ECHO
  210. if hasattr(signal, 'SIGTTOU'):
  211. old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
  212. else:
  213. old_handler = None
  214. termios.tcsetattr(fd, termios.TCSANOW, attr_list)
  215. if old_handler is not None:
  216. signal.signal(signal.SIGTTOU, old_handler)
  217. def reloader_thread():
  218. ensure_echo_on()
  219. if USE_INOTIFY:
  220. fn = inotify_code_changed
  221. else:
  222. fn = code_changed
  223. while RUN_RELOADER:
  224. change = fn()
  225. if change == FILE_MODIFIED:
  226. sys.exit(3) # force reload
  227. elif change == I18N_MODIFIED:
  228. reset_translations()
  229. time.sleep(1)
  230. def restart_with_reloader():
  231. while True:
  232. args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] + sys.argv
  233. if sys.platform == "win32":
  234. args = ['"%s"' % arg for arg in args]
  235. new_environ = os.environ.copy()
  236. new_environ["RUN_MAIN"] = 'true'
  237. exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)
  238. if exit_code != 3:
  239. return exit_code
  240. def python_reloader(main_func, args, kwargs):
  241. if os.environ.get("RUN_MAIN") == "true":
  242. thread.start_new_thread(main_func, args, kwargs)
  243. try:
  244. reloader_thread()
  245. except KeyboardInterrupt:
  246. pass
  247. else:
  248. try:
  249. exit_code = restart_with_reloader()
  250. if exit_code < 0:
  251. os.kill(os.getpid(), -exit_code)
  252. else:
  253. sys.exit(exit_code)
  254. except KeyboardInterrupt:
  255. pass
  256. def jython_reloader(main_func, args, kwargs):
  257. from _systemrestart import SystemRestart
  258. thread.start_new_thread(main_func, args)
  259. while True:
  260. if code_changed():
  261. raise SystemRestart
  262. time.sleep(1)
  263. def main(main_func, args=None, kwargs=None):
  264. if args is None:
  265. args = ()
  266. if kwargs is None:
  267. kwargs = {}
  268. if sys.platform.startswith('java'):
  269. reloader = jython_reloader
  270. else:
  271. reloader = python_reloader
  272. wrapped_main_func = check_errors(main_func)
  273. reloader(wrapped_main_func, args, kwargs)