# Source code for tornado.autoreload

    #
    # 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()