#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# wb_inotify.py
#
# Copyright 2018 Jelle Smet <development@smetj.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
#
from wishbone.module import InputModule
# I know no other working way to actually monkey patch select
# when inotify is imported.
# If you know another way, please consider submitting a
# patch or merge request
# todo(smetj): Fix dirty monkey patch of inotify select
import sys
from gevent import monkey; monkey.patch_all()
sys.modules["select"] = sys.modules["gevent.select"]
sys.modules["select"].epoll = sys.modules["select"].poll
from inotify.adapters import Inotify
from inotify import constants
from gevent import sleep
import os
import fnmatch
[docs]class WBInotify(InputModule):
'''**Monitors one or more paths for inotify events.**
Monitors one or more paths for the defined inotify events.
Inotify events can have following values:
- IN_ACCESS
- IN_ALL_EVENTS
- IN_ATTRIB
- IN_CLOEXEC
- IN_CLOSE
- IN_CLOSE_NOWRITE
- IN_CLOSE_WRITE
- IN_CREATE
- IN_DELETE
- IN_DELETE_SELF
- IN_DONT_FOLLOW
- IN_IGNORED
- IN_ISDIR
- IN_MASK_ADD
- IN_MODIFY
- IN_MOVE
- IN_MOVED_FROM
- IN_MOVED_TO
- IN_MOVE_SELF
- IN_NONBLOCK
- IN_ONESHOT
- IN_ONLYDIR
- IN_OPEN
- IN_Q_OVERFLOW
- IN_UNMOUNT
Outgoing events have following format::
{"path": "/tmp/test", "inotify_type": "IN_ACCESS"}
Parameters::
- destination(str)(data)
| In which field to store the inotify event data.
- native_events(bool)(False)
| Whether to expect incoming events to be native Wishbone events.
- initial_listing(bool)(True)
| When True, generates for each defined path an event. This is
| useful to initially give depending modules the filenames they
| need to function.
- glob_pattern(str)(*)
| A glob pattern to filter out only matching files.
- paths(dict)({"/tmp": ["IN_CREATE", "IN_CLOSE_WRITE"]})
| A dict of paths with a list of inotify events to monitor. When
| the list is empty no filtering is done and results into all
| inotify events going through.
Queues::
- outbox
| Outgoing notify events.
'''
def __init__(self, actor_config, native_events=False, destination="data",
initial_listing=True, glob_pattern="*", paths={"/tmp": ["IN_CREATE", "IN_CLOSE_WRITE"]}):
InputModule.__init__(self, actor_config)
self.pool.createQueue("outbox")
def preHook(self):
for path, event_types in self.kwargs.paths.items():
for event_type in event_types:
if event_type not in constants.__dict__:
raise Exception("Inotify event type '%s' defined for path '%s' is not valid." % (event_type, path))
for path, inotify_types in self.kwargs.paths.items():
self.sendToBackground(self.monitor, path, inotify_types, self.kwargs.glob_pattern)
def monitor(self, path, inotify_types, glob_pattern):
'''Monitors ``path`` for ``inotify_types`` on files and dirs matching ``glob_pattern``
:param str path: The path to monitor
:param list inotify_types: A list of inotify types to monitor
:param str glob_pattern: Paths need to match in order to be returned.
:return: None
'''
all_types = ', '.join(inotify_types)
if all_types == '':
all_types = "ALL"
while self.loop():
if os.path.exists(path) and os.access(path, os.R_OK):
self.logging.info("Started to monitor path '%s' for '%s' inotify events on paths matching '%s'." % (os.path.abspath(path), all_types, glob_pattern))
if self.kwargs.initial_listing:
for p in self.__getAllFiles(path, glob_pattern):
for payload in self.decode(p):
event = self.generateEvent({"path": os.path.abspath(payload), "inotify_type": "WISHBONE_INIT"}, self.kwargs.destination)
self.submit(event, "outbox")
try:
for abs_path, i_type in self.__setupInotifyMonitor(path, inotify_types, glob_pattern):
for payload in self.decode(abs_path):
event = self.generateEvent({"path": payload, "inotify_type": i_type}, self.kwargs.destination)
self.submit(event, "outbox")
except Exception as err:
self.logging.critical('Failed to initialize inotify monitor. This needs immediate attention. Reason: %s' % err)
sleep(1)
else:
self.logging.warning("The defined path '%s' does not exist or is not readable. Will sleep for 5 seconds and try again." % (path))
sleep(5)
def __getAllFiles(self, path, glob_pattern):
"""
Returns all files it can find in ``path``. If ``path`` is a directory
it returns all files found in the directory (not recursive)
:param str path: The path to investigate.
:param str glob_pattern: The patterns to which files and dirs have to match.
:return: Generator
"""
if os.path.isdir(path):
all_files = [os.path.abspath("%s/%s" % (path, name)) for name in os.listdir(path) if os.path.isfile("%s/%s" % (path, name))]
else:
all_files = [os.path.abspath(path)]
for f in all_files:
if fnmatch.fnmatch(f, glob_pattern):
yield f
def __setupInotifyMonitor(self, path, inotify_types, glob_pattern):
'''
Initializes an inotify monitor process on ``path`` and yields the
defined ``inotify_types`` generated on the paths matching the
``glob_pattern``.
:param str path: The path to monitor
:param list inotify_types: A list of inotify types to monitor
:param str glob_pattern: Paths need to match in order to be returned.
:return: generator
'''
file_exists = True
i = Inotify()
i.add_watch(path)
while file_exists and self.loop():
for event in i.event_gen(yield_nones=False):
if event is not None:
for inotify_type in event[1]:
if inotify_type in inotify_types or inotify_types == []:
abs_path = os.path.abspath("%s/%s" % (event[2], event[3]))
if fnmatch.fnmatch(abs_path, glob_pattern):
yield abs_path.rstrip('/'), inotify_type
if inotify_type == "IN_DELETE_SELF":
file_exists = False
break
else:
sleep(1)