""" Cement watchdog extension module. **Note** This extension has an external dependency on ``watchdog``. Cement explicitly does **not** include external dependencies for optional extensions. * In Cement ``>=3.0.8`` you must include ``cement[watchdog]`` in your applications dependencies. * In Cement ``<3.0.8`` you must include ``watchdog`` in your applications dependencies. """ from __future__ import annotations import os from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler, FileSystemEvent from typing import Any, List, Optional, Type, TYPE_CHECKING from ..core.meta import MetaMixin from ..core.exc import FrameworkError from ..utils.misc import minimal_logger from ..utils import fs if TYPE_CHECKING: from ..core.foundation import App # pragma: nocover LOG = minimal_logger(__name__) class WatchdogEventHandler(FileSystemEventHandler): """ Default event handler used by Cement, that logs all events to the application's debug log. Additional ``*args`` and ``**kwargs`` are passed to the parent class. :param app: The application object """ def __init__(self, app: App, *args: Any, **kw: Any) -> None: super(WatchdogEventHandler, self).__init__(*args, **kw) self.app = app def on_any_event(self, event: FileSystemEvent) -> None: self.app.log.debug(f"Watchdog Event: {event}") # pragma: nocover class WatchdogManager(MetaMixin): """ The manager class that is attached to the application object via ``App.extend()``. Usage: .. code-block:: python with MyApp() as app: app.watchdog.start() app.watchdog.stop() app.watchdog.join() """ class Meta: #: The observer class to use on the backend observer = Observer #: The default event handler class to use if none is provided default_event_handler = WatchdogEventHandler _meta: Meta # type: ignore def __init__(self, app: App, *args: Any, **kw: Any) -> None: super(WatchdogManager, self).__init__(*args, **kw) self.app = app self.paths: List[str] = [] self.observer = self._meta.observer() def add(self, path: str, event_handler: Optional[Type] = None, recursive: bool = True) -> bool: """ Add a directory path and event handler to the observer. Args: path (str): A directory path to monitor (str) Keyword Args: event_handler (class): An event handler class used to handle events for ``path`` (class) recursive (bool): Whether to monitor the ``path`` recursively Returns: bool: ``True`` if the path is added, ``False`` otherwise. """ path = fs.abspath(path) if not os.path.exists(path): LOG.debug(f'watchdog path {path} does not exist... ignoring') return False if event_handler is None: event_handler = self._meta.default_event_handler LOG.debug(f'adding path {path} with event handler {event_handler}') self.observer.schedule(event_handler(self.app), path, recursive=recursive) # type: ignore return True def start(self, *args: Any, **kw: Any) -> None: """ Start the observer. All ``*args`` and ``**kwargs`` are passed down to the backend observer. """ for res in self.app.hook.run('watchdog_pre_start', self.app): pass LOG.debug('starting watchdog observer') self.observer.start(*args, **kw) # type: ignore for res in self.app.hook.run('watchdog_post_start', self.app): pass def stop(self, *args: Any, **kw: Any) -> None: """ Stop the observer. All ``*args`` and ``**kwargs`` are passed down to the backend observer. """ for res in self.app.hook.run('watchdog_pre_stop', self.app): pass LOG.debug('stopping watchdog observer') self.observer.stop(*args, **kw) # type: ignore for res in self.app.hook.run('watchdog_post_stop', self.app): pass def join(self, *args: Any, **kw: Any) -> None: """ Join the observer with the parent process. All ``*args`` and ``**kwargs`` are passed down to the backend observer. """ for res in self.app.hook.run('watchdog_pre_join', self.app): pass LOG.debug('joining watchdog observer') self.observer.join(*args, **kw) for res in self.app.hook.run('watchdog_post_join', self.app): pass def watchdog_extend_app(app: App) -> None: app.extend('watchdog', WatchdogManager(app)) def watchdog_start(app: App) -> None: app.watchdog.start() def watchdog_cleanup(app: App) -> None: if app.watchdog.observer.is_alive(): app.watchdog.stop() app.watchdog.join() def watchdog_add_paths(app: App) -> None: if hasattr(app._meta, 'watchdog_paths'): for path_spec in app._meta.watchdog_paths: # odd... if a tuple is a single item it ends up as a str? # FIXME: coverage gets lots in testing if isinstance(path_spec, str): app.watchdog.add(path_spec) # pragma: nocover elif isinstance(path_spec, tuple): app.watchdog.add(*path_spec) # pragma: nocover else: raise FrameworkError( "Watchdog path spec must be a tuple, not '%s' in: %s" % (type(path_spec).__name__, path_spec) ) def load(app: App) -> None: app.hook.define('watchdog_pre_start') app.hook.define('watchdog_post_start') app.hook.define('watchdog_pre_stop') app.hook.define('watchdog_post_stop') app.hook.define('watchdog_pre_join') app.hook.define('watchdog_post_join') app.hook.register('post_setup', watchdog_extend_app, weight=-1) app.hook.register('post_setup', watchdog_add_paths) app.hook.register('pre_run', watchdog_start) app.hook.register('pre_close', watchdog_cleanup)