reloader.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. from __future__ import print_function
  32. import os
  33. import sys
  34. import time
  35. import threading
  36. import traceback
  37. from paste.util.classinstance import classinstancemethod
  38. def install(poll_interval=1):
  39. """
  40. Install the reloading monitor.
  41. On some platforms server threads may not terminate when the main
  42. thread does, causing ports to remain open/locked. The
  43. ``raise_keyboard_interrupt`` option creates a unignorable signal
  44. which causes the whole application to shut-down (rudely).
  45. """
  46. mon = Monitor(poll_interval=poll_interval)
  47. t = threading.Thread(target=mon.periodic_reload)
  48. t.setDaemon(True)
  49. t.start()
  50. class Monitor(object):
  51. instances = []
  52. global_extra_files = []
  53. global_file_callbacks = []
  54. def __init__(self, poll_interval):
  55. self.module_mtimes = {}
  56. self.keep_running = True
  57. self.poll_interval = poll_interval
  58. self.extra_files = list(self.global_extra_files)
  59. self.instances.append(self)
  60. self.file_callbacks = list(self.global_file_callbacks)
  61. def periodic_reload(self):
  62. while True:
  63. if not self.check_reload():
  64. # use os._exit() here and not sys.exit() since within a
  65. # thread sys.exit() just closes the given thread and
  66. # won't kill the process; note os._exit does not call
  67. # any atexit callbacks, nor does it do finally blocks,
  68. # flush open files, etc. In otherwords, it is rude.
  69. os._exit(3)
  70. break
  71. time.sleep(self.poll_interval)
  72. def check_reload(self):
  73. filenames = list(self.extra_files)
  74. for file_callback in self.file_callbacks:
  75. try:
  76. filenames.extend(file_callback())
  77. except:
  78. print("Error calling paste.reloader callback %r:" % file_callback,
  79. file=sys.stderr)
  80. traceback.print_exc()
  81. for module in sys.modules.values():
  82. try:
  83. filename = module.__file__
  84. except (AttributeError, ImportError):
  85. continue
  86. if filename is not None:
  87. filenames.append(filename)
  88. for filename in filenames:
  89. try:
  90. stat = os.stat(filename)
  91. if stat:
  92. mtime = stat.st_mtime
  93. else:
  94. mtime = 0
  95. except (OSError, IOError):
  96. continue
  97. if filename.endswith('.pyc') and os.path.exists(filename[:-1]):
  98. mtime = max(os.stat(filename[:-1]).st_mtime, mtime)
  99. elif filename.endswith('$py.class') and \
  100. os.path.exists(filename[:-9] + '.py'):
  101. mtime = max(os.stat(filename[:-9] + '.py').st_mtime, mtime)
  102. if not self.module_mtimes.has_key(filename):
  103. self.module_mtimes[filename] = mtime
  104. elif self.module_mtimes[filename] < mtime:
  105. print("%s changed; reloading..." % filename, file=sys.stderr)
  106. return False
  107. return True
  108. def watch_file(self, cls, filename):
  109. """Watch the named file for changes"""
  110. filename = os.path.abspath(filename)
  111. if self is None:
  112. for instance in cls.instances:
  113. instance.watch_file(filename)
  114. cls.global_extra_files.append(filename)
  115. else:
  116. self.extra_files.append(filename)
  117. watch_file = classinstancemethod(watch_file)
  118. def add_file_callback(self, cls, callback):
  119. """Add a callback -- a function that takes no parameters -- that will
  120. return a list of filenames to watch for changes."""
  121. if self is None:
  122. for instance in cls.instances:
  123. instance.add_file_callback(callback)
  124. cls.global_file_callbacks.append(callback)
  125. else:
  126. self.file_callbacks.append(callback)
  127. add_file_callback = classinstancemethod(add_file_callback)
  128. if sys.platform.startswith('java'):
  129. try:
  130. from _systemrestart import SystemRestart
  131. except ImportError:
  132. pass
  133. else:
  134. class JythonMonitor(Monitor):
  135. """
  136. Monitor that utilizes Jython's special
  137. ``_systemrestart.SystemRestart`` exception.
  138. When raised from the main thread it causes Jython to reload
  139. the interpreter in the existing Java process (avoiding
  140. startup time).
  141. Note that this functionality of Jython is experimental and
  142. may change in the future.
  143. """
  144. def periodic_reload(self):
  145. while True:
  146. if not self.check_reload():
  147. raise SystemRestart()
  148. time.sleep(self.poll_interval)
  149. watch_file = Monitor.watch_file
  150. add_file_callback = Monitor.add_file_callback