From 972dcfcdbfe75dcfeb777150c136576cf1a71e99 Mon Sep 17 00:00:00 2001 From: Tudor Florea Date: Fri, 9 Oct 2015 22:59:03 +0200 Subject: initial commit for Enea Linux 5.0 arm Signed-off-by: Tudor Florea --- bitbake/lib/pyinotify.py | 2416 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2416 insertions(+) create mode 100644 bitbake/lib/pyinotify.py (limited to 'bitbake/lib/pyinotify.py') diff --git a/bitbake/lib/pyinotify.py b/bitbake/lib/pyinotify.py new file mode 100644 index 0000000000..2dae002118 --- /dev/null +++ b/bitbake/lib/pyinotify.py @@ -0,0 +1,2416 @@ +#!/usr/bin/env python + +# pyinotify.py - python interface to inotify +# Copyright (c) 2005-2015 Sebastien Martini +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +pyinotify + +@author: Sebastien Martini +@license: MIT License +@contact: seb@dbzteam.org +""" + +class PyinotifyError(Exception): + """Indicates exceptions raised by a Pyinotify class.""" + pass + + +class UnsupportedPythonVersionError(PyinotifyError): + """ + Raised on unsupported Python versions. + """ + def __init__(self, version): + """ + @param version: Current Python version + @type version: string + """ + err = 'Python %s is unsupported, requires at least Python 2.4' + PyinotifyError.__init__(self, err % version) + + +# Check Python version +import sys +if sys.version_info < (2, 4): + raise UnsupportedPythonVersionError(sys.version) + + +# Import directives +import threading +import os +import select +import struct +import fcntl +import errno +import termios +import array +import logging +import atexit +from collections import deque +from datetime import datetime, timedelta +import time +import re +import asyncore +import subprocess + +try: + from functools import reduce +except ImportError: + pass # Will fail on Python 2.4 which has reduce() builtin anyway. + +try: + from glob import iglob as glob +except ImportError: + # Python 2.4 does not have glob.iglob(). + from glob import glob as glob + +try: + import ctypes + import ctypes.util +except ImportError: + ctypes = None + +try: + import inotify_syscalls +except ImportError: + inotify_syscalls = None + + +__author__ = "seb@dbzteam.org (Sebastien Martini)" + +__version__ = "0.9.5" + +__metaclass__ = type # Use new-style classes by default + + +# Compatibity mode: set to True to improve compatibility with +# Pyinotify 0.7.1. Do not set this variable yourself, call the +# function compatibility_mode() instead. +COMPATIBILITY_MODE = False + + +class InotifyBindingNotFoundError(PyinotifyError): + """ + Raised when no inotify support couldn't be found. + """ + def __init__(self): + err = "Couldn't find any inotify binding" + PyinotifyError.__init__(self, err) + + +class INotifyWrapper: + """ + Abstract class wrapping access to inotify's functions. This is an + internal class. + """ + @staticmethod + def create(): + # First, try to use ctypes. + if ctypes: + inotify = _CtypesLibcINotifyWrapper() + if inotify.init(): + return inotify + # Second, see if C extension is compiled. + if inotify_syscalls: + inotify = _INotifySyscallsWrapper() + if inotify.init(): + return inotify + + def get_errno(self): + """ + Return None is no errno code is available. + """ + return self._get_errno() + + def str_errno(self): + code = self.get_errno() + if code is None: + return 'Errno: no errno support' + return 'Errno=%s (%s)' % (os.strerror(code), errno.errorcode[code]) + + def inotify_init(self): + return self._inotify_init() + + def inotify_add_watch(self, fd, pathname, mask): + # Unicode strings must be encoded to string prior to calling this + # method. + assert isinstance(pathname, str) + return self._inotify_add_watch(fd, pathname, mask) + + def inotify_rm_watch(self, fd, wd): + return self._inotify_rm_watch(fd, wd) + + +class _INotifySyscallsWrapper(INotifyWrapper): + def __init__(self): + # Stores the last errno value. + self._last_errno = None + + def init(self): + assert inotify_syscalls + return True + + def _get_errno(self): + return self._last_errno + + def _inotify_init(self): + try: + fd = inotify_syscalls.inotify_init() + except IOError, err: + self._last_errno = err.errno + return -1 + return fd + + def _inotify_add_watch(self, fd, pathname, mask): + try: + wd = inotify_syscalls.inotify_add_watch(fd, pathname, mask) + except IOError, err: + self._last_errno = err.errno + return -1 + return wd + + def _inotify_rm_watch(self, fd, wd): + try: + ret = inotify_syscalls.inotify_rm_watch(fd, wd) + except IOError, err: + self._last_errno = err.errno + return -1 + return ret + + +class _CtypesLibcINotifyWrapper(INotifyWrapper): + def __init__(self): + self._libc = None + self._get_errno_func = None + + def init(self): + assert ctypes + + try_libc_name = 'c' + if sys.platform.startswith('freebsd'): + try_libc_name = 'inotify' + + libc_name = None + try: + libc_name = ctypes.util.find_library(try_libc_name) + except (OSError, IOError): + pass # Will attemp to load it with None anyway. + + if sys.version_info >= (2, 6): + self._libc = ctypes.CDLL(libc_name, use_errno=True) + self._get_errno_func = ctypes.get_errno + else: + self._libc = ctypes.CDLL(libc_name) + try: + location = self._libc.__errno_location + location.restype = ctypes.POINTER(ctypes.c_int) + self._get_errno_func = lambda: location().contents.value + except AttributeError: + pass + + # Eventually check that libc has needed inotify bindings. + if (not hasattr(self._libc, 'inotify_init') or + not hasattr(self._libc, 'inotify_add_watch') or + not hasattr(self._libc, 'inotify_rm_watch')): + return False + + self._libc.inotify_init.argtypes = [] + self._libc.inotify_init.restype = ctypes.c_int + self._libc.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, + ctypes.c_uint32] + self._libc.inotify_add_watch.restype = ctypes.c_int + self._libc.inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int] + self._libc.inotify_rm_watch.restype = ctypes.c_int + return True + + def _get_errno(self): + if self._get_errno_func is not None: + return self._get_errno_func() + return None + + def _inotify_init(self): + assert self._libc is not None + return self._libc.inotify_init() + + def _inotify_add_watch(self, fd, pathname, mask): + assert self._libc is not None + pathname = ctypes.create_string_buffer(pathname) + return self._libc.inotify_add_watch(fd, pathname, mask) + + def _inotify_rm_watch(self, fd, wd): + assert self._libc is not None + return self._libc.inotify_rm_watch(fd, wd) + + def _sysctl(self, *args): + assert self._libc is not None + return self._libc.sysctl(*args) + + +# Logging +def logger_init(): + """Initialize logger instance.""" + log = logging.getLogger("pyinotify") + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter("[%(asctime)s %(name)s %(levelname)s] %(message)s")) + log.addHandler(console_handler) + log.setLevel(20) + return log + +log = logger_init() + + +# inotify's variables +class SysCtlINotify: + """ + Access (read, write) inotify's variables through sysctl. Usually it + requires administrator rights to update them. + + Examples: + - Read max_queued_events attribute: myvar = max_queued_events.value + - Update max_queued_events attribute: max_queued_events.value = 42 + """ + + inotify_attrs = {'max_user_instances': 1, + 'max_user_watches': 2, + 'max_queued_events': 3} + + def __init__(self, attrname, inotify_wrapper): + # FIXME: right now only supporting ctypes + assert ctypes + self._attrname = attrname + self._inotify_wrapper = inotify_wrapper + sino = ctypes.c_int * 3 + self._attr = sino(5, 20, SysCtlINotify.inotify_attrs[attrname]) + + @staticmethod + def create(attrname): + """ + Factory method instanciating and returning the right wrapper. + """ + # FIXME: right now only supporting ctypes + if ctypes is None: + return None + inotify_wrapper = _CtypesLibcINotifyWrapper() + if not inotify_wrapper.init(): + return None + return SysCtlINotify(attrname, inotify_wrapper) + + def get_val(self): + """ + Gets attribute's value. Raises OSError if the operation failed. + + @return: stored value. + @rtype: int + """ + oldv = ctypes.c_int(0) + size = ctypes.c_int(ctypes.sizeof(oldv)) + sysctl = self._inotify_wrapper._sysctl + res = sysctl(self._attr, 3, + ctypes.c_voidp(ctypes.addressof(oldv)), + ctypes.addressof(size), + None, 0) + if res == -1: + raise OSError(self._inotify_wrapper.get_errno(), + self._inotify_wrapper.str_errno()) + return oldv.value + + def set_val(self, nval): + """ + Sets new attribute's value. Raises OSError if the operation failed. + + @param nval: replaces current value by nval. + @type nval: int + """ + oldv = ctypes.c_int(0) + sizeo = ctypes.c_int(ctypes.sizeof(oldv)) + newv = ctypes.c_int(nval) + sizen = ctypes.c_int(ctypes.sizeof(newv)) + sysctl = self._inotify_wrapper._sysctl + res = sysctl(self._attr, 3, + ctypes.c_voidp(ctypes.addressof(oldv)), + ctypes.addressof(sizeo), + ctypes.c_voidp(ctypes.addressof(newv)), + sizen) + if res == -1: + raise OSError(self._inotify_wrapper.get_errno(), + self._inotify_wrapper.str_errno()) + + value = property(get_val, set_val) + + def __repr__(self): + return '<%s=%d>' % (self._attrname, self.get_val()) + + +# Inotify's variables +# +# FIXME: currently these variables are only accessible when ctypes is used, +# otherwise there are set to None. +# +# read: myvar = max_queued_events.value +# update: max_queued_events.value = 42 +# +for attrname in ('max_queued_events', 'max_user_instances', 'max_user_watches'): + globals()[attrname] = SysCtlINotify.create(attrname) + + +class EventsCodes: + """ + Set of codes corresponding to each kind of events. + Some of these flags are used to communicate with inotify, whereas + the others are sent to userspace by inotify notifying some events. + + @cvar IN_ACCESS: File was accessed. + @type IN_ACCESS: int + @cvar IN_MODIFY: File was modified. + @type IN_MODIFY: int + @cvar IN_ATTRIB: Metadata changed. + @type IN_ATTRIB: int + @cvar IN_CLOSE_WRITE: Writtable file was closed. + @type IN_CLOSE_WRITE: int + @cvar IN_CLOSE_NOWRITE: Unwrittable file closed. + @type IN_CLOSE_NOWRITE: int + @cvar IN_OPEN: File was opened. + @type IN_OPEN: int + @cvar IN_MOVED_FROM: File was moved from X. + @type IN_MOVED_FROM: int + @cvar IN_MOVED_TO: File was moved to Y. + @type IN_MOVED_TO: int + @cvar IN_CREATE: Subfile was created. + @type IN_CREATE: int + @cvar IN_DELETE: Subfile was deleted. + @type IN_DELETE: int + @cvar IN_DELETE_SELF: Self (watched item itself) was deleted. + @type IN_DELETE_SELF: int + @cvar IN_MOVE_SELF: Self (watched item itself) was moved. + @type IN_MOVE_SELF: int + @cvar IN_UNMOUNT: Backing fs was unmounted. + @type IN_UNMOUNT: int + @cvar IN_Q_OVERFLOW: Event queued overflowed. + @type IN_Q_OVERFLOW: int + @cvar IN_IGNORED: File was ignored. + @type IN_IGNORED: int + @cvar IN_ONLYDIR: only watch the path if it is a directory (new + in kernel 2.6.15). + @type IN_ONLYDIR: int + @cvar IN_DONT_FOLLOW: don't follow a symlink (new in kernel 2.6.15). + IN_ONLYDIR we can make sure that we don't watch + the target of symlinks. + @type IN_DONT_FOLLOW: int + @cvar IN_EXCL_UNLINK: Events are not generated for children after they + have been unlinked from the watched directory. + (new in kernel 2.6.36). + @type IN_EXCL_UNLINK: int + @cvar IN_MASK_ADD: add to the mask of an already existing watch (new + in kernel 2.6.14). + @type IN_MASK_ADD: int + @cvar IN_ISDIR: Event occurred against dir. + @type IN_ISDIR: int + @cvar IN_ONESHOT: Only send event once. + @type IN_ONESHOT: int + @cvar ALL_EVENTS: Alias for considering all of the events. + @type ALL_EVENTS: int + """ + + # The idea here is 'configuration-as-code' - this way, we get our nice class + # constants, but we also get nice human-friendly text mappings to do lookups + # against as well, for free: + FLAG_COLLECTIONS = {'OP_FLAGS': { + 'IN_ACCESS' : 0x00000001, # File was accessed + 'IN_MODIFY' : 0x00000002, # File was modified + 'IN_ATTRIB' : 0x00000004, # Metadata changed + 'IN_CLOSE_WRITE' : 0x00000008, # Writable file was closed + 'IN_CLOSE_NOWRITE' : 0x00000010, # Unwritable file closed + 'IN_OPEN' : 0x00000020, # File was opened + 'IN_MOVED_FROM' : 0x00000040, # File was moved from X + 'IN_MOVED_TO' : 0x00000080, # File was moved to Y + 'IN_CREATE' : 0x00000100, # Subfile was created + 'IN_DELETE' : 0x00000200, # Subfile was deleted + 'IN_DELETE_SELF' : 0x00000400, # Self (watched item itself) + # was deleted + 'IN_MOVE_SELF' : 0x00000800, # Self (watched item itself) was moved + }, + 'EVENT_FLAGS': { + 'IN_UNMOUNT' : 0x00002000, # Backing fs was unmounted + 'IN_Q_OVERFLOW' : 0x00004000, # Event queued overflowed + 'IN_IGNORED' : 0x00008000, # File was ignored + }, + 'SPECIAL_FLAGS': { + 'IN_ONLYDIR' : 0x01000000, # only watch the path if it is a + # directory + 'IN_DONT_FOLLOW' : 0x02000000, # don't follow a symlink + 'IN_EXCL_UNLINK' : 0x04000000, # exclude events on unlinked objects + 'IN_MASK_ADD' : 0x20000000, # add to the mask of an already + # existing watch + 'IN_ISDIR' : 0x40000000, # event occurred against dir + 'IN_ONESHOT' : 0x80000000, # only send event once + }, + } + + def maskname(mask): + """ + Returns the event name associated to mask. IN_ISDIR is appended to + the result when appropriate. Note: only one event is returned, because + only one event can be raised at a given time. + + @param mask: mask. + @type mask: int + @return: event name. + @rtype: str + """ + ms = mask + name = '%s' + if mask & IN_ISDIR: + ms = mask - IN_ISDIR + name = '%s|IN_ISDIR' + return name % EventsCodes.ALL_VALUES[ms] + + maskname = staticmethod(maskname) + + +# So let's now turn the configuration into code +EventsCodes.ALL_FLAGS = {} +EventsCodes.ALL_VALUES = {} +for flagc, valc in EventsCodes.FLAG_COLLECTIONS.items(): + # Make the collections' members directly accessible through the + # class dictionary + setattr(EventsCodes, flagc, valc) + + # Collect all the flags under a common umbrella + EventsCodes.ALL_FLAGS.update(valc) + + # Make the individual masks accessible as 'constants' at globals() scope + # and masknames accessible by values. + for name, val in valc.items(): + globals()[name] = val + EventsCodes.ALL_VALUES[val] = name + + +# all 'normal' events +ALL_EVENTS = reduce(lambda x, y: x | y, EventsCodes.OP_FLAGS.values()) +EventsCodes.ALL_FLAGS['ALL_EVENTS'] = ALL_EVENTS +EventsCodes.ALL_VALUES[ALL_EVENTS] = 'ALL_EVENTS' + + +class _Event: + """ + Event structure, represent events raised by the system. This + is the base class and should be subclassed. + + """ + def __init__(self, dict_): + """ + Attach attributes (contained in dict_) to self. + + @param dict_: Set of attributes. + @type dict_: dictionary + """ + for tpl in dict_.items(): + setattr(self, *tpl) + + def __repr__(self): + """ + @return: Generic event string representation. + @rtype: str + """ + s = '' + for attr, value in sorted(self.__dict__.items(), key=lambda x: x[0]): + if attr.startswith('_'): + continue + if attr == 'mask': + value = hex(getattr(self, attr)) + elif isinstance(value, basestring) and not value: + value = "''" + s += ' %s%s%s' % (output_format.field_name(attr), + output_format.punctuation('='), + output_format.field_value(value)) + + s = '%s%s%s %s' % (output_format.punctuation('<'), + output_format.class_name(self.__class__.__name__), + s, + output_format.punctuation('>')) + return s + + def __str__(self): + return repr(self) + + +class _RawEvent(_Event): + """ + Raw event, it contains only the informations provided by the system. + It doesn't infer anything. + """ + def __init__(self, wd, mask, cookie, name): + """ + @param wd: Watch Descriptor. + @type wd: int + @param mask: Bitmask of events. + @type mask: int + @param cookie: Cookie. + @type cookie: int + @param name: Basename of the file or directory against which the + event was raised in case where the watched directory + is the parent directory. None if the event was raised + on the watched item itself. + @type name: string or None + """ + # Use this variable to cache the result of str(self), this object + # is immutable. + self._str = None + # name: remove trailing '\0' + d = {'wd': wd, + 'mask': mask, + 'cookie': cookie, + 'name': name.rstrip('\0')} + _Event.__init__(self, d) + log.debug(str(self)) + + def __str__(self): + if self._str is None: + self._str = _Event.__str__(self) + return self._str + + +class Event(_Event): + """ + This class contains all the useful informations about the observed + event. However, the presence of each field is not guaranteed and + depends on the type of event. In effect, some fields are irrelevant + for some kind of event (for example 'cookie' is meaningless for + IN_CREATE whereas it is mandatory for IN_MOVE_TO). + + The possible fields are: + - wd (int): Watch Descriptor. + - mask (int): Mask. + - maskname (str): Readable event name. + - path (str): path of the file or directory being watched. + - name (str): Basename of the file or directory against which the + event was raised in case where the watched directory + is the parent directory. None if the event was raised + on the watched item itself. This field is always provided + even if the string is ''. + - pathname (str): Concatenation of 'path' and 'name'. + - src_pathname (str): Only present for IN_MOVED_TO events and only in + the case where IN_MOVED_FROM events are watched too. Holds the + source pathname from where pathname was moved from. + - cookie (int): Cookie. + - dir (bool): True if the event was raised against a directory. + + """ + def __init__(self, raw): + """ + Concretely, this is the raw event plus inferred infos. + """ + _Event.__init__(self, raw) + self.maskname = EventsCodes.maskname(self.mask) + if COMPATIBILITY_MODE: + self.event_name = self.maskname + try: + if self.name: + self.pathname = os.path.abspath(os.path.join(self.path, + self.name)) + else: + self.pathname = os.path.abspath(self.path) + except AttributeError, err: + # Usually it is not an error some events are perfectly valids + # despite the lack of these attributes. + log.debug(err) + + +class ProcessEventError(PyinotifyError): + """ + ProcessEventError Exception. Raised on ProcessEvent error. + """ + def __init__(self, err): + """ + @param err: Exception error description. + @type err: string + """ + PyinotifyError.__init__(self, err) + + +class _ProcessEvent: + """ + Abstract processing event class. + """ + def __call__(self, event): + """ + To behave like a functor the object must be callable. + This method is a dispatch method. Its lookup order is: + 1. process_MASKNAME method + 2. process_FAMILY_NAME method + 3. otherwise calls process_default + + @param event: Event to be processed. + @type event: Event object + @return: By convention when used from the ProcessEvent class: + - Returning False or None (default value) means keep on + executing next chained functors (see chain.py example). + - Returning True instead means do not execute next + processing functions. + @rtype: bool + @raise ProcessEventError: Event object undispatchable, + unknown event. + """ + stripped_mask = event.mask - (event.mask & IN_ISDIR) + maskname = EventsCodes.ALL_VALUES.get(stripped_mask) + if maskname is None: + raise ProcessEventError("Unknown mask 0x%08x" % stripped_mask) + + # 1- look for process_MASKNAME + meth = getattr(self, 'process_' + maskname, None) + if meth is not None: + return meth(event) + # 2- look for process_FAMILY_NAME + meth = getattr(self, 'process_IN_' + maskname.split('_')[1], None) + if meth is not None: + return meth(event) + # 3- default call method process_default + return self.process_default(event) + + def __repr__(self): + return '<%s>' % self.__class__.__name__ + + +class _SysProcessEvent(_ProcessEvent): + """ + There is three kind of processing according to each event: + + 1. special handling (deletion from internal container, bug, ...). + 2. default treatment: which is applied to the majority of events. + 3. IN_ISDIR is never sent alone, he is piggybacked with a standard + event, he is not processed as the others events, instead, its + value is captured and appropriately aggregated to dst event. + """ + def __init__(self, wm, notifier): + """ + + @param wm: Watch Manager. + @type wm: WatchManager instance + @param notifier: Notifier. + @type notifier: Notifier instance + """ + self._watch_manager = wm # watch manager + self._notifier = notifier # notifier + self._mv_cookie = {} # {cookie(int): (src_path(str), date), ...} + self._mv = {} # {src_path(str): (dst_path(str), date), ...} + + def cleanup(self): + """ + Cleanup (delete) old (>1mn) records contained in self._mv_cookie + and self._mv. + """ + date_cur_ = datetime.now() + for seq in [self._mv_cookie, self._mv]: + for k in seq.keys(): + if (date_cur_ - seq[k][1]) > timedelta(minutes=1): + log.debug('Cleanup: deleting entry %s', seq[k][0]) + del seq[k] + + def process_IN_CREATE(self, raw_event): + """ + If the event affects a directory and the auto_add flag of the + targetted watch is set to True, a new watch is added on this + new directory, with the same attribute values than those of + this watch. + """ + if raw_event.mask & IN_ISDIR: + watch_ = self._watch_manager.get_watch(raw_event.wd) + created_dir = os.path.join(watch_.path, raw_event.name) + if watch_.auto_add and not watch_.exclude_filter(created_dir): + addw = self._watch_manager.add_watch + # The newly monitored directory inherits attributes from its + # parent directory. + addw_ret = addw(created_dir, watch_.mask, + proc_fun=watch_.proc_fun, + rec=False, auto_add=watch_.auto_add, + exclude_filter=watch_.exclude_filter) + + # Trick to handle mkdir -p /d1/d2/t3 where d1 is watched and + # d2 and t3 (directory or file) are created. + # Since the directory d2 is new, then everything inside it must + # also be new. + created_dir_wd = addw_ret.get(created_dir) + if ((created_dir_wd is not None) and (created_dir_wd > 0) and + os.path.isdir(created_dir)): + try: + for name in os.listdir(created_dir): + inner = os.path.join(created_dir, name) + if self._watch_manager.get_wd(inner) is not None: + continue + # Generate (simulate) creation events for sub- + # directories and files. + if os.path.isfile(inner): + # symlinks are handled as files. + flags = IN_CREATE + elif os.path.isdir(inner): + flags = IN_CREATE | IN_ISDIR + else: + # This path should not be taken. + continue + rawevent = _RawEvent(created_dir_wd, flags, 0, name) + self._notifier.append_event(rawevent) + except OSError, err: + msg = "process_IN_CREATE, invalid directory %s: %s" + log.debug(msg % (created_dir, str(err))) + return self.process_default(raw_event) + + def process_IN_MOVED_FROM(self, raw_event): + """ + Map the cookie with the source path (+ date for cleaning). + """ + watch_ = self._watch_manager.get_watch(raw_event.wd) + path_ = watch_.path + src_path = os.path.normpath(os.path.join(path_, raw_event.name)) + self._mv_cookie[raw_event.cookie] = (src_path, datetime.now()) + return self.process_default(raw_event, {'cookie': raw_event.cookie}) + + def process_IN_MOVED_TO(self, raw_event): + """ + Map the source path with the destination path (+ date for + cleaning). + """ + watch_ = self._watch_manager.get_watch(raw_event.wd) + path_ = watch_.path + dst_path = os.path.normpath(os.path.join(path_, raw_event.name)) + mv_ = self._mv_cookie.get(raw_event.cookie) + to_append = {'cookie': raw_event.cookie} + if mv_ is not None: + self._mv[mv_[0]] = (dst_path, datetime.now()) + # Let's assume that IN_MOVED_FROM event is always queued before + # that its associated (they share a common cookie) IN_MOVED_TO + # event is queued itself. It is then possible in that scenario + # to provide as additional information to the IN_MOVED_TO event + # the original pathname of the moved file/directory. + to_append['src_pathname'] = mv_[0] + elif (raw_event.mask & IN_ISDIR and watch_.auto_add and + not watch_.exclude_filter(dst_path)): + # We got a diretory that's "moved in" from an unknown source and + # auto_add is enabled. Manually add watches to the inner subtrees. + # The newly monitored directory inherits attributes from its + # parent directory. + self._watch_manager.add_watch(dst_path, watch_.mask, + proc_fun=watch_.proc_fun, + rec=True, auto_add=True, + exclude_filter=watch_.exclude_filter) + return self.process_default(raw_event, to_append) + + def process_IN_MOVE_SELF(self, raw_event): + """ + STATUS: the following bug has been fixed in recent kernels (FIXME: + which version ?). Now it raises IN_DELETE_SELF instead. + + Old kernels were bugged, this event raised when the watched item + were moved, so we had to update its path, but under some circumstances + it was impossible: if its parent directory and its destination + directory wasn't watched. The kernel (see include/linux/fsnotify.h) + doesn't bring us enough informations like the destination path of + moved items. + """ + watch_ = self._watch_manager.get_watch(raw_event.wd) + src_path = watch_.path + mv_ = self._mv.get(src_path) + if mv_: + dest_path = mv_[0] + watch_.path = dest_path + # add the separator to the source path to avoid overlapping + # path issue when testing with startswith() + src_path += os.path.sep + src_path_len = len(src_path) + # The next loop renames all watches with src_path as base path. + # It seems that IN_MOVE_SELF does not provide IN_ISDIR information + # therefore the next loop is iterated even if raw_event is a file. + for w in self._watch_manager.watches.values(): + if w.path.startswith(src_path): + # Note that dest_path is a normalized path. + w.path = os.path.join(dest_path, w.path[src_path_len:]) + else: + log.error("The pathname '%s' of this watch %s has probably changed " + "and couldn't be updated, so it cannot be trusted " + "anymore. To fix this error move directories/files only " + "between watched parents directories, in this case e.g. " + "put a watch on '%s'.", + watch_.path, watch_, + os.path.normpath(os.path.join(watch_.path, + os.path.pardir))) + if not watch_.path.endswith('-unknown-path'): + watch_.path += '-unknown-path' + return self.process_default(raw_event) + + def process_IN_Q_OVERFLOW(self, raw_event): + """ + Only signal an overflow, most of the common flags are irrelevant + for this event (path, wd, name). + """ + return Event({'mask': raw_event.mask}) + + def process_IN_IGNORED(self, raw_event): + """ + The watch descriptor raised by this event is now ignored (forever), + it can be safely deleted from the watch manager dictionary. + After this event we can be sure that neither the event queue nor + the system will raise an event associated to this wd again. + """ + event_ = self.process_default(raw_event) + self._watch_manager.del_watch(raw_event.wd) + return event_ + + def process_default(self, raw_event, to_append=None): + """ + Commons handling for the followings events: + + IN_ACCESS, IN_MODIFY, IN_ATTRIB, IN_CLOSE_WRITE, IN_CLOSE_NOWRITE, + IN_OPEN, IN_DELETE, IN_DELETE_SELF, IN_UNMOUNT. + """ + watch_ = self._watch_manager.get_watch(raw_event.wd) + if raw_event.mask & (IN_DELETE_SELF | IN_MOVE_SELF): + # Unfornulately this information is not provided by the kernel + dir_ = watch_.dir + else: + dir_ = bool(raw_event.mask & IN_ISDIR) + dict_ = {'wd': raw_event.wd, + 'mask': raw_event.mask, + 'path': watch_.path, + 'name': raw_event.name, + 'dir': dir_} + if COMPATIBILITY_MODE: + dict_['is_dir'] = dir_ + if to_append is not None: + dict_.update(to_append) + return Event(dict_) + + +class ProcessEvent(_ProcessEvent): + """ + Process events objects, can be specialized via subclassing, thus its + behavior can be overriden: + + Note: you should not override __init__ in your subclass instead define + a my_init() method, this method will be called automatically from the + constructor of this class with its optionals parameters. + + 1. Provide specialized individual methods, e.g. process_IN_DELETE for + processing a precise type of event (e.g. IN_DELETE in this case). + 2. Or/and provide methods for processing events by 'family', e.g. + process_IN_CLOSE method will process both IN_CLOSE_WRITE and + IN_CLOSE_NOWRITE events (if process_IN_CLOSE_WRITE and + process_IN_CLOSE_NOWRITE aren't defined though). + 3. Or/and override process_default for catching and processing all + the remaining types of events. + """ + pevent = None + + def __init__(self, pevent=None, **kargs): + """ + Enable chaining of ProcessEvent instances. + + @param pevent: Optional callable object, will be called on event + processing (before self). + @type pevent: callable + @param kargs: This constructor is implemented as a template method + delegating its optionals keyworded arguments to the + method my_init(). + @type kargs: dict + """ + self.pevent = pevent + self.my_init(**kargs) + + def my_init(self, **kargs): + """ + This method is called from ProcessEvent.__init__(). This method is + empty here and must be redefined to be useful. In effect, if you + need to specifically initialize your subclass' instance then you + just have to override this method in your subclass. Then all the + keyworded arguments passed to ProcessEvent.__init__() will be + transmitted as parameters to this method. Beware you MUST pass + keyword arguments though. + + @param kargs: optional delegated arguments from __init__(). + @type kargs: dict + """ + pass + + def __call__(self, event): + stop_chaining = False + if self.pevent is not None: + # By default methods return None so we set as guideline + # that methods asking for stop chaining must explicitely + # return non None or non False values, otherwise the default + # behavior will be to accept chain call to the corresponding + # local method. + stop_chaining = self.pevent(event) + if not stop_chaining: + return _ProcessEvent.__call__(self, event) + + def nested_pevent(self): + return self.pevent + + def process_IN_Q_OVERFLOW(self, event): + """ + By default this method only reports warning messages, you can overredide + it by subclassing ProcessEvent and implement your own + process_IN_Q_OVERFLOW method. The actions you can take on receiving this + event is either to update the variable max_queued_events in order to + handle more simultaneous events or to modify your code in order to + accomplish a better filtering diminishing the number of raised events. + Because this method is defined, IN_Q_OVERFLOW will never get + transmitted as arguments to process_default calls. + + @param event: IN_Q_OVERFLOW event. + @type event: dict + """ + log.warning('Event queue overflowed.') + + def process_default(self, event): + """ + Default processing event method. By default does nothing. Subclass + ProcessEvent and redefine this method in order to modify its behavior. + + @param event: Event to be processed. Can be of any type of events but + IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW). + @type event: Event instance + """ + pass + + +class PrintAllEvents(ProcessEvent): + """ + Dummy class used to print events strings representations. For instance this + class is used from command line to print all received events to stdout. + """ + def my_init(self, out=None): + """ + @param out: Where events will be written. + @type out: Object providing a valid file object interface. + """ + if out is None: + out = sys.stdout + self._out = out + + def process_default(self, event): + """ + Writes event string representation to file object provided to + my_init(). + + @param event: Event to be processed. Can be of any type of events but + IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW). + @type event: Event instance + """ + self._out.write(str(event)) + self._out.write('\n') + self._out.flush() + + +class ChainIfTrue(ProcessEvent): + """ + Makes conditional chaining depending on the result of the nested + processing instance. + """ + def my_init(self, func): + """ + Method automatically called from base class constructor. + """ + self._func = func + + def process_default(self, event): + return not self._func(event) + + +class Stats(ProcessEvent): + """ + Compute and display trivial statistics about processed events. + """ + def my_init(self): + """ + Method automatically called from base class constructor. + """ + self._start_time = time.time() + self._stats = {} + self._stats_lock = threading.Lock() + + def process_default(self, event): + """ + Processes |event|. + """ + self._stats_lock.acquire() + try: + events = event.maskname.split('|') + for event_name in events: + count = self._stats.get(event_name, 0) + self._stats[event_name] = count + 1 + finally: + self._stats_lock.release() + + def _stats_copy(self): + self._stats_lock.acquire() + try: + return self._stats.copy() + finally: + self._stats_lock.release() + + def __repr__(self): + stats = self._stats_copy() + + elapsed = int(time.time() - self._start_time) + elapsed_str = '' + if elapsed < 60: + elapsed_str = str(elapsed) + 'sec' + elif 60 <= elapsed < 3600: + elapsed_str = '%dmn%dsec' % (elapsed / 60, elapsed % 60) + elif 3600 <= elapsed < 86400: + elapsed_str = '%dh%dmn' % (elapsed / 3600, (elapsed % 3600) / 60) + elif elapsed >= 86400: + elapsed_str = '%dd%dh' % (elapsed / 86400, (elapsed % 86400) / 3600) + stats['ElapsedTime'] = elapsed_str + + l = [] + for ev, value in sorted(stats.items(), key=lambda x: x[0]): + l.append(' %s=%s' % (output_format.field_name(ev), + output_format.field_value(value))) + s = '<%s%s >' % (output_format.class_name(self.__class__.__name__), + ''.join(l)) + return s + + def dump(self, filename): + """ + Dumps statistics. + + @param filename: filename where stats will be dumped, filename is + created and must not exist prior to this call. + @type filename: string + """ + flags = os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW|os.O_EXCL + fd = os.open(filename, flags, 0600) + os.write(fd, str(self)) + os.close(fd) + + def __str__(self, scale=45): + stats = self._stats_copy() + if not stats: + return '' + + m = max(stats.values()) + unity = float(scale) / m + fmt = '%%-26s%%-%ds%%s' % (len(output_format.field_value('@' * scale)) + + 1) + def func(x): + return fmt % (output_format.field_name(x[0]), + output_format.field_value('@' * int(x[1] * unity)), + output_format.simple('%d' % x[1], 'yellow')) + s = '\n'.join(map(func, sorted(stats.items(), key=lambda x: x[0]))) + return s + + +class NotifierError(PyinotifyError): + """ + Notifier Exception. Raised on Notifier error. + + """ + def __init__(self, err): + """ + @param err: Exception string's description. + @type err: string + """ + PyinotifyError.__init__(self, err) + + +class Notifier: + """ + Read notifications, process events. + + """ + def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, + threshold=0, timeout=None): + """ + Initialization. read_freq, threshold and timeout parameters are used + when looping. + + @param watch_manager: Watch Manager. + @type watch_manager: WatchManager instance + @param default_proc_fun: Default processing method. If None, a new + instance of PrintAllEvents will be assigned. + @type default_proc_fun: instance of ProcessEvent + @param read_freq: if read_freq == 0, events are read asap, + if read_freq is > 0, this thread sleeps + max(0, read_freq - timeout) seconds. But if + timeout is None it may be different because + poll is blocking waiting for something to read. + @type read_freq: int + @param threshold: File descriptor will be read only if the accumulated + size to read becomes >= threshold. If != 0, you likely + want to use it in combination with an appropriate + value for read_freq because without that you would + keep looping without really reading anything and that + until the amount of events to read is >= threshold. + At least with read_freq set you might sleep. + @type threshold: int + @param timeout: + https://docs.python.org/3/library/select.html#polling-objects + @type timeout: int + """ + # Watch Manager instance + self._watch_manager = watch_manager + # File descriptor + self._fd = self._watch_manager.get_fd() + # Poll object and registration + self._pollobj = select.poll() + self._pollobj.register(self._fd, select.POLLIN) + # This pipe is correctely initialized and used by ThreadedNotifier + self._pipe = (-1, -1) + # Event queue + self._eventq = deque() + # System processing functor, common to all events + self._sys_proc_fun = _SysProcessEvent(self._watch_manager, self) + # Default processing method + self._default_proc_fun = default_proc_fun + if default_proc_fun is None: + self._default_proc_fun = PrintAllEvents() + # Loop parameters + self._read_freq = read_freq + self._threshold = threshold + self._timeout = timeout + # Coalesce events option + self._coalesce = False + # set of str(raw_event), only used when coalesce option is True + self._eventset = set() + + def append_event(self, event): + """ + Append a raw event to the event queue. + + @param event: An event. + @type event: _RawEvent instance. + """ + self._eventq.append(event) + + def proc_fun(self): + return self._default_proc_fun + + def coalesce_events(self, coalesce=True): + """ + Coalescing events. Events are usually processed by batchs, their size + depend on various factors. Thus, before processing them, events received + from inotify are aggregated in a fifo queue. If this coalescing + option is enabled events are filtered based on their unicity, only + unique events are enqueued, doublons are discarded. An event is unique + when the combination of its fields (wd, mask, cookie, name) is unique + among events of a same batch. After a batch of events is processed any + events is accepted again. By default this option is disabled, you have + to explictly call this function to turn it on. + + @param coalesce: Optional new coalescing value. True by default. + @type coalesce: Bool + """ + self._coalesce = coalesce + if not coalesce: + self._eventset.clear() + + def check_events(self, timeout=None): + """ + Check for new events available to read, blocks up to timeout + milliseconds. + + @param timeout: If specified it overrides the corresponding instance + attribute _timeout. + @type timeout: int + + @return: New events to read. + @rtype: bool + """ + while True: + try: + # blocks up to 'timeout' milliseconds + if timeout is None: + timeout = self._timeout + ret = self._pollobj.poll(timeout) + except select.error, err: + if err[0] == errno.EINTR: + continue # interrupted, retry + else: + raise + else: + break + + if not ret or (self._pipe[0] == ret[0][0]): + return False + # only one fd is polled + return ret[0][1] & select.POLLIN + + def read_events(self): + """ + Read events from device, build _RawEvents, and enqueue them. + """ + buf_ = array.array('i', [0]) + # get event queue size + if fcntl.ioctl(self._fd, termios.FIONREAD, buf_, 1) == -1: + return + queue_size = buf_[0] + if queue_size < self._threshold: + log.debug('(fd: %d) %d bytes available to read but threshold is ' + 'fixed to %d bytes', self._fd, queue_size, + self._threshold) + return + + try: + # Read content from file + r = os.read(self._fd, queue_size) + except Exception, msg: + raise NotifierError(msg) + log.debug('Event queue size: %d', queue_size) + rsum = 0 # counter + while rsum < queue_size: + s_size = 16 + # Retrieve wd, mask, cookie and fname_len + wd, mask, cookie, fname_len = struct.unpack('iIII', + r[rsum:rsum+s_size]) + # Retrieve name + fname, = struct.unpack('%ds' % fname_len, + r[rsum + s_size:rsum + s_size + fname_len]) + rawevent = _RawEvent(wd, mask, cookie, fname) + if self._coalesce: + # Only enqueue new (unique) events. + raweventstr = str(rawevent) + if raweventstr not in self._eventset: + self._eventset.add(raweventstr) + self._eventq.append(rawevent) + else: + self._eventq.append(rawevent) + rsum += s_size + fname_len + + def process_events(self): + """ + Routine for processing events from queue by calling their + associated proccessing method (an instance of ProcessEvent). + It also does internal processings, to keep the system updated. + """ + while self._eventq: + raw_event = self._eventq.popleft() # pop next event + if self._watch_manager.ignore_events: + log.debug("Event ignored: %s" % repr(raw_event)) + continue + watch_ = self._watch_manager.get_watch(raw_event.wd) + if (watch_ is None) and not (raw_event.mask & IN_Q_OVERFLOW): + if not (raw_event.mask & IN_IGNORED): + # Not really sure how we ended up here, nor how we should + # handle these types of events and if it is appropriate to + # completly skip them (like we are doing here). + log.warning("Unable to retrieve Watch object associated to %s", + repr(raw_event)) + continue + revent = self._sys_proc_fun(raw_event) # system processings + if watch_ and watch_.proc_fun: + watch_.proc_fun(revent) # user processings + else: + self._default_proc_fun(revent) + self._sys_proc_fun.cleanup() # remove olds MOVED_* events records + if self._coalesce: + self._eventset.clear() + + def __daemonize(self, pid_file=None, stdin=os.devnull, stdout=os.devnull, + stderr=os.devnull): + """ + @param pid_file: file where the pid will be written. If pid_file=None + the pid is written to + /var/run/.pid, if pid_file=False + no pid_file is written. + @param stdin: + @param stdout: + @param stderr: files associated to common streams. + """ + if pid_file is None: + dirname = '/var/run/' + basename = os.path.basename(sys.argv[0]) or 'pyinotify' + pid_file = os.path.join(dirname, basename + '.pid') + + if pid_file != False and os.path.lexists(pid_file): + err = 'Cannot daemonize: pid file %s already exists.' % pid_file + raise NotifierError(err) + + def fork_daemon(): + # Adapted from Chad J. Schroeder's recipe + # @see http://code.activestate.com/recipes/278731/ + pid = os.fork() + if (pid == 0): + # parent 2 + os.setsid() + pid = os.fork() + if (pid == 0): + # child + os.chdir('/') + os.umask(022) + else: + # parent 2 + os._exit(0) + else: + # parent 1 + os._exit(0) + + fd_inp = os.open(stdin, os.O_RDONLY) + os.dup2(fd_inp, 0) + fd_out = os.open(stdout, os.O_WRONLY|os.O_CREAT, 0600) + os.dup2(fd_out, 1) + fd_err = os.open(stderr, os.O_WRONLY|os.O_CREAT, 0600) + os.dup2(fd_err, 2) + + # Detach task + fork_daemon() + + # Write pid + if pid_file != False: + flags = os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW|os.O_EXCL + fd_pid = os.open(pid_file, flags, 0600) + os.write(fd_pid, str(os.getpid()) + '\n') + os.close(fd_pid) + # Register unlink function + atexit.register(lambda : os.unlink(pid_file)) + + def _sleep(self, ref_time): + # Only consider sleeping if read_freq is > 0 + if self._read_freq > 0: + cur_time = time.time() + sleep_amount = self._read_freq - (cur_time - ref_time) + if sleep_amount > 0: + log.debug('Now sleeping %d seconds', sleep_amount) + time.sleep(sleep_amount) + + def loop(self, callback=None, daemonize=False, **args): + """ + Events are read only one time every min(read_freq, timeout) + seconds at best and only if the size to read is >= threshold. + After this method returns it must not be called again for the same + instance. + + @param callback: Functor called after each event processing iteration. + Expects to receive the notifier object (self) as first + parameter. If this function returns True the loop is + immediately terminated otherwise the loop method keeps + looping. + @type callback: callable object or function + @param daemonize: This thread is daemonized if set to True. + @type daemonize: boolean + @param args: Optional and relevant only if daemonize is True. Remaining + keyworded arguments are directly passed to daemonize see + __daemonize() method. If pid_file=None or is set to a + pathname the caller must ensure the file does not exist + before this method is called otherwise an exception + pyinotify.NotifierError will be raised. If pid_file=False + it is still daemonized but the pid is not written in any + file. + @type args: various + """ + if daemonize: + self.__daemonize(**args) + + # Read and process events forever + while 1: + try: + self.process_events() + if (callback is not None) and (callback(self) is True): + break + ref_time = time.time() + # check_events is blocking + if self.check_events(): + self._sleep(ref_time) + self.read_events() + except KeyboardInterrupt: + # Stop monitoring if sigint is caught (Control-C). + log.debug('Pyinotify stops monitoring.') + break + # Close internals + self.stop() + + def stop(self): + """ + Close inotify's instance (close its file descriptor). + It destroys all existing watches, pending events,... + This method is automatically called at the end of loop(). + """ + self._pollobj.unregister(self._fd) + os.close(self._fd) + self._sys_proc_fun = None + + +class ThreadedNotifier(threading.Thread, Notifier): + """ + This notifier inherits from threading.Thread for instanciating a separate + thread, and also inherits from Notifier, because it is a threaded notifier. + + Note that every functionality provided by this class is also provided + through Notifier class. Moreover Notifier should be considered first because + it is not threaded and could be easily daemonized. + """ + def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, + threshold=0, timeout=None): + """ + Initialization, initialize base classes. read_freq, threshold and + timeout parameters are used when looping. + + @param watch_manager: Watch Manager. + @type watch_manager: WatchManager instance + @param default_proc_fun: Default processing method. See base class. + @type default_proc_fun: instance of ProcessEvent + @param read_freq: if read_freq == 0, events are read asap, + if read_freq is > 0, this thread sleeps + max(0, read_freq - timeout) seconds. + @type read_freq: int + @param threshold: File descriptor will be read only if the accumulated + size to read becomes >= threshold. If != 0, you likely + want to use it in combination with an appropriate + value set for read_freq because without that you would + keep looping without really reading anything and that + until the amount of events to read is >= threshold. At + least with read_freq you might sleep. + @type threshold: int + @param timeout: + https://docs.python.org/3/library/select.html#polling-objects + @type timeout: int + """ + # Init threading base class + threading.Thread.__init__(self) + # Stop condition + self._stop_event = threading.Event() + # Init Notifier base class + Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, + threshold, timeout) + # Create a new pipe used for thread termination + self._pipe = os.pipe() + self._pollobj.register(self._pipe[0], select.POLLIN) + + def stop(self): + """ + Stop notifier's loop. Stop notification. Join the thread. + """ + self._stop_event.set() + os.write(self._pipe[1], 'stop') + threading.Thread.join(self) + Notifier.stop(self) + self._pollobj.unregister(self._pipe[0]) + os.close(self._pipe[0]) + os.close(self._pipe[1]) + + def loop(self): + """ + Thread's main loop. Don't meant to be called by user directly. + Call inherited start() method instead. + + Events are read only once time every min(read_freq, timeout) + seconds at best and only if the size of events to read is >= threshold. + """ + # When the loop must be terminated .stop() is called, 'stop' + # is written to pipe fd so poll() returns and .check_events() + # returns False which make evaluate the While's stop condition + # ._stop_event.isSet() wich put an end to the thread's execution. + while not self._stop_event.isSet(): + self.process_events() + ref_time = time.time() + if self.check_events(): + self._sleep(ref_time) + self.read_events() + + def run(self): + """ + Start thread's loop: read and process events until the method + stop() is called. + Never call this method directly, instead call the start() method + inherited from threading.Thread, which then will call run() in + its turn. + """ + self.loop() + + +class AsyncNotifier(asyncore.file_dispatcher, Notifier): + """ + This notifier inherits from asyncore.file_dispatcher in order to be able to + use pyinotify along with the asyncore framework. + + """ + def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, + threshold=0, timeout=None, channel_map=None): + """ + Initializes the async notifier. The only additional parameter is + 'channel_map' which is the optional asyncore private map. See + Notifier class for the meaning of the others parameters. + + """ + Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, + threshold, timeout) + asyncore.file_dispatcher.__init__(self, self._fd, channel_map) + + def handle_read(self): + """ + When asyncore tells us we can read from the fd, we proceed processing + events. This method can be overridden for handling a notification + differently. + + """ + self.read_events() + self.process_events() + + +class TornadoAsyncNotifier(Notifier): + """ + Tornado ioloop adapter. + + """ + def __init__(self, watch_manager, ioloop, callback=None, + default_proc_fun=None, read_freq=0, threshold=0, timeout=None, + channel_map=None): + """ + Note that if later you must call ioloop.close() be sure to let the + default parameter to all_fds=False. + + See example tornado_notifier.py for an example using this notifier. + + @param ioloop: Tornado's IO loop. + @type ioloop: tornado.ioloop.IOLoop instance. + @param callback: Functor called at the end of each call to handle_read + (IOLoop's read handler). Expects to receive the + notifier object (self) as single parameter. + @type callback: callable object or function + """ + self.io_loop = ioloop + self.handle_read_callback = callback + Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, + threshold, timeout) + ioloop.add_handler(self._fd, self.handle_read, ioloop.READ) + + def stop(self): + self.io_loop.remove_handler(self._fd) + Notifier.stop(self) + + def handle_read(self, *args, **kwargs): + """ + See comment in AsyncNotifier. + + """ + self.read_events() + self.process_events() + if self.handle_read_callback is not None: + self.handle_read_callback(self) + + +class AsyncioNotifier(Notifier): + """ + + asyncio/trollius event loop adapter. + + """ + def __init__(self, watch_manager, loop, callback=None, + default_proc_fun=None, read_freq=0, threshold=0, timeout=None): + """ + + See examples/asyncio_notifier.py for an example usage. + + @param loop: asyncio or trollius event loop instance. + @type loop: asyncio.BaseEventLoop or trollius.BaseEventLoop instance. + @param callback: Functor called at the end of each call to handle_read. + Expects to receive the notifier object (self) as + single parameter. + @type callback: callable object or function + + """ + self.loop = loop + self.handle_read_callback = callback + Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, + threshold, timeout) + loop.add_reader(self._fd, self.handle_read) + + def stop(self): + self.loop.remove_reader(self._fd) + Notifier.stop(self) + + def handle_read(self, *args, **kwargs): + self.read_events() + self.process_events() + if self.handle_read_callback is not None: + self.handle_read_callback(self) + + +class Watch: + """ + Represent a watch, i.e. a file or directory being watched. + + """ + __slots__ = ('wd', 'path', 'mask', 'proc_fun', 'auto_add', + 'exclude_filter', 'dir') + + def __init__(self, wd, path, mask, proc_fun, auto_add, exclude_filter): + """ + Initializations. + + @param wd: Watch descriptor. + @type wd: int + @param path: Path of the file or directory being watched. + @type path: str + @param mask: Mask. + @type mask: int + @param proc_fun: Processing callable object. + @type proc_fun: + @param auto_add: Automatically add watches on new directories. + @type auto_add: bool + @param exclude_filter: Boolean function, used to exclude new + directories from being automatically watched. + See WatchManager.__init__ + @type exclude_filter: callable object + """ + self.wd = wd + self.path = path + self.mask = mask + self.proc_fun = proc_fun + self.auto_add = auto_add + self.exclude_filter = exclude_filter + self.dir = os.path.isdir(self.path) + + def __repr__(self): + """ + @return: String representation. + @rtype: str + """ + s = ' '.join(['%s%s%s' % (output_format.field_name(attr), + output_format.punctuation('='), + output_format.field_value(getattr(self, + attr))) \ + for attr in self.__slots__ if not attr.startswith('_')]) + + s = '%s%s %s %s' % (output_format.punctuation('<'), + output_format.class_name(self.__class__.__name__), + s, + output_format.punctuation('>')) + return s + + +class ExcludeFilter: + """ + ExcludeFilter is an exclusion filter. + + """ + def __init__(self, arg_lst): + """ + Examples: + ef1 = ExcludeFilter(["/etc/rc.*", "/etc/hostname"]) + ef2 = ExcludeFilter("/my/path/exclude.lst") + Where exclude.lst contains: + /etc/rc.* + /etc/hostname + + Note: it is not possible to exclude a file if its encapsulating + directory is itself watched. See this issue for more details + https://github.com/seb-m/pyinotify/issues/31 + + @param arg_lst: is either a list of patterns or a filename from which + patterns will be loaded. + @type arg_lst: list of str or str + """ + if isinstance(arg_lst, str): + lst = self._load_patterns_from_file(arg_lst) + elif isinstance(arg_lst, list): + lst = arg_lst + else: + raise TypeError + + self._lregex = [] + for regex in lst: + self._lregex.append(re.compile(regex, re.UNICODE)) + + def _load_patterns_from_file(self, filename): + lst = [] + file_obj = file(filename, 'r') + try: + for line in file_obj.readlines(): + # Trim leading an trailing whitespaces + pattern = line.strip() + if not pattern or pattern.startswith('#'): + continue + lst.append(pattern) + finally: + file_obj.close() + return lst + + def _match(self, regex, path): + return regex.match(path) is not None + + def __call__(self, path): + """ + @param path: Path to match against provided regexps. + @type path: str + @return: Return True if path has been matched and should + be excluded, False otherwise. + @rtype: bool + """ + for regex in self._lregex: + if self._match(regex, path): + return True + return False + + +class WatchManagerError(Exception): + """ + WatchManager Exception. Raised on error encountered on watches + operations. + + """ + def __init__(self, msg, wmd): + """ + @param msg: Exception string's description. + @type msg: string + @param wmd: This dictionary contains the wd assigned to paths of the + same call for which watches were successfully added. + @type wmd: dict + """ + self.wmd = wmd + Exception.__init__(self, msg) + + +class WatchManager: + """ + Provide operations for watching files and directories. Its internal + dictionary is used to reference watched items. When used inside + threaded code, one must instanciate as many WatchManager instances as + there are ThreadedNotifier instances. + + """ + def __init__(self, exclude_filter=lambda path: False): + """ + Initialization: init inotify, init watch manager dictionary. + Raise OSError if initialization fails, raise InotifyBindingNotFoundError + if no inotify binding was found (through ctypes or from direct access to + syscalls). + + @param exclude_filter: boolean function, returns True if current + path must be excluded from being watched. + Convenient for providing a common exclusion + filter for every call to add_watch. + @type exclude_filter: callable object + """ + self._ignore_events = False + self._exclude_filter = exclude_filter + self._wmd = {} # watch dict key: watch descriptor, value: watch + + self._inotify_wrapper = INotifyWrapper.create() + if self._inotify_wrapper is None: + raise InotifyBindingNotFoundError() + + self._fd = self._inotify_wrapper.inotify_init() # file descriptor + if self._fd < 0: + err = 'Cannot initialize new instance of inotify, %s' + raise OSError(err % self._inotify_wrapper.str_errno()) + + def close(self): + """ + Close inotify's file descriptor, this action will also automatically + remove (i.e. stop watching) all its associated watch descriptors. + After a call to this method the WatchManager's instance become useless + and cannot be reused, a new instance must then be instanciated. It + makes sense to call this method in few situations for instance if + several independant WatchManager must be instanciated or if all watches + must be removed and no other watches need to be added. + """ + os.close(self._fd) + + def get_fd(self): + """ + Return assigned inotify's file descriptor. + + @return: File descriptor. + @rtype: int + """ + return self._fd + + def get_watch(self, wd): + """ + Get watch from provided watch descriptor wd. + + @param wd: Watch descriptor. + @type wd: int + """ + return self._wmd.get(wd) + + def del_watch(self, wd): + """ + Remove watch entry associated to watch descriptor wd. + + @param wd: Watch descriptor. + @type wd: int + """ + try: + del self._wmd[wd] + except KeyError, err: + log.error('Cannot delete unknown watch descriptor %s' % str(err)) + + @property + def watches(self): + """ + Get a reference on the internal watch manager dictionary. + + @return: Internal watch manager dictionary. + @rtype: dict + """ + return self._wmd + + def __format_path(self, path): + """ + Format path to its internal (stored in watch manager) representation. + """ + # Unicode strings are converted back to strings, because it seems + # that inotify_add_watch from ctypes does not work well when + # it receives an ctypes.create_unicode_buffer instance as argument. + # Therefore even wd are indexed with bytes string and not with + # unicode paths. + if isinstance(path, unicode): + path = path.encode(sys.getfilesystemencoding()) + return os.path.normpath(path) + + def __add_watch(self, path, mask, proc_fun, auto_add, exclude_filter): + """ + Add a watch on path, build a Watch object and insert it in the + watch manager dictionary. Return the wd value. + """ + path = self.__format_path(path) + if auto_add and not mask & IN_CREATE: + mask |= IN_CREATE + wd = self._inotify_wrapper.inotify_add_watch(self._fd, path, mask) + if wd < 0: + return wd + watch = Watch(wd=wd, path=path, mask=mask, proc_fun=proc_fun, + auto_add=auto_add, exclude_filter=exclude_filter) + self._wmd[wd] = watch + log.debug('New %s', watch) + return wd + + def __glob(self, path, do_glob): + if do_glob: + return glob(path) + else: + return [path] + + def add_watch(self, path, mask, proc_fun=None, rec=False, + auto_add=False, do_glob=False, quiet=True, + exclude_filter=None): + """ + Add watch(s) on the provided |path|(s) with associated |mask| flag + value and optionally with a processing |proc_fun| function and + recursive flag |rec| set to True. + Ideally |path| components should not be unicode objects. Note that + although unicode paths are accepted there are converted to byte + strings before a watch is put on that path. The encoding used for + converting the unicode object is given by sys.getfilesystemencoding(). + If |path| si already watched it is ignored, but if it is called with + option rec=True a watch is put on each one of its not-watched + subdirectory. + + @param path: Path to watch, the path can either be a file or a + directory. Also accepts a sequence (list) of paths. + @type path: string or list of strings + @param mask: Bitmask of events. + @type mask: int + @param proc_fun: Processing object. + @type proc_fun: function or ProcessEvent instance or instance of + one of its subclasses or callable object. + @param rec: Recursively add watches from path on all its + subdirectories, set to False by default (doesn't + follows symlinks in any case). + @type rec: bool + @param auto_add: Automatically add watches on newly created + directories in watched parent |path| directory. + If |auto_add| is True, IN_CREATE is ored with |mask| + when the watch is added. + @type auto_add: bool + @param do_glob: Do globbing on pathname (see standard globbing + module for more informations). + @type do_glob: bool + @param quiet: if False raises a WatchManagerError exception on + error. See example not_quiet.py. + @type quiet: bool + @param exclude_filter: predicate (boolean function), which returns + True if the current path must be excluded + from being watched. This argument has + precedence over exclude_filter passed to + the class' constructor. + @type exclude_filter: callable object + @return: dict of paths associated to watch descriptors. A wd value + is positive if the watch was added sucessfully, + otherwise the value is negative. If the path was invalid + or was already watched it is not included into this returned + dictionary. + @rtype: dict of {str: int} + """ + ret_ = {} # return {path: wd, ...} + + if exclude_filter is None: + exclude_filter = self._exclude_filter + + # normalize args as list elements + for npath in self.__format_param(path): + # unix pathname pattern expansion + for apath in self.__glob(npath, do_glob): + # recursively list subdirs according to rec param + for rpath in self.__walk_rec(apath, rec): + if not exclude_filter(rpath): + wd = ret_[rpath] = self.__add_watch(rpath, mask, + proc_fun, + auto_add, + exclude_filter) + if wd < 0: + err = ('add_watch: cannot watch %s WD=%d, %s' % \ + (rpath, wd, + self._inotify_wrapper.str_errno())) + if quiet: + log.error(err) + else: + raise WatchManagerError(err, ret_) + else: + # Let's say -2 means 'explicitely excluded + # from watching'. + ret_[rpath] = -2 + return ret_ + + def __get_sub_rec(self, lpath): + """ + Get every wd from self._wmd if its path is under the path of + one (at least) of those in lpath. Doesn't follow symlinks. + + @param lpath: list of watch descriptor + @type lpath: list of int + @return: list of watch descriptor + @rtype: list of int + """ + for d in lpath: + root = self.get_path(d) + if root is not None: + # always keep root + yield d + else: + # if invalid + continue + + # nothing else to expect + if not os.path.isdir(root): + continue + + # normalization + root = os.path.normpath(root) + # recursion + lend = len(root) + for iwd in self._wmd.items(): + cur = iwd[1].path + pref = os.path.commonprefix([root, cur]) + if root == os.sep or (len(pref) == lend and \ + len(cur) > lend and \ + cur[lend] == os.sep): + yield iwd[1].wd + + def update_watch(self, wd, mask=None, proc_fun=None, rec=False, + auto_add=False, quiet=True): + """ + Update existing watch descriptors |wd|. The |mask| value, the + processing object |proc_fun|, the recursive param |rec| and the + |auto_add| and |quiet| flags can all be updated. + + @param wd: Watch Descriptor to update. Also accepts a list of + watch descriptors. + @type wd: int or list of int + @param mask: Optional new bitmask of events. + @type mask: int + @param proc_fun: Optional new processing function. + @type proc_fun: function or ProcessEvent instance or instance of + one of its subclasses or callable object. + @param rec: Optionally adds watches recursively on all + subdirectories contained into |wd| directory. + @type rec: bool + @param auto_add: Automatically adds watches on newly created + directories in the watch's path corresponding to |wd|. + If |auto_add| is True, IN_CREATE is ored with |mask| + when the watch is updated. + @type auto_add: bool + @param quiet: If False raises a WatchManagerError exception on + error. See example not_quiet.py + @type quiet: bool + @return: dict of watch descriptors associated to booleans values. + True if the corresponding wd has been successfully + updated, False otherwise. + @rtype: dict of {int: bool} + """ + lwd = self.__format_param(wd) + if rec: + lwd = self.__get_sub_rec(lwd) + + ret_ = {} # return {wd: bool, ...} + for awd in lwd: + apath = self.get_path(awd) + if not apath or awd < 0: + err = 'update_watch: invalid WD=%d' % awd + if quiet: + log.error(err) + continue + raise WatchManagerError(err, ret_) + + if mask: + wd_ = self._inotify_wrapper.inotify_add_watch(self._fd, apath, + mask) + if wd_ < 0: + ret_[awd] = False + err = ('update_watch: cannot update %s WD=%d, %s' % \ + (apath, wd_, self._inotify_wrapper.str_errno())) + if quiet: + log.error(err) + continue + raise WatchManagerError(err, ret_) + + assert(awd == wd_) + + if proc_fun or auto_add: + watch_ = self._wmd[awd] + + if proc_fun: + watch_.proc_fun = proc_fun + + if auto_add: + watch_.auto_add = auto_add + + ret_[awd] = True + log.debug('Updated watch - %s', self._wmd[awd]) + return ret_ + + def __format_param(self, param): + """ + @param param: Parameter. + @type param: string or int + @return: wrap param. + @rtype: list of type(param) + """ + if isinstance(param, list): + for p_ in param: + yield p_ + else: + yield param + + def get_wd(self, path): + """ + Returns the watch descriptor associated to path. This method + presents a prohibitive cost, always prefer to keep the WD + returned by add_watch(). If the path is unknown it returns None. + + @param path: Path. + @type path: str + @return: WD or None. + @rtype: int or None + """ + path = self.__format_path(path) + for iwd in self._wmd.items(): + if iwd[1].path == path: + return iwd[0] + + def get_path(self, wd): + """ + Returns the path associated to WD, if WD is unknown it returns None. + + @param wd: Watch descriptor. + @type wd: int + @return: Path or None. + @rtype: string or None + """ + watch_ = self._wmd.get(wd) + if watch_ is not None: + return watch_.path + + def __walk_rec(self, top, rec): + """ + Yields each subdirectories of top, doesn't follow symlinks. + If rec is false, only yield top. + + @param top: root directory. + @type top: string + @param rec: recursive flag. + @type rec: bool + @return: path of one subdirectory. + @rtype: string + """ + if not rec or os.path.islink(top) or not os.path.isdir(top): + yield top + else: + for root, dirs, files in os.walk(top): + yield root + + def rm_watch(self, wd, rec=False, quiet=True): + """ + Removes watch(s). + + @param wd: Watch Descriptor of the file or directory to unwatch. + Also accepts a list of WDs. + @type wd: int or list of int. + @param rec: Recursively removes watches on every already watched + subdirectories and subfiles. + @type rec: bool + @param quiet: If False raises a WatchManagerError exception on + error. See example not_quiet.py + @type quiet: bool + @return: dict of watch descriptors associated to booleans values. + True if the corresponding wd has been successfully + removed, False otherwise. + @rtype: dict of {int: bool} + """ + lwd = self.__format_param(wd) + if rec: + lwd = self.__get_sub_rec(lwd) + + ret_ = {} # return {wd: bool, ...} + for awd in lwd: + # remove watch + wd_ = self._inotify_wrapper.inotify_rm_watch(self._fd, awd) + if wd_ < 0: + ret_[awd] = False + err = ('rm_watch: cannot remove WD=%d, %s' % \ + (awd, self._inotify_wrapper.str_errno())) + if quiet: + log.error(err) + continue + raise WatchManagerError(err, ret_) + + # Remove watch from our dictionary + if awd in self._wmd: + del self._wmd[awd] + ret_[awd] = True + log.debug('Watch WD=%d (%s) removed', awd, self.get_path(awd)) + return ret_ + + + def watch_transient_file(self, filename, mask, proc_class): + """ + Watch a transient file, which will be created and deleted frequently + over time (e.g. pid file). + + @attention: Currently under the call to this function it is not + possible to correctly watch the events triggered into the same + base directory than the directory where is located this watched + transient file. For instance it would be wrong to make these + two successive calls: wm.watch_transient_file('/var/run/foo.pid', ...) + and wm.add_watch('/var/run/', ...) + + @param filename: Filename. + @type filename: string + @param mask: Bitmask of events, should contain IN_CREATE and IN_DELETE. + @type mask: int + @param proc_class: ProcessEvent (or of one of its subclass), beware of + accepting a ProcessEvent's instance as argument into + __init__, see transient_file.py example for more + details. + @type proc_class: ProcessEvent's instance or of one of its subclasses. + @return: Same as add_watch(). + @rtype: Same as add_watch(). + """ + dirname = os.path.dirname(filename) + if dirname == '': + return {} # Maintains coherence with add_watch() + basename = os.path.basename(filename) + # Assuming we are watching at least for IN_CREATE and IN_DELETE + mask |= IN_CREATE | IN_DELETE + + def cmp_name(event): + if getattr(event, 'name') is None: + return False + return basename == event.name + return self.add_watch(dirname, mask, + proc_fun=proc_class(ChainIfTrue(func=cmp_name)), + rec=False, + auto_add=False, do_glob=False, + exclude_filter=lambda path: False) + + def get_ignore_events(self): + return self._ignore_events + + def set_ignore_events(self, nval): + self._ignore_events = nval + + ignore_events = property(get_ignore_events, set_ignore_events, + "Make watch manager ignoring new events.") + + + +class RawOutputFormat: + """ + Format string representations. + """ + def __init__(self, format=None): + self.format = format or {} + + def simple(self, s, attribute): + if not isinstance(s, str): + s = str(s) + return (self.format.get(attribute, '') + s + + self.format.get('normal', '')) + + def punctuation(self, s): + """Punctuation color.""" + return self.simple(s, 'normal') + + def field_value(self, s): + """Field value color.""" + return self.simple(s, 'purple') + + def field_name(self, s): + """Field name color.""" + return self.simple(s, 'blue') + + def class_name(self, s): + """Class name color.""" + return self.format.get('red', '') + self.simple(s, 'bold') + +output_format = RawOutputFormat() + +class ColoredOutputFormat(RawOutputFormat): + """ + Format colored string representations. + """ + def __init__(self): + f = {'normal': '\033[0m', + 'black': '\033[30m', + 'red': '\033[31m', + 'green': '\033[32m', + 'yellow': '\033[33m', + 'blue': '\033[34m', + 'purple': '\033[35m', + 'cyan': '\033[36m', + 'bold': '\033[1m', + 'uline': '\033[4m', + 'blink': '\033[5m', + 'invert': '\033[7m'} + RawOutputFormat.__init__(self, f) + + +def compatibility_mode(): + """ + Use this function to turn on the compatibility mode. The compatibility + mode is used to improve compatibility with Pyinotify 0.7.1 (or older) + programs. The compatibility mode provides additional variables 'is_dir', + 'event_name', 'EventsCodes.IN_*' and 'EventsCodes.ALL_EVENTS' as + Pyinotify 0.7.1 provided. Do not call this function from new programs!! + Especially if there are developped for Pyinotify >= 0.8.x. + """ + setattr(EventsCodes, 'ALL_EVENTS', ALL_EVENTS) + for evname in globals(): + if evname.startswith('IN_'): + setattr(EventsCodes, evname, globals()[evname]) + global COMPATIBILITY_MODE + COMPATIBILITY_MODE = True + + +def command_line(): + """ + By default the watched path is '/tmp' and all types of events are + monitored. Events monitoring serves forever, type c^c to stop it. + """ + from optparse import OptionParser + + usage = "usage: %prog [options] [path1] [path2] [pathn]" + + parser = OptionParser(usage=usage) + parser.add_option("-v", "--verbose", action="store_true", + dest="verbose", help="Verbose mode") + parser.add_option("-r", "--recursive", action="store_true", + dest="recursive", + help="Add watches recursively on paths") + parser.add_option("-a", "--auto_add", action="store_true", + dest="auto_add", + help="Automatically add watches on new directories") + parser.add_option("-g", "--glob", action="store_true", + dest="glob", + help="Treat paths as globs") + parser.add_option("-e", "--events-list", metavar="EVENT[,...]", + dest="events_list", + help=("A comma-separated list of events to watch for - " + "see the documentation for valid options (defaults" + " to everything)")) + parser.add_option("-s", "--stats", action="store_true", + dest="stats", + help="Display dummy statistics") + parser.add_option("-V", "--version", action="store_true", + dest="version", help="Pyinotify version") + parser.add_option("-f", "--raw-format", action="store_true", + dest="raw_format", + help="Disable enhanced output format.") + parser.add_option("-c", "--command", action="store", + dest="command", + help="Shell command to run upon event") + + (options, args) = parser.parse_args() + + if options.verbose: + log.setLevel(10) + + if options.version: + print(__version__) + + if not options.raw_format: + global output_format + output_format = ColoredOutputFormat() + + if len(args) < 1: + path = '/tmp' # default watched path + else: + path = args + + # watch manager instance + wm = WatchManager() + # notifier instance and init + if options.stats: + notifier = Notifier(wm, default_proc_fun=Stats(), read_freq=5) + else: + notifier = Notifier(wm, default_proc_fun=PrintAllEvents()) + + # What mask to apply + mask = 0 + if options.events_list: + events_list = options.events_list.split(',') + for ev in events_list: + evcode = EventsCodes.ALL_FLAGS.get(ev, 0) + if evcode: + mask |= evcode + else: + parser.error("The event '%s' specified with option -e" + " is not valid" % ev) + else: + mask = ALL_EVENTS + + # stats + cb_fun = None + if options.stats: + def cb(s): + sys.stdout.write(repr(s.proc_fun())) + sys.stdout.write('\n') + sys.stdout.write(str(s.proc_fun())) + sys.stdout.write('\n') + sys.stdout.flush() + cb_fun = cb + + # External command + if options.command: + def cb(s): + subprocess.Popen(options.command, shell=True) + cb_fun = cb + + log.debug('Start monitoring %s, (press c^c to halt pyinotify)' % path) + + wm.add_watch(path, mask, rec=options.recursive, auto_add=options.auto_add, do_glob=options.glob) + # Loop forever (until sigint signal get caught) + notifier.loop(callback=cb_fun) + + +if __name__ == '__main__': + command_line() -- cgit v1.2.3-54-g00ecf