You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			351 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			351 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
#
 | 
						|
# Copyright 2009 Facebook
 | 
						|
#
 | 
						|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
						|
# not use this file except in compliance with the License. You may obtain
 | 
						|
# a copy of the License at
 | 
						|
#
 | 
						|
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
# Unless required by applicable law or agreed to in writing, software
 | 
						|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
						|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
						|
# License for the specific language governing permissions and limitations
 | 
						|
# under the License.
 | 
						|
 | 
						|
"""Automatically restart the server when a source file is modified.
 | 
						|
 | 
						|
Most applications should not access this module directly.  Instead,
 | 
						|
pass the keyword argument ``autoreload=True`` to the
 | 
						|
`tornado.web.Application` constructor (or ``debug=True``, which
 | 
						|
enables this setting and several others).  This will enable autoreload
 | 
						|
mode as well as checking for changes to templates and static
 | 
						|
resources.  Note that restarting is a destructive operation and any
 | 
						|
requests in progress will be aborted when the process restarts.  (If
 | 
						|
you want to disable autoreload while using other debug-mode features,
 | 
						|
pass both ``debug=True`` and ``autoreload=False``).
 | 
						|
 | 
						|
This module can also be used as a command-line wrapper around scripts
 | 
						|
such as unit test runners.  See the `main` method for details.
 | 
						|
 | 
						|
The command-line wrapper and Application debug modes can be used together.
 | 
						|
This combination is encouraged as the wrapper catches syntax errors and
 | 
						|
other import-time failures, while debug mode catches changes once
 | 
						|
the server has started.
 | 
						|
 | 
						|
This module will not work correctly when `.HTTPServer`'s multi-process
 | 
						|
mode is used.
 | 
						|
 | 
						|
Reloading loses any Python interpreter command-line arguments (e.g. ``-u``)
 | 
						|
because it re-executes Python using ``sys.executable`` and ``sys.argv``.
 | 
						|
Additionally, modifying these variables will cause reloading to behave
 | 
						|
incorrectly.
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
import os
 | 
						|
import sys
 | 
						|
 | 
						|
# sys.path handling
 | 
						|
# -----------------
 | 
						|
#
 | 
						|
# If a module is run with "python -m", the current directory (i.e. "")
 | 
						|
# is automatically prepended to sys.path, but not if it is run as
 | 
						|
# "path/to/file.py".  The processing for "-m" rewrites the former to
 | 
						|
# the latter, so subsequent executions won't have the same path as the
 | 
						|
# original.
 | 
						|
#
 | 
						|
# Conversely, when run as path/to/file.py, the directory containing
 | 
						|
# file.py gets added to the path, which can cause confusion as imports
 | 
						|
# may become relative in spite of the future import.
 | 
						|
#
 | 
						|
# We address the former problem by reconstructing the original command
 | 
						|
# line before re-execution so the new process will
 | 
						|
# see the correct path.  We attempt to address the latter problem when
 | 
						|
# tornado.autoreload is run as __main__.
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    # This sys.path manipulation must come before our imports (as much
 | 
						|
    # as possible - if we introduced a tornado.sys or tornado.os
 | 
						|
    # module we'd be in trouble), or else our imports would become
 | 
						|
    # relative again despite the future import.
 | 
						|
    #
 | 
						|
    # There is a separate __main__ block at the end of the file to call main().
 | 
						|
    if sys.path[0] == os.path.dirname(__file__):
 | 
						|
        del sys.path[0]
 | 
						|
 | 
						|
import functools
 | 
						|
import importlib.abc
 | 
						|
import os
 | 
						|
import pkgutil
 | 
						|
import sys
 | 
						|
import traceback
 | 
						|
import types
 | 
						|
import subprocess
 | 
						|
import weakref
 | 
						|
 | 
						|
from tornado import ioloop
 | 
						|
from tornado.log import gen_log
 | 
						|
from tornado import process
 | 
						|
 | 
						|
try:
 | 
						|
    import signal
 | 
						|
except ImportError:
 | 
						|
    signal = None  # type: ignore
 | 
						|
 | 
						|
from typing import Callable, Dict, Optional, List, Union
 | 
						|
 | 
						|
# os.execv is broken on Windows and can't properly parse command line
 | 
						|
# arguments and executable name if they contain whitespaces. subprocess
 | 
						|
# fixes that behavior.
 | 
						|
_has_execv = sys.platform != "win32"
 | 
						|
 | 
						|
_watched_files = set()
 | 
						|
_reload_hooks = []
 | 
						|
_reload_attempted = False
 | 
						|
_io_loops: "weakref.WeakKeyDictionary[ioloop.IOLoop, bool]" = (
 | 
						|
    weakref.WeakKeyDictionary()
 | 
						|
)
 | 
						|
_autoreload_is_main = False
 | 
						|
_original_argv: Optional[List[str]] = None
 | 
						|
_original_spec = None
 | 
						|
 | 
						|
 | 
						|
def start(check_time: int = 500) -> None:
 | 
						|
    """Begins watching source files for changes.
 | 
						|
 | 
						|
    .. versionchanged:: 5.0
 | 
						|
       The ``io_loop`` argument (deprecated since version 4.1) has been removed.
 | 
						|
    """
 | 
						|
    io_loop = ioloop.IOLoop.current()
 | 
						|
    if io_loop in _io_loops:
 | 
						|
        return
 | 
						|
    _io_loops[io_loop] = True
 | 
						|
    if len(_io_loops) > 1:
 | 
						|
        gen_log.warning("tornado.autoreload started more than once in the same process")
 | 
						|
    modify_times: Dict[str, float] = {}
 | 
						|
    callback = functools.partial(_reload_on_update, modify_times)
 | 
						|
    scheduler = ioloop.PeriodicCallback(callback, check_time)
 | 
						|
    scheduler.start()
 | 
						|
 | 
						|
 | 
						|
def wait() -> None:
 | 
						|
    """Wait for a watched file to change, then restart the process.
 | 
						|
 | 
						|
    Intended to be used at the end of scripts like unit test runners,
 | 
						|
    to run the tests again after any source file changes (but see also
 | 
						|
    the command-line interface in `main`)
 | 
						|
    """
 | 
						|
    io_loop = ioloop.IOLoop()
 | 
						|
    io_loop.add_callback(start)
 | 
						|
    io_loop.start()
 | 
						|
 | 
						|
 | 
						|
def watch(filename: str) -> None:
 | 
						|
    """Add a file to the watch list.
 | 
						|
 | 
						|
    All imported modules are watched by default.
 | 
						|
    """
 | 
						|
    _watched_files.add(filename)
 | 
						|
 | 
						|
 | 
						|
def add_reload_hook(fn: Callable[[], None]) -> None:
 | 
						|
    """Add a function to be called before reloading the process.
 | 
						|
 | 
						|
    Note that for open file and socket handles it is generally
 | 
						|
    preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or
 | 
						|
    `os.set_inheritable`) instead of using a reload hook to close them.
 | 
						|
    """
 | 
						|
    _reload_hooks.append(fn)
 | 
						|
 | 
						|
 | 
						|
def _reload_on_update(modify_times: Dict[str, float]) -> None:
 | 
						|
    if _reload_attempted:
 | 
						|
        # We already tried to reload and it didn't work, so don't try again.
 | 
						|
        return
 | 
						|
    if process.task_id() is not None:
 | 
						|
        # We're in a child process created by fork_processes.  If child
 | 
						|
        # processes restarted themselves, they'd all restart and then
 | 
						|
        # all call fork_processes again.
 | 
						|
        return
 | 
						|
    for module in list(sys.modules.values()):
 | 
						|
        # Some modules play games with sys.modules (e.g. email/__init__.py
 | 
						|
        # in the standard library), and occasionally this can cause strange
 | 
						|
        # failures in getattr.  Just ignore anything that's not an ordinary
 | 
						|
        # module.
 | 
						|
        if not isinstance(module, types.ModuleType):
 | 
						|
            continue
 | 
						|
        path = getattr(module, "__file__", None)
 | 
						|
        if not path:
 | 
						|
            continue
 | 
						|
        if path.endswith(".pyc") or path.endswith(".pyo"):
 | 
						|
            path = path[:-1]
 | 
						|
        _check_file(modify_times, path)
 | 
						|
    for path in _watched_files:
 | 
						|
        _check_file(modify_times, path)
 | 
						|
 | 
						|
 | 
						|
def _check_file(modify_times: Dict[str, float], path: str) -> None:
 | 
						|
    try:
 | 
						|
        modified = os.stat(path).st_mtime
 | 
						|
    except Exception:
 | 
						|
        return
 | 
						|
    if path not in modify_times:
 | 
						|
        modify_times[path] = modified
 | 
						|
        return
 | 
						|
    if modify_times[path] != modified:
 | 
						|
        gen_log.info("%s modified; restarting server", path)
 | 
						|
        _reload()
 | 
						|
 | 
						|
 | 
						|
def _reload() -> None:
 | 
						|
    global _reload_attempted
 | 
						|
    _reload_attempted = True
 | 
						|
    for fn in _reload_hooks:
 | 
						|
        fn()
 | 
						|
    if sys.platform != "win32":
 | 
						|
        # Clear the alarm signal set by
 | 
						|
        # ioloop.set_blocking_log_threshold so it doesn't fire
 | 
						|
        # after the exec.
 | 
						|
        signal.setitimer(signal.ITIMER_REAL, 0, 0)
 | 
						|
    # sys.path fixes: see comments at top of file.  If __main__.__spec__
 | 
						|
    # exists, we were invoked with -m and the effective path is about to
 | 
						|
    # change on re-exec.  Reconstruct the original command line to
 | 
						|
    # ensure that the new process sees the same path we did.
 | 
						|
    if _autoreload_is_main:
 | 
						|
        assert _original_argv is not None
 | 
						|
        spec = _original_spec
 | 
						|
        argv = _original_argv
 | 
						|
    else:
 | 
						|
        spec = getattr(sys.modules["__main__"], "__spec__", None)
 | 
						|
        argv = sys.argv
 | 
						|
    if spec and spec.name != "__main__":
 | 
						|
        # __spec__ is set in two cases: when running a module, and when running a directory. (when
 | 
						|
        # running a file, there is no spec). In the former case, we must pass -m to maintain the
 | 
						|
        # module-style behavior (setting sys.path), even though python stripped -m from its argv at
 | 
						|
        # startup. If sys.path is exactly __main__, we're running a directory and should fall
 | 
						|
        # through to the non-module behavior.
 | 
						|
        #
 | 
						|
        # Some of this, including the use of exactly __main__ as a spec for directory mode,
 | 
						|
        # is documented at https://docs.python.org/3/library/runpy.html#runpy.run_path
 | 
						|
        argv = ["-m", spec.name] + argv[1:]
 | 
						|
 | 
						|
    if not _has_execv:
 | 
						|
        subprocess.Popen([sys.executable] + argv)
 | 
						|
        os._exit(0)
 | 
						|
    else:
 | 
						|
        os.execv(sys.executable, [sys.executable] + argv)
 | 
						|
 | 
						|
 | 
						|
_USAGE = """
 | 
						|
  python -m tornado.autoreload -m module.to.run [args...]
 | 
						|
  python -m tornado.autoreload path/to/script.py [args...]
 | 
						|
"""
 | 
						|
 | 
						|
 | 
						|
def main() -> None:
 | 
						|
    """Command-line wrapper to re-run a script whenever its source changes.
 | 
						|
 | 
						|
    Scripts may be specified by filename or module name::
 | 
						|
 | 
						|
        python -m tornado.autoreload -m tornado.test.runtests
 | 
						|
        python -m tornado.autoreload tornado/test/runtests.py
 | 
						|
 | 
						|
    Running a script with this wrapper is similar to calling
 | 
						|
    `tornado.autoreload.wait` at the end of the script, but this wrapper
 | 
						|
    can catch import-time problems like syntax errors that would otherwise
 | 
						|
    prevent the script from reaching its call to `wait`.
 | 
						|
    """
 | 
						|
    # Remember that we were launched with autoreload as main.
 | 
						|
    # The main module can be tricky; set the variables both in our globals
 | 
						|
    # (which may be __main__) and the real importable version.
 | 
						|
    #
 | 
						|
    # We use optparse instead of the newer argparse because we want to
 | 
						|
    # mimic the python command-line interface which requires stopping
 | 
						|
    # parsing at the first positional argument. optparse supports
 | 
						|
    # this but as far as I can tell argparse does not.
 | 
						|
    import optparse
 | 
						|
    import tornado.autoreload
 | 
						|
 | 
						|
    global _autoreload_is_main
 | 
						|
    global _original_argv, _original_spec
 | 
						|
    tornado.autoreload._autoreload_is_main = _autoreload_is_main = True
 | 
						|
    original_argv = sys.argv
 | 
						|
    tornado.autoreload._original_argv = _original_argv = original_argv
 | 
						|
    original_spec = getattr(sys.modules["__main__"], "__spec__", None)
 | 
						|
    tornado.autoreload._original_spec = _original_spec = original_spec
 | 
						|
 | 
						|
    parser = optparse.OptionParser(
 | 
						|
        prog="python -m tornado.autoreload",
 | 
						|
        usage=_USAGE,
 | 
						|
        epilog="Either -m or a path must be specified, but not both",
 | 
						|
    )
 | 
						|
    parser.disable_interspersed_args()
 | 
						|
    parser.add_option("-m", dest="module", metavar="module", help="module to run")
 | 
						|
    parser.add_option(
 | 
						|
        "--until-success",
 | 
						|
        action="store_true",
 | 
						|
        help="stop reloading after the program exist successfully (status code 0)",
 | 
						|
    )
 | 
						|
    opts, rest = parser.parse_args()
 | 
						|
    if opts.module is None:
 | 
						|
        if not rest:
 | 
						|
            print("Either -m or a path must be specified", file=sys.stderr)
 | 
						|
            sys.exit(1)
 | 
						|
        path = rest[0]
 | 
						|
        sys.argv = rest[:]
 | 
						|
    else:
 | 
						|
        path = None
 | 
						|
        sys.argv = [sys.argv[0]] + rest
 | 
						|
 | 
						|
    # SystemExit.code is typed funny: https://github.com/python/typeshed/issues/8513
 | 
						|
    # All we care about is truthiness
 | 
						|
    exit_status: Union[int, str, None] = 1
 | 
						|
    try:
 | 
						|
        import runpy
 | 
						|
 | 
						|
        if opts.module is not None:
 | 
						|
            runpy.run_module(opts.module, run_name="__main__", alter_sys=True)
 | 
						|
        else:
 | 
						|
            assert path is not None
 | 
						|
            runpy.run_path(path, run_name="__main__")
 | 
						|
    except SystemExit as e:
 | 
						|
        exit_status = e.code
 | 
						|
        gen_log.info("Script exited with status %s", e.code)
 | 
						|
    except Exception as e:
 | 
						|
        gen_log.warning("Script exited with uncaught exception", exc_info=True)
 | 
						|
        # If an exception occurred at import time, the file with the error
 | 
						|
        # never made it into sys.modules and so we won't know to watch it.
 | 
						|
        # Just to make sure we've covered everything, walk the stack trace
 | 
						|
        # from the exception and watch every file.
 | 
						|
        for filename, lineno, name, line in traceback.extract_tb(sys.exc_info()[2]):
 | 
						|
            watch(filename)
 | 
						|
        if isinstance(e, SyntaxError):
 | 
						|
            # SyntaxErrors are special:  their innermost stack frame is fake
 | 
						|
            # so extract_tb won't see it and we have to get the filename
 | 
						|
            # from the exception object.
 | 
						|
            if e.filename is not None:
 | 
						|
                watch(e.filename)
 | 
						|
    else:
 | 
						|
        exit_status = 0
 | 
						|
        gen_log.info("Script exited normally")
 | 
						|
    # restore sys.argv so subsequent executions will include autoreload
 | 
						|
    sys.argv = original_argv
 | 
						|
 | 
						|
    if opts.module is not None:
 | 
						|
        assert opts.module is not None
 | 
						|
        # runpy did a fake import of the module as __main__, but now it's
 | 
						|
        # no longer in sys.modules.  Figure out where it is and watch it.
 | 
						|
        loader = pkgutil.get_loader(opts.module)
 | 
						|
        if loader is not None and isinstance(loader, importlib.abc.FileLoader):
 | 
						|
            watch(loader.get_filename())
 | 
						|
    if opts.until_success and not exit_status:
 | 
						|
        return
 | 
						|
    wait()
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    # See also the other __main__ block at the top of the file, which modifies
 | 
						|
    # sys.path before our imports
 | 
						|
    main()
 |