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