ext_reload_config.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. """
  2. WARNING: THIS EXTENSION IS EXPERIMENTAL
  3. Experimental extension may (and probably will) change at any time. Please do
  4. not rely on these features until they are more fully vetted.
  5. The Reload Config Framework Extension enables applications Built on Cement
  6. (tm) to easily reload configuration settings any time configuration files are
  7. modified without stopping/restarting the process.
  8. Requirements
  9. ------------
  10. * Python 2.6+, Python 3+
  11. * Python Modules: pyinotify
  12. * Linux (Kernel 2.6.13+)
  13. Features
  14. --------
  15. * Application configuration files (``CementApp.Meta.config_files``) are
  16. reloaded if modified.
  17. * Application plugin configuration files (Anything found in
  18. (``CementApp.Meta.plugin_config_dirs``) are reloaded if modified.
  19. * The framework calls ``CementApp.config.parse_file()`` on any watched files
  20. once the kernel has signaled a modification.
  21. * New configurations settings are accessible via ``CementApp.config`` nearly
  22. immediately once the kernel (inotify) picks up the change.
  23. * Provides a ``pre_reload_config`` and ``post_reload_config`` hook so that
  24. applications can tie into the event and perform operations any time a
  25. configuration file is modified.
  26. * Asynchronously monitors configuration files for changes via inotify. Long
  27. running processes are not blocked by the operations performed when files
  28. are detected to be modified.
  29. Limitations
  30. -----------
  31. * Currently this extension only re-parses configuration files into the
  32. config handler. Some applications may need further work in-order to truly
  33. honor those changes. For example, if a configuration settings toggles
  34. something on or off, or triggers something else to happen (like making an
  35. API connection, etc)... this extension does not currently handle that, and
  36. it is left up to the application developer to tie into the events via the
  37. provided hooks.
  38. * Only available on Linux based systems.
  39. Configuration
  40. -------------
  41. This extension does not currently honor any configuration settings.
  42. Hooks
  43. -----
  44. This extension defines the following hooks:
  45. pre_reload_config
  46. ^^^^^^^^^^^^^^^^^
  47. Run right before any framework actions are performed once modifications to
  48. any of the watched files are detected. Expects a single argument, which is
  49. the ``app`` object, and does not expect anything in return.
  50. .. code-block:: python
  51. def my_pre_reload_config_hook(app):
  52. # do something with app?
  53. pass
  54. post_reload_config
  55. ^^^^^^^^^^^^^^^^^^
  56. Run right after any framework actions are performed once modifications to any
  57. of the watched files are detected. Expects a single argument, which is the
  58. ``app`` object, and does not expect anything in return.
  59. .. code-block:: python
  60. def my_post_reload_config_hook(app):
  61. # do something with app?
  62. pass
  63. Usage
  64. -----
  65. The following example shows how to add the reload_config extension, as well as
  66. perform an arbitrary action any time configuration changes are detected.
  67. .. code-block:: python
  68. from time import sleep
  69. from cement.core.exc import CaughtSignal
  70. from cement.core.foundation import CementApp
  71. from cement.core.controller import CementBaseController, expose
  72. def print_foo(app):
  73. print "Foo => %s" % app.config.get('myapp', 'foo')
  74. class Base(CementBaseController):
  75. class Meta:
  76. label = 'base'
  77. @expose(hide=True)
  78. def default(self):
  79. print('Inside Base.default()')
  80. # simulate a long running process
  81. while True:
  82. sleep(30)
  83. class MyApp(CementApp):
  84. class Meta:
  85. label = 'myapp'
  86. base_controller = Base
  87. extensions = ['reload_config']
  88. with MyApp() as app:
  89. # run this anytime the configuration has changed
  90. app.hook.register('post_reload_config', print_foo)
  91. try:
  92. app.run()
  93. except CaughtSignal as e:
  94. # maybe do something... but catch it regardless so app.close() is
  95. # called when exiting `with` cleanly.
  96. print(e)
  97. The following would output something like the following when ``~/.myapp.conf``
  98. or any other configuration file is modified (spaces added for clarity):
  99. .. code-block:: console
  100. Inside Base.default()
  101. 2015-05-05 03:00:32,023 (DEBUG) cement.ext.ext_reload_config : config
  102. path modified: mask=IN_CLOSE_WRITE,
  103. path=/home/vagrant/.myapp.conf
  104. 2015-05-05 03:00:32,023 (DEBUG) cement.core.config : config file
  105. '/home/vagrant/.myapp.conf' exists,
  106. loading settings...
  107. 2015-05-05 03:00:32,023 (DEBUG) cement.core.hook : running hook
  108. 'post_reload_config' (<function print_foo
  109. at 0x7f1b52a5ab70>) from __main__
  110. Foo => bar
  111. 2015-05-05 03:00:32,023 (DEBUG) cement.ext.ext_reload_config : config
  112. path modified: mask=IN_CLOSE_WRITE,
  113. path=/home/vagrant/.myapp.conf
  114. 2015-05-05 03:00:32,023 (DEBUG) cement.core.config : config file
  115. '/home/vagrant/.myapp.conf' exists,
  116. loading settings...
  117. 2015-05-05 03:00:32,023 (DEBUG) cement.core.hook : running hook
  118. 'post_reload_config' (<function print_foo
  119. at 0x7f1b52a5ab70>) from __main__
  120. Foo => bar2
  121. 2015-05-05 03:00:32,023 (DEBUG) cement.ext.ext_reload_config : config
  122. path modified: mask=IN_CLOSE_WRITE,
  123. path=/home/vagrant/.myapp.conf
  124. 2015-05-05 03:00:32,023 (DEBUG) cement.core.config : config file
  125. '/home/vagrant/.myapp.conf' exists,
  126. loading settings...
  127. 2015-05-05 03:00:32,023 (DEBUG) cement.core.hook : running hook
  128. 'post_reload_config' (<function print_foo
  129. at 0x7f1b52a5ab70>) from __main__
  130. Foo => bar3
  131. """
  132. import os
  133. import signal
  134. import pyinotify
  135. from ..core import backend
  136. from ..utils.misc import minimal_logger
  137. from ..utils import shell, fs
  138. LOG = minimal_logger(__name__)
  139. MASK = pyinotify.IN_CLOSE_WRITE
  140. NOTIFIER = None
  141. class ConfigEventHandler(pyinotify.ProcessEvent):
  142. def __init__(self, app, watched_files, **kw):
  143. self.app = app
  144. self.watched_files = watched_files
  145. super(ConfigEventHandler, self).__init__()
  146. def process_default(self, event):
  147. if event.pathname in self.watched_files:
  148. LOG.debug('config path modified: mask=%s, path=%s' %
  149. (event.maskname, event.pathname))
  150. for res in self.app.hook.run('pre_reload_config', self.app):
  151. pass
  152. self.app.config.parse_file(event.pathname)
  153. for res in self.app.hook.run('post_reload_config', self.app):
  154. pass
  155. def spawn_watcher(app):
  156. global NOTIFIER
  157. # directories to tell inotify to watch
  158. watched_dirs = []
  159. # files to actual perform actions on
  160. watched_files = []
  161. # watch manager
  162. wm = pyinotify.WatchManager()
  163. # watch all config files, and plugin config files
  164. watched_files = []
  165. for plugin_dir in app._meta.plugin_config_dirs:
  166. plugin_dir = fs.abspath(plugin_dir)
  167. if not os.path.exists(plugin_dir):
  168. continue
  169. if plugin_dir not in watched_dirs:
  170. watched_dirs.append(plugin_dir)
  171. # just want the first one... looks wierd, but python 2/3 compat
  172. res = os.walk(plugin_dir)
  173. for path, dirs, files in os.walk(plugin_dir):
  174. plugin_config_files = files
  175. break
  176. for plugin_config in plugin_config_files:
  177. full_plugin_path = os.path.join(plugin_dir, plugin_config)
  178. if full_plugin_path not in watched_files:
  179. watched_files.append(full_plugin_path)
  180. for path in app._meta.config_files:
  181. if os.path.exists(path):
  182. if path not in watched_files:
  183. watched_files.append(path)
  184. this_dir = os.path.dirname(path)
  185. if this_dir not in watched_dirs:
  186. watched_dirs.append(this_dir)
  187. for path in watched_dirs:
  188. wm.add_watch(path, MASK, rec=True)
  189. # event handler
  190. eh = ConfigEventHandler(app, watched_files)
  191. # notifier
  192. NOTIFIER = pyinotify.ThreadedNotifier(wm, eh)
  193. NOTIFIER.start()
  194. def kill_watcher(app):
  195. if NOTIFIER.isAlive():
  196. NOTIFIER.stop()
  197. def signal_handler(app, signum, frame):
  198. if signum in [signal.SIGTERM, signal.SIGINT]:
  199. if NOTIFIER.isAlive():
  200. NOTIFIER.stop()
  201. def load(app):
  202. app.hook.define('pre_reload_config')
  203. app.hook.define('post_reload_config')
  204. app.hook.register('pre_run', spawn_watcher)
  205. app.hook.register('pre_close', kill_watcher)
  206. app.hook.register('signal', signal_handler)