Development of an internal social media platform with personalised dashboards for students
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.

autoreload.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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 subprocess
  33. import sys
  34. import time
  35. import traceback
  36. import _thread
  37. from django.apps import apps
  38. from django.conf import settings
  39. from django.core.signals import request_finished
  40. # This import does nothing, but it's necessary to avoid some race conditions
  41. # in the threading module. See http://code.djangoproject.com/ticket/2330 .
  42. try:
  43. import threading # NOQA
  44. except ImportError:
  45. pass
  46. try:
  47. import termios
  48. except ImportError:
  49. termios = None
  50. USE_INOTIFY = False
  51. try:
  52. # Test whether inotify is enabled and likely to work
  53. import pyinotify
  54. fd = pyinotify.INotifyWrapper.create().inotify_init()
  55. if fd >= 0:
  56. USE_INOTIFY = True
  57. os.close(fd)
  58. except ImportError:
  59. pass
  60. RUN_RELOADER = True
  61. FILE_MODIFIED = 1
  62. I18N_MODIFIED = 2
  63. _mtimes = {}
  64. _win = (sys.platform == "win32")
  65. _exception = None
  66. _error_files = []
  67. _cached_modules = set()
  68. _cached_filenames = []
  69. def gen_filenames(only_new=False):
  70. """
  71. Return a list of filenames referenced in sys.modules and translation files.
  72. """
  73. # N.B. ``list(...)`` is needed, because this runs in parallel with
  74. # application code which might be mutating ``sys.modules``, and this will
  75. # fail with RuntimeError: cannot mutate dictionary while iterating
  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 + clean_files(_error_files)
  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 + clean_files(_error_files)
  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. Check 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. pyinotify.IN_DELETE_SELF |
  157. pyinotify.IN_MOVE_SELF
  158. )
  159. for path in gen_filenames(only_new=True):
  160. wm.add_watch(path, mask)
  161. # New modules may get imported when a request is processed.
  162. request_finished.connect(update_watch)
  163. # Block until an event happens.
  164. update_watch()
  165. notifier.check_events(timeout=None)
  166. notifier.read_events()
  167. notifier.process_events()
  168. notifier.stop()
  169. # If we are here the code must have changed.
  170. return EventHandler.modified_code
  171. def code_changed():
  172. global _mtimes, _win
  173. for filename in gen_filenames():
  174. stat = os.stat(filename)
  175. mtime = stat.st_mtime
  176. if _win:
  177. mtime -= stat.st_ctime
  178. if filename not in _mtimes:
  179. _mtimes[filename] = mtime
  180. continue
  181. if mtime != _mtimes[filename]:
  182. _mtimes = {}
  183. try:
  184. del _error_files[_error_files.index(filename)]
  185. except ValueError:
  186. pass
  187. return I18N_MODIFIED if filename.endswith('.mo') else FILE_MODIFIED
  188. return False
  189. def check_errors(fn):
  190. def wrapper(*args, **kwargs):
  191. global _exception
  192. try:
  193. fn(*args, **kwargs)
  194. except Exception:
  195. _exception = sys.exc_info()
  196. et, ev, tb = _exception
  197. if getattr(ev, 'filename', None) is None:
  198. # get the filename from the last item in the stack
  199. filename = traceback.extract_tb(tb)[-1][0]
  200. else:
  201. filename = ev.filename
  202. if filename not in _error_files:
  203. _error_files.append(filename)
  204. raise
  205. return wrapper
  206. def raise_last_exception():
  207. global _exception
  208. if _exception is not None:
  209. raise _exception[1]
  210. def ensure_echo_on():
  211. if termios:
  212. fd = sys.stdin
  213. if fd.isatty():
  214. attr_list = termios.tcgetattr(fd)
  215. if not attr_list[3] & termios.ECHO:
  216. attr_list[3] |= termios.ECHO
  217. if hasattr(signal, 'SIGTTOU'):
  218. old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
  219. else:
  220. old_handler = None
  221. termios.tcsetattr(fd, termios.TCSANOW, attr_list)
  222. if old_handler is not None:
  223. signal.signal(signal.SIGTTOU, old_handler)
  224. def reloader_thread():
  225. ensure_echo_on()
  226. if USE_INOTIFY:
  227. fn = inotify_code_changed
  228. else:
  229. fn = code_changed
  230. while RUN_RELOADER:
  231. change = fn()
  232. if change == FILE_MODIFIED:
  233. sys.exit(3) # force reload
  234. elif change == I18N_MODIFIED:
  235. reset_translations()
  236. time.sleep(1)
  237. def restart_with_reloader():
  238. import django.__main__
  239. while True:
  240. args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions]
  241. if sys.argv[0] == django.__main__.__file__:
  242. # The server was started with `python -m django runserver`.
  243. args += ['-m', 'django']
  244. args += sys.argv[1:]
  245. else:
  246. args += sys.argv
  247. new_environ = {**os.environ, 'RUN_MAIN': 'true'}
  248. exit_code = subprocess.call(args, env=new_environ)
  249. if exit_code != 3:
  250. return exit_code
  251. def python_reloader(main_func, args, kwargs):
  252. if os.environ.get("RUN_MAIN") == "true":
  253. _thread.start_new_thread(main_func, args, kwargs)
  254. try:
  255. reloader_thread()
  256. except KeyboardInterrupt:
  257. pass
  258. else:
  259. try:
  260. exit_code = restart_with_reloader()
  261. if exit_code < 0:
  262. os.kill(os.getpid(), -exit_code)
  263. else:
  264. sys.exit(exit_code)
  265. except KeyboardInterrupt:
  266. pass
  267. def main(main_func, args=None, kwargs=None):
  268. if args is None:
  269. args = ()
  270. if kwargs is None:
  271. kwargs = {}
  272. wrapped_main_func = check_errors(main_func)
  273. python_reloader(wrapped_main_func, args, kwargs)