reloader.py 5.9 KB

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