123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- # -*- test-case-name: twisted.internet.test.test_inotify -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- This module provides support for Twisted to linux inotify API.
- In order to use this support, simply do the following (and start a reactor
- at some point)::
- from twisted.internet import inotify
- from twisted.python import filepath
- def notify(ignored, filepath, mask):
- \"""
- For historical reasons, an opaque handle is passed as first
- parameter. This object should never be used.
- @param filepath: FilePath on which the event happened.
- @param mask: inotify event as hexadecimal masks
- \"""
- print("event %s on %s" % (
- ', '.join(inotify.humanReadableMask(mask)), filepath))
- notifier = inotify.INotify()
- notifier.startReading()
- notifier.watch(filepath.FilePath("/some/directory"), callbacks=[notify])
- notifier.watch(filepath.FilePath(b"/some/directory2"), callbacks=[notify])
- Note that in the above example, a L{FilePath} which is a L{bytes} path name
- or L{str} path name may be used. However, no matter what type of
- L{FilePath} is passed to this module, internally the L{FilePath} is
- converted to L{bytes} according to L{sys.getfilesystemencoding}.
- For any L{FilePath} returned by this module, the caller is responsible for
- converting from a L{bytes} path name to a L{str} path name.
- @since: 10.1
- """
- import os
- import struct
- from twisted.internet import fdesc
- from twisted.internet.abstract import FileDescriptor
- from twisted.python import _inotify, log
- # from /usr/src/linux/include/linux/inotify.h
- IN_ACCESS = 0x00000001 # File was accessed
- IN_MODIFY = 0x00000002 # File was modified
- IN_ATTRIB = 0x00000004 # Metadata changed
- IN_CLOSE_WRITE = 0x00000008 # Writeable file was closed
- IN_CLOSE_NOWRITE = 0x00000010 # Unwriteable 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 delete
- IN_DELETE_SELF = 0x00000400 # Self was deleted
- IN_MOVE_SELF = 0x00000800 # Self was moved
- IN_UNMOUNT = 0x00002000 # Backing fs was unmounted
- IN_Q_OVERFLOW = 0x00004000 # Event queued overflowed
- IN_IGNORED = 0x00008000 # File was ignored
- IN_ONLYDIR = 0x01000000 # only watch the path if it is a directory
- IN_DONT_FOLLOW = 0x02000000 # don't follow a sym link
- 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
- IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE # closes
- IN_MOVED = IN_MOVED_FROM | IN_MOVED_TO # moves
- IN_CHANGED = IN_MODIFY | IN_ATTRIB # changes
- IN_WATCH_MASK = (
- IN_MODIFY
- | IN_ATTRIB
- | IN_CREATE
- | IN_DELETE
- | IN_DELETE_SELF
- | IN_MOVE_SELF
- | IN_UNMOUNT
- | IN_MOVED_FROM
- | IN_MOVED_TO
- )
- _FLAG_TO_HUMAN = [
- (IN_ACCESS, "access"),
- (IN_MODIFY, "modify"),
- (IN_ATTRIB, "attrib"),
- (IN_CLOSE_WRITE, "close_write"),
- (IN_CLOSE_NOWRITE, "close_nowrite"),
- (IN_OPEN, "open"),
- (IN_MOVED_FROM, "moved_from"),
- (IN_MOVED_TO, "moved_to"),
- (IN_CREATE, "create"),
- (IN_DELETE, "delete"),
- (IN_DELETE_SELF, "delete_self"),
- (IN_MOVE_SELF, "move_self"),
- (IN_UNMOUNT, "unmount"),
- (IN_Q_OVERFLOW, "queue_overflow"),
- (IN_IGNORED, "ignored"),
- (IN_ONLYDIR, "only_dir"),
- (IN_DONT_FOLLOW, "dont_follow"),
- (IN_MASK_ADD, "mask_add"),
- (IN_ISDIR, "is_dir"),
- (IN_ONESHOT, "one_shot"),
- ]
- def humanReadableMask(mask):
- """
- Auxiliary function that converts a hexadecimal mask into a series
- of human readable flags.
- """
- s = []
- for k, v in _FLAG_TO_HUMAN:
- if k & mask:
- s.append(v)
- return s
- class _Watch:
- """
- Watch object that represents a Watch point in the filesystem. The
- user should let INotify to create these objects
- @ivar path: The path over which this watch point is monitoring
- @ivar mask: The events monitored by this watchpoint
- @ivar autoAdd: Flag that determines whether this watch point
- should automatically add created subdirectories
- @ivar callbacks: L{list} of callback functions that will be called
- when an event occurs on this watch.
- """
- def __init__(self, path, mask=IN_WATCH_MASK, autoAdd=False, callbacks=None):
- self.path = path.asBytesMode()
- self.mask = mask
- self.autoAdd = autoAdd
- if callbacks is None:
- callbacks = []
- self.callbacks = callbacks
- def _notify(self, filepath, events):
- """
- Callback function used by L{INotify} to dispatch an event.
- """
- filepath = filepath.asBytesMode()
- for callback in self.callbacks:
- callback(self, filepath, events)
- class INotify(FileDescriptor):
- """
- The INotify file descriptor, it basically does everything related
- to INotify, from reading to notifying watch points.
- @ivar _buffer: a L{bytes} containing the data read from the inotify fd.
- @ivar _watchpoints: a L{dict} that maps from inotify watch ids to
- watchpoints objects
- @ivar _watchpaths: a L{dict} that maps from watched paths to the
- inotify watch ids
- """
- _inotify = _inotify
- def __init__(self, reactor=None):
- FileDescriptor.__init__(self, reactor=reactor)
- # Smart way to allow parametrization of libc so I can override
- # it and test for the system errors.
- self._fd = self._inotify.init()
- fdesc.setNonBlocking(self._fd)
- fdesc._setCloseOnExec(self._fd)
- # The next 2 lines are needed to have self.loseConnection()
- # to call connectionLost() on us. Since we already created the
- # fd that talks to inotify we want to be notified even if we
- # haven't yet started reading.
- self.connected = 1
- self._writeDisconnected = True
- self._buffer = b""
- self._watchpoints = {}
- self._watchpaths = {}
- def _addWatch(self, path, mask, autoAdd, callbacks):
- """
- Private helper that abstracts the use of ctypes.
- Calls the internal inotify API and checks for any errors after the
- call. If there's an error L{INotify._addWatch} can raise an
- INotifyError. If there's no error it proceeds creating a watchpoint and
- adding a watchpath for inverse lookup of the file descriptor from the
- path.
- """
- path = path.asBytesMode()
- wd = self._inotify.add(self._fd, path, mask)
- iwp = _Watch(path, mask, autoAdd, callbacks)
- self._watchpoints[wd] = iwp
- self._watchpaths[path] = wd
- return wd
- def _rmWatch(self, wd):
- """
- Private helper that abstracts the use of ctypes.
- Calls the internal inotify API to remove an fd from inotify then
- removes the corresponding watchpoint from the internal mapping together
- with the file descriptor from the watchpath.
- """
- self._inotify.remove(self._fd, wd)
- iwp = self._watchpoints.pop(wd)
- self._watchpaths.pop(iwp.path)
- def connectionLost(self, reason):
- """
- Release the inotify file descriptor and do the necessary cleanup
- """
- FileDescriptor.connectionLost(self, reason)
- if self._fd >= 0:
- try:
- os.close(self._fd)
- except OSError as e:
- log.err(e, "Couldn't close INotify file descriptor.")
- def fileno(self):
- """
- Get the underlying file descriptor from this inotify observer.
- Required by L{abstract.FileDescriptor} subclasses.
- """
- return self._fd
- def doRead(self):
- """
- Read some data from the observed file descriptors
- """
- fdesc.readFromFD(self._fd, self._doRead)
- def _doRead(self, in_):
- """
- Work on the data just read from the file descriptor.
- """
- self._buffer += in_
- while len(self._buffer) >= 16:
- wd, mask, cookie, size = struct.unpack("=LLLL", self._buffer[0:16])
- if size:
- name = self._buffer[16 : 16 + size].rstrip(b"\0")
- else:
- name = None
- self._buffer = self._buffer[16 + size :]
- try:
- iwp = self._watchpoints[wd]
- except KeyError:
- continue
- path = iwp.path.asBytesMode()
- if name:
- path = path.child(name)
- iwp._notify(path, mask)
- if iwp.autoAdd and mask & IN_ISDIR and mask & IN_CREATE:
- # mask & IN_ISDIR already guarantees that the path is a
- # directory. There's no way you can get here without a
- # directory anyway, so no point in checking for that again.
- new_wd = self.watch(
- path, mask=iwp.mask, autoAdd=True, callbacks=iwp.callbacks
- )
- # This is very very very hacky and I'd rather not do this but
- # we have no other alternative that is less hacky other than
- # surrender. We use callLater because we don't want to have
- # too many events waiting while we process these subdirs, we
- # must always answer events as fast as possible or the overflow
- # might come.
- self.reactor.callLater(0, self._addChildren, self._watchpoints[new_wd])
- if mask & IN_DELETE_SELF:
- self._rmWatch(wd)
- self.loseConnection()
- def _addChildren(self, iwp):
- """
- This is a very private method, please don't even think about using it.
- Note that this is a fricking hack... it's because we cannot be fast
- enough in adding a watch to a directory and so we basically end up
- getting here too late if some operations have already been going on in
- the subdir, we basically need to catchup. This eventually ends up
- meaning that we generate double events, your app must be resistant.
- """
- try:
- listdir = iwp.path.children()
- except OSError:
- # Somebody or something (like a test) removed this directory while
- # we were in the callLater(0...) waiting. It doesn't make sense to
- # process it anymore
- return
- # note that it's true that listdir will only see the subdirs inside
- # path at the moment of the call but path is monitored already so if
- # something is created we will receive an event.
- for f in listdir:
- # It's a directory, watch it and then add its children
- if f.isdir():
- wd = self.watch(f, mask=iwp.mask, autoAdd=True, callbacks=iwp.callbacks)
- iwp._notify(f, IN_ISDIR | IN_CREATE)
- # now f is watched, we can add its children the callLater is to
- # avoid recursion
- self.reactor.callLater(0, self._addChildren, self._watchpoints[wd])
- # It's a file and we notify it.
- if f.isfile():
- iwp._notify(f, IN_CREATE | IN_CLOSE_WRITE)
- def watch(
- self, path, mask=IN_WATCH_MASK, autoAdd=False, callbacks=None, recursive=False
- ):
- """
- Watch the 'mask' events in given path. Can raise C{INotifyError} when
- there's a problem while adding a directory.
- @param path: The path needing monitoring
- @type path: L{FilePath}
- @param mask: The events that should be watched
- @type mask: L{int}
- @param autoAdd: if True automatically add newly created
- subdirectories
- @type autoAdd: L{bool}
- @param callbacks: A list of callbacks that should be called
- when an event happens in the given path.
- The callback should accept 3 arguments:
- (ignored, filepath, mask)
- @type callbacks: L{list} of callables
- @param recursive: Also add all the subdirectories in this path
- @type recursive: L{bool}
- """
- if recursive:
- # This behavior is needed to be compatible with the windows
- # interface for filesystem changes:
- # http://msdn.microsoft.com/en-us/library/aa365465(VS.85).aspx
- # ReadDirectoryChangesW can do bWatchSubtree so it doesn't
- # make sense to implement this at a higher abstraction
- # level when other platforms support it already
- for child in path.walk():
- if child.isdir():
- self.watch(child, mask, autoAdd, callbacks, recursive=False)
- else:
- wd = self._isWatched(path)
- if wd:
- return wd
- mask = mask | IN_DELETE_SELF # need this to remove the watch
- return self._addWatch(path, mask, autoAdd, callbacks)
- def ignore(self, path):
- """
- Remove the watch point monitoring the given path
- @param path: The path that should be ignored
- @type path: L{FilePath}
- """
- path = path.asBytesMode()
- wd = self._isWatched(path)
- if wd is None:
- raise KeyError(f"{path!r} is not watched")
- else:
- self._rmWatch(wd)
- def _isWatched(self, path):
- """
- Helper function that checks if the path is already monitored
- and returns its watchdescriptor if so or None otherwise.
- @param path: The path that should be checked
- @type path: L{FilePath}
- """
- path = path.asBytesMode()
- return self._watchpaths.get(path, None)
- INotifyError = _inotify.INotifyError
- __all__ = [
- "INotify",
- "humanReadableMask",
- "IN_WATCH_MASK",
- "IN_ACCESS",
- "IN_MODIFY",
- "IN_ATTRIB",
- "IN_CLOSE_NOWRITE",
- "IN_CLOSE_WRITE",
- "IN_OPEN",
- "IN_MOVED_FROM",
- "IN_MOVED_TO",
- "IN_CREATE",
- "IN_DELETE",
- "IN_DELETE_SELF",
- "IN_MOVE_SELF",
- "IN_UNMOUNT",
- "IN_Q_OVERFLOW",
- "IN_IGNORED",
- "IN_ONLYDIR",
- "IN_DONT_FOLLOW",
- "IN_MASK_ADD",
- "IN_ISDIR",
- "IN_ONESHOT",
- "IN_CLOSE",
- "IN_MOVED",
- "IN_CHANGED",
- ]
|