You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

334 lines
11 KiB

  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. import os
  31. import signal
  32. import sys
  33. import time
  34. import traceback
  35. from django.apps import apps
  36. from django.conf import settings
  37. from django.core.signals import request_finished
  38. from django.utils import six
  39. from django.utils._os import npath
  40. from django.utils.six.moves import _thread as thread
  41. # This import does nothing, but it's necessary to avoid some race conditions
  42. # in the threading module. See http://code.djangoproject.com/ticket/2330 .
  43. try:
  44. import threading # NOQA
  45. except ImportError:
  46. pass
  47. try:
  48. import termios
  49. except ImportError:
  50. termios = None
  51. USE_INOTIFY = False
  52. try:
  53. # Test whether inotify is enabled and likely to work
  54. import pyinotify
  55. fd = pyinotify.INotifyWrapper.create().inotify_init()
  56. if fd >= 0:
  57. USE_INOTIFY = True
  58. os.close(fd)
  59. except ImportError:
  60. pass
  61. RUN_RELOADER = True
  62. FILE_MODIFIED = 1
  63. I18N_MODIFIED = 2
  64. _mtimes = {}
  65. _win = (sys.platform == "win32")
  66. _exception = None
  67. _error_files = []
  68. _cached_modules = set()
  69. _cached_filenames = []
  70. def gen_filenames(only_new=False):
  71. """
  72. Returns a list of filenames referenced in sys.modules and translation
  73. files.
  74. """
  75. # N.B. ``list(...)`` is needed, because this runs in parallel with
  76. # application code which might be mutating ``sys.modules``, and this will
  77. # fail with RuntimeError: cannot mutate dictionary while iterating
  78. global _cached_modules, _cached_filenames
  79. module_values = set(sys.modules.values())
  80. _cached_filenames = clean_files(_cached_filenames)
  81. if _cached_modules == module_values:
  82. # No changes in module list, short-circuit the function
  83. if only_new:
  84. return []
  85. else:
  86. return _cached_filenames + clean_files(_error_files)
  87. new_modules = module_values - _cached_modules
  88. new_filenames = clean_files(
  89. [filename.__file__ for filename in new_modules
  90. if hasattr(filename, '__file__')])
  91. if not _cached_filenames and settings.USE_I18N:
  92. # Add the names of the .mo files that can be generated
  93. # by compilemessages management command to the list of files watched.
  94. basedirs = [os.path.join(os.path.dirname(os.path.dirname(__file__)),
  95. 'conf', 'locale'),
  96. 'locale']
  97. for app_config in reversed(list(apps.get_app_configs())):
  98. basedirs.append(os.path.join(npath(app_config.path), 'locale'))
  99. basedirs.extend(settings.LOCALE_PATHS)
  100. basedirs = [os.path.abspath(basedir) for basedir in basedirs
  101. if os.path.isdir(basedir)]
  102. for basedir in basedirs:
  103. for dirpath, dirnames, locale_filenames in os.walk(basedir):
  104. for filename in locale_filenames:
  105. if filename.endswith('.mo'):
  106. new_filenames.append(os.path.join(dirpath, filename))
  107. _cached_modules = _cached_modules.union(new_modules)
  108. _cached_filenames += new_filenames
  109. if only_new:
  110. return new_filenames + clean_files(_error_files)
  111. else:
  112. return _cached_filenames + clean_files(_error_files)
  113. def clean_files(filelist):
  114. filenames = []
  115. for filename in filelist:
  116. if not filename:
  117. continue
  118. if filename.endswith(".pyc") or filename.endswith(".pyo"):
  119. filename = filename[:-1]
  120. if filename.endswith("$py.class"):
  121. filename = filename[:-9] + ".py"
  122. if os.path.exists(filename):
  123. filenames.append(filename)
  124. return filenames
  125. def reset_translations():
  126. import gettext
  127. from django.utils.translation import trans_real
  128. gettext._translations = {}
  129. trans_real._translations = {}
  130. trans_real._default = None
  131. trans_real._active = threading.local()
  132. def inotify_code_changed():
  133. """
  134. Checks for changed code using inotify. After being called
  135. it blocks until a change event has been fired.
  136. """
  137. class EventHandler(pyinotify.ProcessEvent):
  138. modified_code = None
  139. def process_default(self, event):
  140. if event.path.endswith('.mo'):
  141. EventHandler.modified_code = I18N_MODIFIED
  142. else:
  143. EventHandler.modified_code = FILE_MODIFIED
  144. wm = pyinotify.WatchManager()
  145. notifier = pyinotify.Notifier(wm, EventHandler())
  146. def update_watch(sender=None, **kwargs):
  147. if sender and getattr(sender, 'handles_files', False):
  148. # No need to update watches when request serves files.
  149. # (sender is supposed to be a django.core.handlers.BaseHandler subclass)
  150. return
  151. mask = (
  152. pyinotify.IN_MODIFY |
  153. pyinotify.IN_DELETE |
  154. pyinotify.IN_ATTRIB |
  155. pyinotify.IN_MOVED_FROM |
  156. pyinotify.IN_MOVED_TO |
  157. pyinotify.IN_CREATE |
  158. pyinotify.IN_DELETE_SELF |
  159. pyinotify.IN_MOVE_SELF
  160. )
  161. for path in gen_filenames(only_new=True):
  162. wm.add_watch(path, mask)
  163. # New modules may get imported when a request is processed.
  164. request_finished.connect(update_watch)
  165. # Block until an event happens.
  166. update_watch()
  167. notifier.check_events(timeout=None)
  168. notifier.read_events()
  169. notifier.process_events()
  170. notifier.stop()
  171. # If we are here the code must have changed.
  172. return EventHandler.modified_code
  173. def code_changed():
  174. global _mtimes, _win
  175. for filename in gen_filenames():
  176. stat = os.stat(filename)
  177. mtime = stat.st_mtime
  178. if _win:
  179. mtime -= stat.st_ctime
  180. if filename not in _mtimes:
  181. _mtimes[filename] = mtime
  182. continue
  183. if mtime != _mtimes[filename]:
  184. _mtimes = {}
  185. try:
  186. del _error_files[_error_files.index(filename)]
  187. except ValueError:
  188. pass
  189. return I18N_MODIFIED if filename.endswith('.mo') else FILE_MODIFIED
  190. return False
  191. def check_errors(fn):
  192. def wrapper(*args, **kwargs):
  193. global _exception
  194. try:
  195. fn(*args, **kwargs)
  196. except Exception:
  197. _exception = sys.exc_info()
  198. et, ev, tb = _exception
  199. if getattr(ev, 'filename', None) is None:
  200. # get the filename from the last item in the stack
  201. filename = traceback.extract_tb(tb)[-1][0]
  202. else:
  203. filename = ev.filename
  204. if filename not in _error_files:
  205. _error_files.append(filename)
  206. raise
  207. return wrapper
  208. def raise_last_exception():
  209. global _exception
  210. if _exception is not None:
  211. six.reraise(*_exception)
  212. def ensure_echo_on():
  213. if termios:
  214. fd = sys.stdin
  215. if fd.isatty():
  216. attr_list = termios.tcgetattr(fd)
  217. if not attr_list[3] & termios.ECHO:
  218. attr_list[3] |= termios.ECHO
  219. if hasattr(signal, 'SIGTTOU'):
  220. old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
  221. else:
  222. old_handler = None
  223. termios.tcsetattr(fd, termios.TCSANOW, attr_list)
  224. if old_handler is not None:
  225. signal.signal(signal.SIGTTOU, old_handler)
  226. def reloader_thread():
  227. ensure_echo_on()
  228. if USE_INOTIFY:
  229. fn = inotify_code_changed
  230. else:
  231. fn = code_changed
  232. while RUN_RELOADER:
  233. change = fn()
  234. if change == FILE_MODIFIED:
  235. sys.exit(3) # force reload
  236. elif change == I18N_MODIFIED:
  237. reset_translations()
  238. time.sleep(1)
  239. def restart_with_reloader():
  240. while True:
  241. args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] + sys.argv
  242. if sys.platform == "win32":
  243. args = ['"%s"' % arg for arg in args]
  244. new_environ = os.environ.copy()
  245. new_environ["RUN_MAIN"] = 'true'
  246. exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)
  247. if exit_code != 3:
  248. return exit_code
  249. def python_reloader(main_func, args, kwargs):
  250. if os.environ.get("RUN_MAIN") == "true":
  251. thread.start_new_thread(main_func, args, kwargs)
  252. try:
  253. reloader_thread()
  254. except KeyboardInterrupt:
  255. pass
  256. else:
  257. try:
  258. exit_code = restart_with_reloader()
  259. if exit_code < 0:
  260. os.kill(os.getpid(), -exit_code)
  261. else:
  262. sys.exit(exit_code)
  263. except KeyboardInterrupt:
  264. pass
  265. def jython_reloader(main_func, args, kwargs):
  266. from _systemrestart import SystemRestart
  267. thread.start_new_thread(main_func, args)
  268. while True:
  269. if code_changed():
  270. raise SystemRestart
  271. time.sleep(1)
  272. def main(main_func, args=None, kwargs=None):
  273. if args is None:
  274. args = ()
  275. if kwargs is None:
  276. kwargs = {}
  277. if sys.platform.startswith('java'):
  278. reloader = jython_reloader
  279. else:
  280. reloader = python_reloader
  281. wrapped_main_func = check_errors(main_func)
  282. reloader(wrapped_main_func, args, kwargs)