Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

336 wiersze
11KB

  1. import os
  2. import subprocess
  3. import sys
  4. import threading
  5. import time
  6. from itertools import chain
  7. from ._compat import iteritems
  8. from ._compat import PY2
  9. from ._compat import text_type
  10. from ._internal import _log
  11. def _iter_module_files():
  12. """This iterates over all relevant Python files. It goes through all
  13. loaded files from modules, all files in folders of already loaded modules
  14. as well as all files reachable through a package.
  15. """
  16. # The list call is necessary on Python 3 in case the module
  17. # dictionary modifies during iteration.
  18. for module in list(sys.modules.values()):
  19. if module is None:
  20. continue
  21. filename = getattr(module, "__file__", None)
  22. if filename:
  23. if os.path.isdir(filename) and os.path.exists(
  24. os.path.join(filename, "__init__.py")
  25. ):
  26. filename = os.path.join(filename, "__init__.py")
  27. old = None
  28. while not os.path.isfile(filename):
  29. old = filename
  30. filename = os.path.dirname(filename)
  31. if filename == old:
  32. break
  33. else:
  34. if filename[-4:] in (".pyc", ".pyo"):
  35. filename = filename[:-1]
  36. yield filename
  37. def _find_observable_paths(extra_files=None):
  38. """Finds all paths that should be observed."""
  39. rv = set(
  40. os.path.dirname(os.path.abspath(x)) if os.path.isfile(x) else os.path.abspath(x)
  41. for x in sys.path
  42. )
  43. for filename in extra_files or ():
  44. rv.add(os.path.dirname(os.path.abspath(filename)))
  45. for module in list(sys.modules.values()):
  46. fn = getattr(module, "__file__", None)
  47. if fn is None:
  48. continue
  49. fn = os.path.abspath(fn)
  50. rv.add(os.path.dirname(fn))
  51. return _find_common_roots(rv)
  52. def _get_args_for_reloading():
  53. """Determine how the script was executed, and return the args needed
  54. to execute it again in a new process.
  55. """
  56. rv = [sys.executable]
  57. py_script = sys.argv[0]
  58. args = sys.argv[1:]
  59. # Need to look at main module to determine how it was executed.
  60. __main__ = sys.modules["__main__"]
  61. # The value of __package__ indicates how Python was called. It may
  62. # not exist if a setuptools script is installed as an egg.
  63. if getattr(__main__, "__package__", None) is None:
  64. # Executed a file, like "python app.py".
  65. py_script = os.path.abspath(py_script)
  66. if os.name == "nt":
  67. # Windows entry points have ".exe" extension and should be
  68. # called directly.
  69. if not os.path.exists(py_script) and os.path.exists(py_script + ".exe"):
  70. py_script += ".exe"
  71. if (
  72. os.path.splitext(rv[0])[1] == ".exe"
  73. and os.path.splitext(py_script)[1] == ".exe"
  74. ):
  75. rv.pop(0)
  76. rv.append(py_script)
  77. else:
  78. # Executed a module, like "python -m werkzeug.serving".
  79. if sys.argv[0] == "-m":
  80. # Flask works around previous behavior by putting
  81. # "-m flask" in sys.argv.
  82. # TODO remove this once Flask no longer misbehaves
  83. args = sys.argv
  84. else:
  85. if os.path.isfile(py_script):
  86. # Rewritten by Python from "-m script" to "/path/to/script.py".
  87. py_module = __main__.__package__
  88. name = os.path.splitext(os.path.basename(py_script))[0]
  89. if name != "__main__":
  90. py_module += "." + name
  91. else:
  92. # Incorrectly rewritten by pydevd debugger from "-m script" to "script".
  93. py_module = py_script
  94. rv.extend(("-m", py_module.lstrip(".")))
  95. rv.extend(args)
  96. return rv
  97. def _find_common_roots(paths):
  98. """Out of some paths it finds the common roots that need monitoring."""
  99. paths = [x.split(os.path.sep) for x in paths]
  100. root = {}
  101. for chunks in sorted(paths, key=len, reverse=True):
  102. node = root
  103. for chunk in chunks:
  104. node = node.setdefault(chunk, {})
  105. node.clear()
  106. rv = set()
  107. def _walk(node, path):
  108. for prefix, child in iteritems(node):
  109. _walk(child, path + (prefix,))
  110. if not node:
  111. rv.add("/".join(path))
  112. _walk(root, ())
  113. return rv
  114. class ReloaderLoop(object):
  115. name = None
  116. # monkeypatched by testsuite. wrapping with `staticmethod` is required in
  117. # case time.sleep has been replaced by a non-c function (e.g. by
  118. # `eventlet.monkey_patch`) before we get here
  119. _sleep = staticmethod(time.sleep)
  120. def __init__(self, extra_files=None, interval=1):
  121. self.extra_files = set(os.path.abspath(x) for x in extra_files or ())
  122. self.interval = interval
  123. def run(self):
  124. pass
  125. def restart_with_reloader(self):
  126. """Spawn a new Python interpreter with the same arguments as this one,
  127. but running the reloader thread.
  128. """
  129. while 1:
  130. _log("info", " * Restarting with %s" % self.name)
  131. args = _get_args_for_reloading()
  132. # a weird bug on windows. sometimes unicode strings end up in the
  133. # environment and subprocess.call does not like this, encode them
  134. # to latin1 and continue.
  135. if os.name == "nt" and PY2:
  136. new_environ = {}
  137. for key, value in iteritems(os.environ):
  138. if isinstance(key, text_type):
  139. key = key.encode("iso-8859-1")
  140. if isinstance(value, text_type):
  141. value = value.encode("iso-8859-1")
  142. new_environ[key] = value
  143. else:
  144. new_environ = os.environ.copy()
  145. new_environ["WERKZEUG_RUN_MAIN"] = "true"
  146. exit_code = subprocess.call(args, env=new_environ, close_fds=False)
  147. if exit_code != 3:
  148. return exit_code
  149. def trigger_reload(self, filename):
  150. self.log_reload(filename)
  151. sys.exit(3)
  152. def log_reload(self, filename):
  153. filename = os.path.abspath(filename)
  154. _log("info", " * Detected change in %r, reloading" % filename)
  155. class StatReloaderLoop(ReloaderLoop):
  156. name = "stat"
  157. def run(self):
  158. mtimes = {}
  159. while 1:
  160. for filename in chain(_iter_module_files(), self.extra_files):
  161. try:
  162. mtime = os.stat(filename).st_mtime
  163. except OSError:
  164. continue
  165. old_time = mtimes.get(filename)
  166. if old_time is None:
  167. mtimes[filename] = mtime
  168. continue
  169. elif mtime > old_time:
  170. self.trigger_reload(filename)
  171. self._sleep(self.interval)
  172. class WatchdogReloaderLoop(ReloaderLoop):
  173. def __init__(self, *args, **kwargs):
  174. ReloaderLoop.__init__(self, *args, **kwargs)
  175. from watchdog.observers import Observer
  176. from watchdog.events import FileSystemEventHandler
  177. self.observable_paths = set()
  178. def _check_modification(filename):
  179. if filename in self.extra_files:
  180. self.trigger_reload(filename)
  181. dirname = os.path.dirname(filename)
  182. if dirname.startswith(tuple(self.observable_paths)):
  183. if filename.endswith((".pyc", ".pyo", ".py")):
  184. self.trigger_reload(filename)
  185. class _CustomHandler(FileSystemEventHandler):
  186. def on_created(self, event):
  187. _check_modification(event.src_path)
  188. def on_modified(self, event):
  189. _check_modification(event.src_path)
  190. def on_moved(self, event):
  191. _check_modification(event.src_path)
  192. _check_modification(event.dest_path)
  193. def on_deleted(self, event):
  194. _check_modification(event.src_path)
  195. reloader_name = Observer.__name__.lower()
  196. if reloader_name.endswith("observer"):
  197. reloader_name = reloader_name[:-8]
  198. reloader_name += " reloader"
  199. self.name = reloader_name
  200. self.observer_class = Observer
  201. self.event_handler = _CustomHandler()
  202. self.should_reload = False
  203. def trigger_reload(self, filename):
  204. # This is called inside an event handler, which means throwing
  205. # SystemExit has no effect.
  206. # https://github.com/gorakhargosh/watchdog/issues/294
  207. self.should_reload = True
  208. self.log_reload(filename)
  209. def run(self):
  210. watches = {}
  211. observer = self.observer_class()
  212. observer.start()
  213. try:
  214. while not self.should_reload:
  215. to_delete = set(watches)
  216. paths = _find_observable_paths(self.extra_files)
  217. for path in paths:
  218. if path not in watches:
  219. try:
  220. watches[path] = observer.schedule(
  221. self.event_handler, path, recursive=True
  222. )
  223. except OSError:
  224. # Clear this path from list of watches We don't want
  225. # the same error message showing again in the next
  226. # iteration.
  227. watches[path] = None
  228. to_delete.discard(path)
  229. for path in to_delete:
  230. watch = watches.pop(path, None)
  231. if watch is not None:
  232. observer.unschedule(watch)
  233. self.observable_paths = paths
  234. self._sleep(self.interval)
  235. finally:
  236. observer.stop()
  237. observer.join()
  238. sys.exit(3)
  239. reloader_loops = {"stat": StatReloaderLoop, "watchdog": WatchdogReloaderLoop}
  240. try:
  241. __import__("watchdog.observers")
  242. except ImportError:
  243. reloader_loops["auto"] = reloader_loops["stat"]
  244. else:
  245. reloader_loops["auto"] = reloader_loops["watchdog"]
  246. def ensure_echo_on():
  247. """Ensure that echo mode is enabled. Some tools such as PDB disable
  248. it which causes usability issues after reload."""
  249. # tcgetattr will fail if stdin isn't a tty
  250. if not sys.stdin.isatty():
  251. return
  252. try:
  253. import termios
  254. except ImportError:
  255. return
  256. attributes = termios.tcgetattr(sys.stdin)
  257. if not attributes[3] & termios.ECHO:
  258. attributes[3] |= termios.ECHO
  259. termios.tcsetattr(sys.stdin, termios.TCSANOW, attributes)
  260. def run_with_reloader(main_func, extra_files=None, interval=1, reloader_type="auto"):
  261. """Run the given function in an independent python interpreter."""
  262. import signal
  263. reloader = reloader_loops[reloader_type](extra_files, interval)
  264. signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
  265. try:
  266. if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
  267. ensure_echo_on()
  268. t = threading.Thread(target=main_func, args=())
  269. t.setDaemon(True)
  270. t.start()
  271. reloader.run()
  272. else:
  273. sys.exit(reloader.restart_with_reloader())
  274. except KeyboardInterrupt:
  275. pass