autoreload.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2009 Facebook
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License"); you may
00006 # not use this file except in compliance with the License. You may obtain
00007 # a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
00013 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
00014 # License for the specific language governing permissions and limitations
00015 # under the License.
00016 
00017 """A module to automatically restart the server when a module is modified.
00018 
00019 Most applications should not call this module directly.  Instead, pass the
00020 keyword argument ``debug=True`` to the `tornado.web.Application` constructor.
00021 This will enable autoreload mode as well as checking for changes to templates
00022 and static resources.
00023 
00024 This module depends on IOLoop, so it will not work in WSGI applications
00025 and Google AppEngine.  It also will not work correctly when HTTPServer's
00026 multi-process mode is used.
00027 """
00028 
00029 from __future__ import absolute_import, division, with_statement
00030 
00031 import os
00032 import sys
00033 
00034 # sys.path handling
00035 # -----------------
00036 #
00037 # If a module is run with "python -m", the current directory (i.e. "")
00038 # is automatically prepended to sys.path, but not if it is run as
00039 # "path/to/file.py".  The processing for "-m" rewrites the former to
00040 # the latter, so subsequent executions won't have the same path as the
00041 # original.
00042 #
00043 # Conversely, when run as path/to/file.py, the directory containing
00044 # file.py gets added to the path, which can cause confusion as imports
00045 # may become relative in spite of the future import.
00046 #
00047 # We address the former problem by setting the $PYTHONPATH environment
00048 # variable before re-execution so the new process will see the correct
00049 # path.  We attempt to address the latter problem when tornado.autoreload
00050 # is run as __main__, although we can't fix the general case because
00051 # we cannot reliably reconstruct the original command line
00052 # (http://bugs.python.org/issue14208).
00053 
00054 if __name__ == "__main__":
00055     # This sys.path manipulation must come before our imports (as much
00056     # as possible - if we introduced a tornado.sys or tornado.os
00057     # module we'd be in trouble), or else our imports would become
00058     # relative again despite the future import.
00059     #
00060     # There is a separate __main__ block at the end of the file to call main().
00061     if sys.path[0] == os.path.dirname(__file__):
00062         del sys.path[0]
00063 
00064 import functools
00065 import logging
00066 import os
00067 import pkgutil
00068 import sys
00069 import types
00070 import subprocess
00071 
00072 from tornado import ioloop
00073 from tornado import process
00074 
00075 try:
00076     import signal
00077 except ImportError:
00078     signal = None
00079 
00080 
00081 def start(io_loop=None, check_time=500):
00082     """Restarts the process automatically when a module is modified.
00083 
00084     We run on the I/O loop, and restarting is a destructive operation,
00085     so will terminate any pending requests.
00086     """
00087     io_loop = io_loop or ioloop.IOLoop.instance()
00088     add_reload_hook(functools.partial(_close_all_fds, io_loop))
00089     modify_times = {}
00090     callback = functools.partial(_reload_on_update, modify_times)
00091     scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
00092     scheduler.start()
00093 
00094 
00095 def wait():
00096     """Wait for a watched file to change, then restart the process.
00097 
00098     Intended to be used at the end of scripts like unit test runners,
00099     to run the tests again after any source file changes (but see also
00100     the command-line interface in `main`)
00101     """
00102     io_loop = ioloop.IOLoop()
00103     start(io_loop)
00104     io_loop.start()
00105 
00106 _watched_files = set()
00107 
00108 
00109 def watch(filename):
00110     """Add a file to the watch list.
00111 
00112     All imported modules are watched by default.
00113     """
00114     _watched_files.add(filename)
00115 
00116 _reload_hooks = []
00117 
00118 
00119 def add_reload_hook(fn):
00120     """Add a function to be called before reloading the process.
00121 
00122     Note that for open file and socket handles it is generally
00123     preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or
00124     `tornado.platform.auto.set_close_exec`) instead of using a reload
00125     hook to close them.
00126     """
00127     _reload_hooks.append(fn)
00128 
00129 
00130 def _close_all_fds(io_loop):
00131     for fd in io_loop._handlers.keys():
00132         try:
00133             os.close(fd)
00134         except Exception:
00135             pass
00136 
00137 _reload_attempted = False
00138 
00139 
00140 def _reload_on_update(modify_times):
00141     if _reload_attempted:
00142         # We already tried to reload and it didn't work, so don't try again.
00143         return
00144     if process.task_id() is not None:
00145         # We're in a child process created by fork_processes.  If child
00146         # processes restarted themselves, they'd all restart and then
00147         # all call fork_processes again.
00148         return
00149     for module in sys.modules.values():
00150         # Some modules play games with sys.modules (e.g. email/__init__.py
00151         # in the standard library), and occasionally this can cause strange
00152         # failures in getattr.  Just ignore anything that's not an ordinary
00153         # module.
00154         if not isinstance(module, types.ModuleType):
00155             continue
00156         path = getattr(module, "__file__", None)
00157         if not path:
00158             continue
00159         if path.endswith(".pyc") or path.endswith(".pyo"):
00160             path = path[:-1]
00161         _check_file(modify_times, path)
00162     for path in _watched_files:
00163         _check_file(modify_times, path)
00164 
00165 
00166 def _check_file(modify_times, path):
00167     try:
00168         modified = os.stat(path).st_mtime
00169     except Exception:
00170         return
00171     if path not in modify_times:
00172         modify_times[path] = modified
00173         return
00174     if modify_times[path] != modified:
00175         logging.info("%s modified; restarting server", path)
00176         _reload()
00177 
00178 
00179 def _reload():
00180     global _reload_attempted
00181     _reload_attempted = True
00182     for fn in _reload_hooks:
00183         fn()
00184     if hasattr(signal, "setitimer"):
00185         # Clear the alarm signal set by
00186         # ioloop.set_blocking_log_threshold so it doesn't fire
00187         # after the exec.
00188         signal.setitimer(signal.ITIMER_REAL, 0, 0)
00189     # sys.path fixes: see comments at top of file.  If sys.path[0] is an empty
00190     # string, we were (probably) invoked with -m and the effective path
00191     # is about to change on re-exec.  Add the current directory to $PYTHONPATH
00192     # to ensure that the new process sees the same path we did.
00193     path_prefix = '.' + os.pathsep
00194     if (sys.path[0] == '' and
00195         not os.environ.get("PYTHONPATH", "").startswith(path_prefix)):
00196         os.environ["PYTHONPATH"] = (path_prefix +
00197                                     os.environ.get("PYTHONPATH", ""))
00198     if sys.platform == 'win32':
00199         # os.execv is broken on Windows and can't properly parse command line
00200         # arguments and executable name if they contain whitespaces. subprocess
00201         # fixes that behavior.
00202         subprocess.Popen([sys.executable] + sys.argv)
00203         sys.exit(0)
00204     else:
00205         try:
00206             os.execv(sys.executable, [sys.executable] + sys.argv)
00207         except OSError:
00208             # Mac OS X versions prior to 10.6 do not support execv in
00209             # a process that contains multiple threads.  Instead of
00210             # re-executing in the current process, start a new one
00211             # and cause the current process to exit.  This isn't
00212             # ideal since the new process is detached from the parent
00213             # terminal and thus cannot easily be killed with ctrl-C,
00214             # but it's better than not being able to autoreload at
00215             # all.
00216             # Unfortunately the errno returned in this case does not
00217             # appear to be consistent, so we can't easily check for
00218             # this error specifically.
00219             os.spawnv(os.P_NOWAIT, sys.executable,
00220                       [sys.executable] + sys.argv)
00221             sys.exit(0)
00222 
00223 _USAGE = """\
00224 Usage:
00225   python -m tornado.autoreload -m module.to.run [args...]
00226   python -m tornado.autoreload path/to/script.py [args...]
00227 """
00228 
00229 
00230 def main():
00231     """Command-line wrapper to re-run a script whenever its source changes.
00232 
00233     Scripts may be specified by filename or module name::
00234 
00235         python -m tornado.autoreload -m tornado.test.runtests
00236         python -m tornado.autoreload tornado/test/runtests.py
00237 
00238     Running a script with this wrapper is similar to calling
00239     `tornado.autoreload.wait` at the end of the script, but this wrapper
00240     can catch import-time problems like syntax errors that would otherwise
00241     prevent the script from reaching its call to `wait`.
00242     """
00243     original_argv = sys.argv
00244     sys.argv = sys.argv[:]
00245     if len(sys.argv) >= 3 and sys.argv[1] == "-m":
00246         mode = "module"
00247         module = sys.argv[2]
00248         del sys.argv[1:3]
00249     elif len(sys.argv) >= 2:
00250         mode = "script"
00251         script = sys.argv[1]
00252         sys.argv = sys.argv[1:]
00253     else:
00254         print >>sys.stderr, _USAGE
00255         sys.exit(1)
00256 
00257     try:
00258         if mode == "module":
00259             import runpy
00260             runpy.run_module(module, run_name="__main__", alter_sys=True)
00261         elif mode == "script":
00262             with open(script) as f:
00263                 global __file__
00264                 __file__ = script
00265                 # Use globals as our "locals" dictionary so that
00266                 # something that tries to import __main__ (e.g. the unittest
00267                 # module) will see the right things.
00268                 exec f.read() in globals(), globals()
00269     except SystemExit, e:
00270         logging.info("Script exited with status %s", e.code)
00271     except Exception, e:
00272         logging.warning("Script exited with uncaught exception", exc_info=True)
00273         if isinstance(e, SyntaxError):
00274             watch(e.filename)
00275     else:
00276         logging.info("Script exited normally")
00277     # restore sys.argv so subsequent executions will include autoreload
00278     sys.argv = original_argv
00279 
00280     if mode == 'module':
00281         # runpy did a fake import of the module as __main__, but now it's
00282         # no longer in sys.modules.  Figure out where it is and watch it.
00283         loader = pkgutil.get_loader(module)
00284         if loader is not None:
00285             watch(loader.get_filename())
00286 
00287     wait()
00288 
00289 
00290 if __name__ == "__main__":
00291     # See also the other __main__ block at the top of the file, which modifies
00292     # sys.path before our imports
00293     main()


roswww
Author(s): Jonathan Mace
autogenerated on Thu Jan 2 2014 11:53:30