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""" 4A file monitor and server restarter. 5 6Use this like: 7 8..code-block:: Python 9 10 import reloader 11 reloader.install() 12 13Then 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 21or 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 28or run a monitoring process in Python (``paster serve --reload`` does 29this). 30 31Use the ``watch_file(filename)`` function to cause a reload/restart for 32other other non-Python files (e.g., configuration files). If you have 33a 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 39Then every time the reloader polls files it will call 40``watch_config_files`` and check all the filenames it returns. 41""" 42 43from __future__ import print_function 44import os 45import sys 46import time 47import threading 48import traceback 49from paste.util.classinstance import classinstancemethod 50 51def 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 65class 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 152if 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 178watch_file = Monitor.watch_file 179add_file_callback = Monitor.add_file_callback 180