config/sysinv/sysinv/sysinv/sysinv/common/inotify.py

275 lines
12 KiB
Python

# Based on inotify_simple: https://github.com/chrisjbillington/inotify_simple
# Licensed under the BSD 2-Clause "Simplified" License
#
# Copyright (c) 2016, Chris Billington
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided wi6h the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
import errno
import os
from collections import namedtuple
from ctypes import CDLL
from ctypes import get_errno
from ctypes import c_int
from ctypes.util import find_library
from enum import IntEnum
from errno import EINTR
from fcntl import ioctl
from io import FileIO
from os import fsencode
from os import fsdecode
from select import select
from struct import calcsize
from struct import unpack_from
from termios import FIONREAD
from time import sleep
__version__ = '1.3.5'
__all__ = ['Event', 'INotify', 'flags', 'masks', 'parse_events']
_libc = None
def _libc_call(function, *args):
"""Wrapper which raises errors and retries on EINTR."""
while True:
rc = function(*args)
if rc != -1:
return rc
err = get_errno()
if err != EINTR:
raise OSError(errno, os.strerror(errno))
#: A ``namedtuple`` (wd, mask, cookie, name) for an inotify event. The
#: :attr:`~inotify_simple.Event.name` field is a ``str`` decoded with
#: ``os.fsdecode()``.
Event = namedtuple('Event', ['wd', 'mask', 'cookie', 'name'])
_EVENT_FMT = 'iIII'
_EVENT_SIZE = calcsize(_EVENT_FMT)
class INotify(FileIO):
#: The inotify file descriptor returned by ``inotify_init()``. You are
#: free to use it directly with ``os.read`` if you'd prefer not to call
#: :func:`~inotify_simple.INotify.read` for some reason. Also available as
#: :func:`~inotify_simple.INotify.fileno`
fd = property(FileIO.fileno)
def __init__(self, inheritable=False, nonblocking=False):
"""File-like object wrapping ``inotify_init1()``. Raises ``OSError`` on failure.
:func:`~inotify_simple.INotify.close` should be called when no longer needed.
Can be used as a context manager to ensure it is closed, and can be used
directly by functions expecting a file-like object, such as ``select``, or with
functions expecting a file descriptor via
:func:`~inotify_simple.INotify.fileno`.
Args:
inheritable (bool): whether the inotify file descriptor will be inherited by
child processes. The default,``False``, corresponds to passing the
``IN_CLOEXEC`` flag to ``inotify_init1()``. Setting this flag when
opening filedescriptors is the default behaviour of Python standard
library functions since PEP 446. On Python < 3.3, the file descriptor
will be inheritable and this argument has no effect, one must instead
use fcntl to set FD_CLOEXEC to make it non-inheritable.
nonblocking (bool): whether to open the inotify file descriptor in
nonblocking mode, corresponding to passing the ``IN_NONBLOCK`` flag to
``inotify_init1()``. This does not affect the normal behaviour of
:func:`~inotify_simple.INotify.read`, which uses ``poll()`` to control
blocking behaviour according to the given timeout, but will cause other
reads of the file descriptor (for example if the application reads data
manually with ``os.read(fd)``) to raise ``BlockingIOError`` if no data
is available."""
try:
libc_so = find_library('c')
except RuntimeError: # Python on Synology NASs raises a RuntimeError
libc_so = None
global _libc
_libc = _libc or CDLL(libc_so or 'libc.so.6', use_errno=True)
O_CLOEXEC = getattr(os, 'O_CLOEXEC', 0) # Only defined in Python 3.3+
flags = (not inheritable) * O_CLOEXEC | bool(nonblocking) * os.O_NONBLOCK
FileIO.__init__(self, _libc_call(_libc.inotify_init1, flags), mode='rb')
def add_watch(self, path, mask):
"""Wrapper around ``inotify_add_watch()``. Returns the watch
descriptor or raises an ``OSError`` on failure.
Args:
path (str, bytes, or PathLike): The path to watch. Will be encoded with
``os.fsencode()`` before being passed to ``inotify_add_watch()``.
mask (int): The mask of events to watch for. Can be constructed by
bitwise-ORing :class:`~inotify_simple.flags` together.
Returns:
int: watch descriptor"""
# Explicit conversion of Path to str required on Python < 3.6
path = str(path) if hasattr(path, 'parts') else path
return _libc_call(_libc.inotify_add_watch, self.fileno(), fsencode(path), mask)
def rm_watch(self, wd):
"""Wrapper around ``inotify_rm_watch()``. Raises ``OSError`` on failure.
Args:
wd (int): The watch descriptor to remove"""
_libc_call(_libc.inotify_rm_watch, self.fileno(), wd)
def poll(self):
"""Wait for I/O completion"""
while True:
try:
read_fs, _, _ = select([self.fileno()], [], [])
break
except select.error as err:
if err.errno == errno.EINTR:
break
else:
raise
return bool(read_fs)
def read(self, timeout=None, read_delay=None):
"""Read the inotify file descriptor and return the resulting
:attr:`~inotify_simple.Event` namedtuples (wd, mask, cookie, name).
Args:
timeout (int): The time in milliseconds to wait for events if there are
none. If negative or ``None``, block until there are events. If zero,
return immediately if there are no events to be read.
read_delay (int): If there are no events immediately available for reading,
then this is the time in milliseconds to wait after the first event
arrives before reading the file descriptor. This allows further events
to accumulate before reading, which allows the kernel to coalesce like
events and can decrease the number of events the application needs to
process. However, this also increases the risk that the event queue will
overflow due to not being emptied fast enough.
Returns:
generator: generator producing :attr:`~inotify_simple.Event` namedtuples
.. warning::
If the same inotify file descriptor is being read by multiple threads
simultaneously, this method may attempt to read the file descriptor when no
data is available. It may return zero events, or block until more events
arrive (regardless of the requested timeout), or in the case that the
:func:`~inotify_simple.INotify` object was instantiated with
``nonblocking=True``, raise ``BlockingIOError``.
"""
data = self._readall()
if not data and timeout != 0 and self.poll():
if read_delay is not None:
sleep(read_delay / 1000.0)
data = self._readall()
return parse_events(data)
def _readall(self):
bytes_avail = c_int()
ioctl(self, FIONREAD, bytes_avail)
if not bytes_avail.value:
return b''
return os.read(self.fileno(), bytes_avail.value)
def parse_events(data):
"""Unpack data read from an inotify file descriptor into
:attr:`~inotify_simple.Event` namedtuples (wd, mask, cookie, name). This function
can be used if the application has read raw data from the inotify file
descriptor rather than calling :func:`~inotify_simple.INotify.read`.
Args:
data (bytes): A bytestring as read from an inotify file descriptor.
Returns:
list: list of :attr:`~inotify_simple.Event` namedtuples"""
pos = 0
events = []
while pos < len(data):
wd, mask, cookie, namesize = unpack_from(_EVENT_FMT, data, pos)
pos += _EVENT_SIZE + namesize
name = data[pos - namesize: pos].split(b'\x00', 1)[0]
events.append(Event(wd, mask, cookie, fsdecode(name)))
return events
class flags(IntEnum):
"""Inotify flags as defined in ``inotify.h`` but with ``IN_`` prefix omitted.
Includes a convenience method :func:`~inotify_simple.flags.from_mask` for extracting
flags from a mask."""
ACCESS = 0x00000001 #: File was accessed
MODIFY = 0x00000002 #: File was modified
ATTRIB = 0x00000004 #: Metadata changed
CLOSE_WRITE = 0x00000008 #: Writable file was closed
CLOSE_NOWRITE = 0x00000010 #: Unwritable file closed
OPEN = 0x00000020 #: File was opened
MOVED_FROM = 0x00000040 #: File was moved from X
MOVED_TO = 0x00000080 #: File was moved to Y
CREATE = 0x00000100 #: Subfile was created
DELETE = 0x00000200 #: Subfile was deleted
DELETE_SELF = 0x00000400 #: Self was deleted
MOVE_SELF = 0x00000800 #: Self was moved
UNMOUNT = 0x00002000 #: Backing fs was unmounted
Q_OVERFLOW = 0x00004000 #: Event queue overflowed
IGNORED = 0x00008000 #: File was ignored
ONLYDIR = 0x01000000 #: only watch the path if it is a directory
DONT_FOLLOW = 0x02000000 #: don't follow a sym link
EXCL_UNLINK = 0x04000000 #: exclude events on unlinked objects
MASK_ADD = 0x20000000 #: add to the mask of an already existing watch
ISDIR = 0x40000000 #: event occurred against dir
ONESHOT = 0x80000000 #: only send event once
@classmethod
def from_mask(cls, mask):
"""Convenience method that returns a list of every flag in a mask."""
return [flag for flag in cls.__members__.values() if flag & mask]
class masks(IntEnum):
"""Convenience masks as defined in ``inotify.h`` but with ``IN_`` prefix omitted."""
#: helper event mask equal to ``flags.CLOSE_WRITE | flags.CLOSE_NOWRITE``
CLOSE = flags.CLOSE_WRITE | flags.CLOSE_NOWRITE
#: helper event mask equal to ``flags.MOVED_FROM | flags.MOVED_TO``
MOVE = flags.MOVED_FROM | flags.MOVED_TO
#: bitwise-OR of all the events that can be passed to
#: :func:`~inotify_simple.INotify.add_watch`
ALL_EVENTS = (flags.ACCESS | flags.MODIFY | flags.ATTRIB | flags.CLOSE_WRITE |
flags.CLOSE_NOWRITE | flags.OPEN | flags.MOVED_FROM | flags.MOVED_TO |
flags.CREATE | flags.DELETE | flags.DELETE_SELF | flags.MOVE_SELF)