Home | History | Annotate | Download | only in paste
      1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
      2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
      3 """
      4 A file monitor and server restarter.
      5 
      6 Use this like:
      7 
      8 ..code-block:: Python
      9 
     10     import reloader
     11     reloader.install()
     12 
     13 Then make sure your server is installed with a shell script like::
     14 
     15     err=3
     16     while test "$err" -eq 3 ; do
     17         python server.py
     18         err="$?"
     19     done
     20 
     21 or is run from this .bat file (if you use Windows)::
     22 
     23     @echo off
     24     :repeat
     25         python server.py
     26     if %errorlevel% == 3 goto repeat
     27 
     28 or run a monitoring process in Python (``paster serve --reload`` does
     29 this).
     30 
     31 Use the ``watch_file(filename)`` function to cause a reload/restart for
     32 other other non-Python files (e.g., configuration files).  If you have
     33 a dynamic set of files that grows over time you can use something like::
     34 
     35     def watch_config_files():
     36         return CONFIG_FILE_CACHE.keys()
     37     paste.reloader.add_file_callback(watch_config_files)
     38 
     39 Then every time the reloader polls files it will call
     40 ``watch_config_files`` and check all the filenames it returns.
     41 """
     42 
     43 from __future__ import print_function
     44 import os
     45 import sys
     46 import time
     47 import threading
     48 import traceback
     49 from paste.util.classinstance import classinstancemethod
     50 
     51 def install(poll_interval=1):
     52     """
     53     Install the reloading monitor.
     54 
     55     On some platforms server threads may not terminate when the main
     56     thread does, causing ports to remain open/locked.  The
     57     ``raise_keyboard_interrupt`` option creates a unignorable signal
     58     which causes the whole application to shut-down (rudely).
     59     """
     60     mon = Monitor(poll_interval=poll_interval)
     61     t = threading.Thread(target=mon.periodic_reload)
     62     t.setDaemon(True)
     63     t.start()
     64 
     65 class Monitor(object):
     66 
     67     instances = []
     68     global_extra_files = []
     69     global_file_callbacks = []
     70 
     71     def __init__(self, poll_interval):
     72         self.module_mtimes = {}
     73         self.keep_running = True
     74         self.poll_interval = poll_interval
     75         self.extra_files = list(self.global_extra_files)
     76         self.instances.append(self)
     77         self.file_callbacks = list(self.global_file_callbacks)
     78 
     79     def periodic_reload(self):
     80         while True:
     81             if not self.check_reload():
     82                 # use os._exit() here and not sys.exit() since within a
     83                 # thread sys.exit() just closes the given thread and
     84                 # won't kill the process; note os._exit does not call
     85                 # any atexit callbacks, nor does it do finally blocks,
     86                 # flush open files, etc.  In otherwords, it is rude.
     87                 os._exit(3)
     88                 break
     89             time.sleep(self.poll_interval)
     90 
     91     def check_reload(self):
     92         filenames = list(self.extra_files)
     93         for file_callback in self.file_callbacks:
     94             try:
     95                 filenames.extend(file_callback())
     96             except:
     97                 print("Error calling paste.reloader callback %r:" % file_callback,
     98                       file=sys.stderr)
     99                 traceback.print_exc()
    100         for module in sys.modules.values():
    101             try:
    102                 filename = module.__file__
    103             except (AttributeError, ImportError):
    104                 continue
    105             if filename is not None:
    106                 filenames.append(filename)
    107         for filename in filenames:
    108             try:
    109                 stat = os.stat(filename)
    110                 if stat:
    111                     mtime = stat.st_mtime
    112                 else:
    113                     mtime = 0
    114             except (OSError, IOError):
    115                 continue
    116             if filename.endswith('.pyc') and os.path.exists(filename[:-1]):
    117                 mtime = max(os.stat(filename[:-1]).st_mtime, mtime)
    118             elif filename.endswith('$py.class') and \
    119                     os.path.exists(filename[:-9] + '.py'):
    120                 mtime = max(os.stat(filename[:-9] + '.py').st_mtime, mtime)
    121             if filename not in self.module_mtimes:
    122                 self.module_mtimes[filename] = mtime
    123             elif self.module_mtimes[filename] < mtime:
    124                 print("%s changed; reloading..." % filename, file=sys.stderr)
    125                 return False
    126         return True
    127 
    128     def watch_file(self, cls, filename):
    129         """Watch the named file for changes"""
    130         filename = os.path.abspath(filename)
    131         if self is None:
    132             for instance in cls.instances:
    133                 instance.watch_file(filename)
    134             cls.global_extra_files.append(filename)
    135         else:
    136             self.extra_files.append(filename)
    137 
    138     watch_file = classinstancemethod(watch_file)
    139 
    140     def add_file_callback(self, cls, callback):
    141         """Add a callback -- a function that takes no parameters -- that will
    142         return a list of filenames to watch for changes."""
    143         if self is None:
    144             for instance in cls.instances:
    145                 instance.add_file_callback(callback)
    146             cls.global_file_callbacks.append(callback)
    147         else:
    148             self.file_callbacks.append(callback)
    149 
    150     add_file_callback = classinstancemethod(add_file_callback)
    151 
    152 if sys.platform.startswith('java'):
    153     try:
    154         from _systemrestart import SystemRestart
    155     except ImportError:
    156         pass
    157     else:
    158         class JythonMonitor(Monitor):
    159 
    160             """
    161             Monitor that utilizes Jython's special
    162             ``_systemrestart.SystemRestart`` exception.
    163 
    164             When raised from the main thread it causes Jython to reload
    165             the interpreter in the existing Java process (avoiding
    166             startup time).
    167 
    168             Note that this functionality of Jython is experimental and
    169             may change in the future.
    170             """
    171 
    172             def periodic_reload(self):
    173                 while True:
    174                     if not self.check_reload():
    175                         raise SystemRestart()
    176                     time.sleep(self.poll_interval)
    177 
    178 watch_file = Monitor.watch_file
    179 add_file_callback = Monitor.add_file_callback
    180