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