autoreload.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. #
  2. # Copyright 2009 Facebook
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. """Automatically restart the server when a source file is modified.
  16. Most applications should not access this module directly. Instead,
  17. pass the keyword argument ``autoreload=True`` to the
  18. `tornado.web.Application` constructor (or ``debug=True``, which
  19. enables this setting and several others). This will enable autoreload
  20. mode as well as checking for changes to templates and static
  21. resources. Note that restarting is a destructive operation and any
  22. requests in progress will be aborted when the process restarts. (If
  23. you want to disable autoreload while using other debug-mode features,
  24. pass both ``debug=True`` and ``autoreload=False``).
  25. This module can also be used as a command-line wrapper around scripts
  26. such as unit test runners. See the `main` method for details.
  27. The command-line wrapper and Application debug modes can be used together.
  28. This combination is encouraged as the wrapper catches syntax errors and
  29. other import-time failures, while debug mode catches changes once
  30. the server has started.
  31. This module will not work correctly when `.HTTPServer`'s multi-process
  32. mode is used.
  33. Reloading loses any Python interpreter command-line arguments (e.g. ``-u``)
  34. because it re-executes Python using ``sys.executable`` and ``sys.argv``.
  35. Additionally, modifying these variables will cause reloading to behave
  36. incorrectly.
  37. """
  38. import os
  39. import sys
  40. # sys.path handling
  41. # -----------------
  42. #
  43. # If a module is run with "python -m", the current directory (i.e. "")
  44. # is automatically prepended to sys.path, but not if it is run as
  45. # "path/to/file.py". The processing for "-m" rewrites the former to
  46. # the latter, so subsequent executions won't have the same path as the
  47. # original.
  48. #
  49. # Conversely, when run as path/to/file.py, the directory containing
  50. # file.py gets added to the path, which can cause confusion as imports
  51. # may become relative in spite of the future import.
  52. #
  53. # We address the former problem by reconstructing the original command
  54. # line before re-execution so the new process will
  55. # see the correct path. We attempt to address the latter problem when
  56. # tornado.autoreload is run as __main__.
  57. if __name__ == "__main__":
  58. # This sys.path manipulation must come before our imports (as much
  59. # as possible - if we introduced a tornado.sys or tornado.os
  60. # module we'd be in trouble), or else our imports would become
  61. # relative again despite the future import.
  62. #
  63. # There is a separate __main__ block at the end of the file to call main().
  64. if sys.path[0] == os.path.dirname(__file__):
  65. del sys.path[0]
  66. import functools
  67. import importlib.abc
  68. import os
  69. import pkgutil
  70. import sys
  71. import traceback
  72. import types
  73. import subprocess
  74. import weakref
  75. from tornado import ioloop
  76. from tornado.log import gen_log
  77. from tornado import process
  78. try:
  79. import signal
  80. except ImportError:
  81. signal = None # type: ignore
  82. from typing import Callable, Dict, Optional, List, Union
  83. # os.execv is broken on Windows and can't properly parse command line
  84. # arguments and executable name if they contain whitespaces. subprocess
  85. # fixes that behavior.
  86. _has_execv = sys.platform != "win32"
  87. _watched_files = set()
  88. _reload_hooks = []
  89. _reload_attempted = False
  90. _io_loops: "weakref.WeakKeyDictionary[ioloop.IOLoop, bool]" = (
  91. weakref.WeakKeyDictionary()
  92. )
  93. _autoreload_is_main = False
  94. _original_argv: Optional[List[str]] = None
  95. _original_spec = None
  96. def start(check_time: int = 500) -> None:
  97. """Begins watching source files for changes.
  98. .. versionchanged:: 5.0
  99. The ``io_loop`` argument (deprecated since version 4.1) has been removed.
  100. """
  101. io_loop = ioloop.IOLoop.current()
  102. if io_loop in _io_loops:
  103. return
  104. _io_loops[io_loop] = True
  105. if len(_io_loops) > 1:
  106. gen_log.warning("tornado.autoreload started more than once in the same process")
  107. modify_times: Dict[str, float] = {}
  108. callback = functools.partial(_reload_on_update, modify_times)
  109. scheduler = ioloop.PeriodicCallback(callback, check_time)
  110. scheduler.start()
  111. def wait() -> None:
  112. """Wait for a watched file to change, then restart the process.
  113. Intended to be used at the end of scripts like unit test runners,
  114. to run the tests again after any source file changes (but see also
  115. the command-line interface in `main`)
  116. """
  117. io_loop = ioloop.IOLoop()
  118. io_loop.add_callback(start)
  119. io_loop.start()
  120. def watch(filename: str) -> None:
  121. """Add a file to the watch list.
  122. All imported modules are watched by default.
  123. """
  124. _watched_files.add(filename)
  125. def add_reload_hook(fn: Callable[[], None]) -> None:
  126. """Add a function to be called before reloading the process.
  127. Note that for open file and socket handles it is generally
  128. preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or
  129. `os.set_inheritable`) instead of using a reload hook to close them.
  130. """
  131. _reload_hooks.append(fn)
  132. def _reload_on_update(modify_times: Dict[str, float]) -> None:
  133. if _reload_attempted:
  134. # We already tried to reload and it didn't work, so don't try again.
  135. return
  136. if process.task_id() is not None:
  137. # We're in a child process created by fork_processes. If child
  138. # processes restarted themselves, they'd all restart and then
  139. # all call fork_processes again.
  140. return
  141. for module in list(sys.modules.values()):
  142. # Some modules play games with sys.modules (e.g. email/__init__.py
  143. # in the standard library), and occasionally this can cause strange
  144. # failures in getattr. Just ignore anything that's not an ordinary
  145. # module.
  146. if not isinstance(module, types.ModuleType):
  147. continue
  148. path = getattr(module, "__file__", None)
  149. if not path:
  150. continue
  151. if path.endswith(".pyc") or path.endswith(".pyo"):
  152. path = path[:-1]
  153. _check_file(modify_times, path)
  154. for path in _watched_files:
  155. _check_file(modify_times, path)
  156. def _check_file(modify_times: Dict[str, float], path: str) -> None:
  157. try:
  158. modified = os.stat(path).st_mtime
  159. except Exception:
  160. return
  161. if path not in modify_times:
  162. modify_times[path] = modified
  163. return
  164. if modify_times[path] != modified:
  165. gen_log.info("%s modified; restarting server", path)
  166. _reload()
  167. def _reload() -> None:
  168. global _reload_attempted
  169. _reload_attempted = True
  170. for fn in _reload_hooks:
  171. fn()
  172. if sys.platform != "win32":
  173. # Clear the alarm signal set by
  174. # ioloop.set_blocking_log_threshold so it doesn't fire
  175. # after the exec.
  176. signal.setitimer(signal.ITIMER_REAL, 0, 0)
  177. # sys.path fixes: see comments at top of file. If __main__.__spec__
  178. # exists, we were invoked with -m and the effective path is about to
  179. # change on re-exec. Reconstruct the original command line to
  180. # ensure that the new process sees the same path we did.
  181. if _autoreload_is_main:
  182. assert _original_argv is not None
  183. spec = _original_spec
  184. argv = _original_argv
  185. else:
  186. spec = getattr(sys.modules["__main__"], "__spec__", None)
  187. argv = sys.argv
  188. if spec and spec.name != "__main__":
  189. # __spec__ is set in two cases: when running a module, and when running a directory. (when
  190. # running a file, there is no spec). In the former case, we must pass -m to maintain the
  191. # module-style behavior (setting sys.path), even though python stripped -m from its argv at
  192. # startup. If sys.path is exactly __main__, we're running a directory and should fall
  193. # through to the non-module behavior.
  194. #
  195. # Some of this, including the use of exactly __main__ as a spec for directory mode,
  196. # is documented at https://docs.python.org/3/library/runpy.html#runpy.run_path
  197. argv = ["-m", spec.name] + argv[1:]
  198. if not _has_execv:
  199. subprocess.Popen([sys.executable] + argv)
  200. os._exit(0)
  201. else:
  202. os.execv(sys.executable, [sys.executable] + argv)
  203. _USAGE = """
  204. python -m tornado.autoreload -m module.to.run [args...]
  205. python -m tornado.autoreload path/to/script.py [args...]
  206. """
  207. def main() -> None:
  208. """Command-line wrapper to re-run a script whenever its source changes.
  209. Scripts may be specified by filename or module name::
  210. python -m tornado.autoreload -m tornado.test.runtests
  211. python -m tornado.autoreload tornado/test/runtests.py
  212. Running a script with this wrapper is similar to calling
  213. `tornado.autoreload.wait` at the end of the script, but this wrapper
  214. can catch import-time problems like syntax errors that would otherwise
  215. prevent the script from reaching its call to `wait`.
  216. """
  217. # Remember that we were launched with autoreload as main.
  218. # The main module can be tricky; set the variables both in our globals
  219. # (which may be __main__) and the real importable version.
  220. #
  221. # We use optparse instead of the newer argparse because we want to
  222. # mimic the python command-line interface which requires stopping
  223. # parsing at the first positional argument. optparse supports
  224. # this but as far as I can tell argparse does not.
  225. import optparse
  226. import tornado.autoreload
  227. global _autoreload_is_main
  228. global _original_argv, _original_spec
  229. tornado.autoreload._autoreload_is_main = _autoreload_is_main = True
  230. original_argv = sys.argv
  231. tornado.autoreload._original_argv = _original_argv = original_argv
  232. original_spec = getattr(sys.modules["__main__"], "__spec__", None)
  233. tornado.autoreload._original_spec = _original_spec = original_spec
  234. parser = optparse.OptionParser(
  235. prog="python -m tornado.autoreload",
  236. usage=_USAGE,
  237. epilog="Either -m or a path must be specified, but not both",
  238. )
  239. parser.disable_interspersed_args()
  240. parser.add_option("-m", dest="module", metavar="module", help="module to run")
  241. parser.add_option(
  242. "--until-success",
  243. action="store_true",
  244. help="stop reloading after the program exist successfully (status code 0)",
  245. )
  246. opts, rest = parser.parse_args()
  247. if opts.module is None:
  248. if not rest:
  249. print("Either -m or a path must be specified", file=sys.stderr)
  250. sys.exit(1)
  251. path = rest[0]
  252. sys.argv = rest[:]
  253. else:
  254. path = None
  255. sys.argv = [sys.argv[0]] + rest
  256. # SystemExit.code is typed funny: https://github.com/python/typeshed/issues/8513
  257. # All we care about is truthiness
  258. exit_status: Union[int, str, None] = 1
  259. try:
  260. import runpy
  261. if opts.module is not None:
  262. runpy.run_module(opts.module, run_name="__main__", alter_sys=True)
  263. else:
  264. assert path is not None
  265. runpy.run_path(path, run_name="__main__")
  266. except SystemExit as e:
  267. exit_status = e.code
  268. gen_log.info("Script exited with status %s", e.code)
  269. except Exception as e:
  270. gen_log.warning("Script exited with uncaught exception", exc_info=True)
  271. # If an exception occurred at import time, the file with the error
  272. # never made it into sys.modules and so we won't know to watch it.
  273. # Just to make sure we've covered everything, walk the stack trace
  274. # from the exception and watch every file.
  275. for filename, lineno, name, line in traceback.extract_tb(sys.exc_info()[2]):
  276. watch(filename)
  277. if isinstance(e, SyntaxError):
  278. # SyntaxErrors are special: their innermost stack frame is fake
  279. # so extract_tb won't see it and we have to get the filename
  280. # from the exception object.
  281. if e.filename is not None:
  282. watch(e.filename)
  283. else:
  284. exit_status = 0
  285. gen_log.info("Script exited normally")
  286. # restore sys.argv so subsequent executions will include autoreload
  287. sys.argv = original_argv
  288. if opts.module is not None:
  289. assert opts.module is not None
  290. # runpy did a fake import of the module as __main__, but now it's
  291. # no longer in sys.modules. Figure out where it is and watch it.
  292. loader = pkgutil.get_loader(opts.module)
  293. if loader is not None and isinstance(loader, importlib.abc.FileLoader):
  294. watch(loader.get_filename())
  295. if opts.until_success and not exit_status:
  296. return
  297. wait()
  298. if __name__ == "__main__":
  299. # See also the other __main__ block at the top of the file, which modifies
  300. # sys.path before our imports
  301. main()