123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346 |
- #
- # pyinotify.py - python interface to inotify
- # Copyright (c) 2005-2015 Sebastien Martini <seb@dbzteam.org>
- #
- # SPDX-License-Identifier: MIT
- #
- """
- 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
- """
- PyinotifyError.__init__(self,
- ('Python %s is unsupported, requires '
- 'at least Python 3.0') % version)
- # Check Python version
- import sys
- if sys.version_info < (3, 0):
- 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 glob
- import locale
- import subprocess
- try:
- from functools import reduce
- except ImportError:
- pass # Will fail on Python 2.4 which has reduce() builtin anyway.
- 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.6"
- # 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():
- """
- Factory method instanciating and returning the right wrapper.
- """
- # 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 as 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 as 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 as 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.
- self._libc = ctypes.CDLL(libc_name, use_errno=True)
- self._get_errno_func = ctypes.get_errno
- # 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):
- assert self._get_errno_func
- return self._get_errno_func()
- 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
- # Encodes path to a bytes string. This conversion seems required because
- # ctypes.create_string_buffer seems to manipulate bytes internally.
- # Moreover it seems that inotify_add_watch does not work very well when
- # it receives an ctypes.create_unicode_buffer instance as argument.
- pathname = pathname.encode(sys.getfilesystemencoding())
- 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)
- # 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 ProcINotify:
- """
- Access (read, write) inotify's variables through /proc/sys/. Note that
- 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
- """
- def __init__(self, attr):
- self._base = "/proc/sys/fs/inotify"
- self._attr = attr
- def get_val(self):
- """
- Gets attribute's value.
- @return: stored value.
- @rtype: int
- @raise IOError: if corresponding file in /proc/sys cannot be read.
- """
- with open(os.path.join(self._base, self._attr), 'r') as file_obj:
- return int(file_obj.readline())
- def set_val(self, nval):
- """
- Sets new attribute's value.
- @param nval: replaces current value by nval.
- @type nval: int
- @raise IOError: if corresponding file in /proc/sys cannot be written.
- """
- with open(os.path.join(self._base, self._attr), 'w') as file_obj:
- file_obj.write(str(nval) + '\n')
- value = property(get_val, set_val)
- def __repr__(self):
- return '<%s=%d>' % (self._attr, self.get_val())
- # Inotify's variables
- #
- # Note: may raise IOError if the corresponding value in /proc/sys
- # cannot be accessed.
- #
- # Examples:
- # - 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] = ProcINotify(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, str) 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 as 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 list(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 as err:
- msg = "process_IN_CREATE, invalid directory: %s"
- log.debug(msg % 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, 0o0600)
- os.write(fd, bytes(self.__str__(), locale.getpreferredencoding()))
- os.close(fd)
- def __str__(self, scale=45):
- stats = self._stats_copy()
- if not stats:
- return ''
- m = max(stats.values())
- unity = 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 / 1000)) 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: see read_freq above. If provided, it must be set in
- milliseconds. See
- https://docs.python.org/3/library/select.html#select.poll.poll
- @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. timeout must be sepcified in
- milliseconds.
- @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 as err:
- if err.args[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 as 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
- bname, = struct.unpack('%ds' % fname_len,
- r[rsum + s_size:rsum + s_size + fname_len])
- # FIXME: should we explictly call sys.getdefaultencoding() here ??
- uname = bname.decode()
- rawevent = _RawEvent(wd, mask, cookie, uname)
- 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):
- """
- pid_file: file where the pid will be written. If pid_file=None the pid
- is written to /var/run/<sys.argv[0]|pyinotify>.pid, if
- pid_file=False no pid_file is written.
- stdin, stdout, 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 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(0o022)
- 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, 0o0600)
- os.dup2(fd_out, 1)
- fd_err = os.open(stderr, os.O_WRONLY|os.O_CREAT, 0o0600)
- os.dup2(fd_err, 2)
- # Detach task
- fork_daemon()
- # Write pid
- if pid_file:
- flags = os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW|os.O_EXCL
- fd_pid = os.open(pid_file, flags, 0o0600)
- os.write(fd_pid, bytes(str(os.getpid()) + '\n',
- locale.getpreferredencoding()))
- 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().
- Afterward it is invalid to access this instance.
- """
- if self._fd is not None:
- self._pollobj.unregister(self._fd)
- os.close(self._fd)
- self._fd = None
- 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 / 1000)) 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: see read_freq above. If provided, it must be set in
- milliseconds. See
- https://docs.python.org/3/library/select.html#select.poll.poll
- @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], b'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 = []
- with open(filename, 'r') as file_obj:
- for line in file_obj.readlines():
- # Trim leading an trailing whitespaces
- pattern = line.strip()
- if not pattern or pattern.startswith('#'):
- continue
- lst.append(pattern)
- 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 as 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.
- """
- # path must be a unicode string (str) and is just normalized.
- 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)
- # wd are _always_ indexed with their original unicode paths in wmd.
- self._wmd[wd] = watch
- log.debug('New %s', watch)
- return wd
- def __glob(self, path, do_glob):
- if do_glob:
- return glob.iglob(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.
- All |path| components _must_ be str (i.e. unicode) objects.
- If |path| is 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):
- # Require that path be a unicode string
- if not isinstance(npath, str):
- ret_[path] = -3
- continue
- # 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()
|