%PDF- %PDF-
Direktori : /lib/calibre/calibre/utils/ |
Current File : //lib/calibre/calibre/utils/inotify.py |
#!/usr/bin/env python3 __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __docformat__ = 'restructuredtext en' import sys, os, errno, select class INotifyError(Exception): pass class NoSuchDir(ValueError): pass class BaseDirChanged(ValueError): pass class DirTooLarge(ValueError): def __init__(self, bdir): ValueError.__init__(self, f'The directory {bdir} is too large to monitor. Try increasing the value in /proc/sys/fs/inotify/max_user_watches') _inotify = None def load_inotify(): # {{{ ''' Initialize the inotify ctypes wrapper ''' global _inotify if _inotify is None: if hasattr(sys, 'getwindowsversion'): # On windows abort before loading the C library. Windows has # multiple, incompatible C runtimes, and we have no way of knowing # if the one chosen by ctypes is compatible with the currently # loaded one. raise INotifyError('INotify not available on windows') if sys.platform == 'darwin': raise INotifyError('INotify not available on OS X') import ctypes if not hasattr(ctypes, 'c_ssize_t'): raise INotifyError('You need python >= 2.7 to use inotify') libc = ctypes.CDLL(None, use_errno=True) for function in ("inotify_add_watch", "inotify_init1", "inotify_rm_watch"): if not hasattr(libc, function): raise INotifyError('libc is too old') # inotify_init1() prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, use_errno=True) init1 = prototype(('inotify_init1', libc), ((1, "flags", 0),)) # inotify_add_watch() prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32, use_errno=True) add_watch = prototype(('inotify_add_watch', libc), ( (1, "fd"), (1, "pathname"), (1, "mask")), use_errno=True) # inotify_rm_watch() prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int, use_errno=True) rm_watch = prototype(('inotify_rm_watch', libc), ( (1, "fd"), (1, "wd")), use_errno=True) # read() prototype = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, use_errno=True) read = prototype(('read', libc), ( (1, "fd"), (1, "buf"), (1, "count")), use_errno=True) _inotify = (init1, add_watch, rm_watch, read) return _inotify # }}} class INotify: # See <sys/inotify.h> for the flags defined below # Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH. ACCESS = 0x00000001 # File was accessed. MODIFY = 0x00000002 # File was modified. ATTRIB = 0x00000004 # Metadata changed. CLOSE_WRITE = 0x00000008 # Writtable file was closed. CLOSE_NOWRITE = 0x00000010 # Unwrittable 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. # Events sent by the kernel. UNMOUNT = 0x00002000 # Backing fs was unmounted. Q_OVERFLOW = 0x00004000 # Event queued overflowed. IGNORED = 0x00008000 # File was ignored. # Helper events. CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close. MOVE = (MOVED_FROM | MOVED_TO) # Moves. # Special flags. ONLYDIR = 0x01000000 # Only watch the path if it is a directory. DONT_FOLLOW = 0x02000000 # Do not 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. # All events which a program can wait on. ALL_EVENTS = (ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO | CREATE | DELETE | DELETE_SELF | MOVE_SELF) # See <bits/inotify.h> CLOEXEC = 0x80000 NONBLOCK = 0x800 def __init__(self, cloexec=True, nonblock=True): import ctypes, struct self._init1, self._add_watch, self._rm_watch, self._read = load_inotify() flags = 0 if cloexec: flags |= self.CLOEXEC if nonblock: flags |= self.NONBLOCK self._inotify_fd = self._init1(flags) if self._inotify_fd == -1: raise INotifyError(os.strerror(ctypes.get_errno())) self._buf = ctypes.create_string_buffer(5120) self.fenc = sys.getfilesystemencoding() or 'utf-8' self.hdr = struct.Struct(b'iIII') if self.fenc == 'ascii': self.fenc = 'utf-8' # We keep a reference to os to prevent it from being deleted # during interpreter shutdown, which would lead to errors in the # __del__ method self.os = os def handle_error(self): import ctypes eno = ctypes.get_errno() extra = '' if eno == errno.ENOSPC: extra = 'You may need to increase the inotify limits on your system, via /proc/sys/inotify/max_user_*' raise OSError(eno, self.os.strerror(eno) + extra) def __del__(self): # This method can be called during interpreter shutdown, which means we # must do the absolute minimum here. Note that there could be running # daemon threads that are trying to call other methods on this object. try: self.os.close(self._inotify_fd) except (AttributeError, TypeError): pass def close(self): if hasattr(self, '_inotify_fd'): self.os.close(self._inotify_fd) del self.os del self._add_watch del self._rm_watch del self._inotify_fd def __enter__(self): return self def __exit__(self, *args): self.close() def read(self, get_name=True): import ctypes buf = [] while True: num = self._read(self._inotify_fd, self._buf, len(self._buf)) if num == 0: break if num < 0: en = ctypes.get_errno() if en == errno.EAGAIN: break # No more data if en == errno.EINTR: continue # Interrupted, try again raise OSError(en, self.os.strerror(en)) buf.append(self._buf.raw[:num]) raw = b''.join(buf) pos = 0 lraw = len(raw) while lraw - pos >= self.hdr.size: wd, mask, cookie, name_len = self.hdr.unpack_from(raw, pos) pos += self.hdr.size name = None if get_name: name = raw[pos:pos+name_len].rstrip(b'\0').decode(self.fenc) pos += name_len self.process_event(wd, mask, cookie, name) def process_event(self, *args): raise NotImplementedError() def wait(self, timeout=None): 'Return True iff there are events waiting to be read. Blocks if timeout is None. Polls if timeout is 0.' return len((select.select([self._inotify_fd], [], []) if timeout is None else select.select([self._inotify_fd], [], [], timeout))[0]) > 0 def realpath(path): return os.path.abspath(os.path.realpath(path)) class INotifyTreeWatcher(INotify): is_dummy = False def __init__(self, basedir, ignore_event=None): super().__init__() self.basedir = realpath(basedir) self.watch_tree() self.modified = set() self.ignore_event = (lambda path, name: False) if ignore_event is None else ignore_event def watch_tree(self): self.watched_dirs = {} self.watched_rmap = {} try: self.add_watches(self.basedir) except OSError as e: if e.errno == errno.ENOSPC: raise DirTooLarge(self.basedir) def add_watches(self, base, top_level=True): ''' Add watches for this directory and all its descendant directories, recursively. ''' base = realpath(base) # There may exist a link which leads to an endless # add_watches loop or to maximum recursion depth exceeded if not top_level and base in self.watched_dirs: return try: is_dir = self.add_watch(base) except OSError as e: if e.errno == errno.ENOENT: # The entry could have been deleted between listdir() and # add_watch(). if top_level: raise NoSuchDir(f'The dir {base} does not exist') return if e.errno == errno.EACCES: # We silently ignore entries for which we dont have permission, # unless they are the top level dir if top_level: raise NoSuchDir(f'You do not have permission to monitor {base}') return raise else: if is_dir: try: files = os.listdir(base) except OSError as e: if e.errno in (errno.ENOTDIR, errno.ENOENT): # The dir was deleted/replaced between the add_watch() # and listdir() if top_level: raise NoSuchDir(f'The dir {base} does not exist') return raise for x in files: self.add_watches(os.path.join(base, x), top_level=False) elif top_level: # The top level dir is a file, not good. raise NoSuchDir(f'The dir {base} does not exist') def add_watch(self, path): import ctypes bpath = path if isinstance(path, bytes) else path.encode(self.fenc) wd = self._add_watch(self._inotify_fd, ctypes.c_char_p(bpath), # Ignore symlinks and watch only directories self.DONT_FOLLOW | self.ONLYDIR | self.MODIFY | self.CREATE | self.DELETE | self.MOVE_SELF | self.MOVED_FROM | self.MOVED_TO | self.ATTRIB | self.DELETE_SELF) if wd == -1: eno = ctypes.get_errno() if eno == errno.ENOTDIR: return False raise OSError(eno, f'Failed to add watch for: {path}: {self.os.strerror(eno)}') self.watched_dirs[path] = wd self.watched_rmap[wd] = path return True def process_event(self, wd, mask, cookie, name): if wd == -1 and (mask & self.Q_OVERFLOW): # We missed some INOTIFY events, so we dont # know the state of any tracked dirs. self.watch_tree() self.modified.add(None) return path = self.watched_rmap.get(wd, None) if path is not None: if not self.ignore_event(path, name): self.modified.add(os.path.join(path, name or '')) if mask & self.CREATE: # A new sub-directory might have been created, monitor it. try: self.add_watch(os.path.join(path, name)) except OSError as e: if e.errno == errno.ENOENT: # Deleted before add_watch() pass elif e.errno == errno.ENOSPC: raise DirTooLarge(self.basedir) else: raise if (mask & self.DELETE_SELF or mask & self.MOVE_SELF) and path == self.basedir: raise BaseDirChanged('The directory %s was moved/deleted' % path) def __call__(self): self.read() ret = self.modified self.modified = set() return ret if __name__ == '__main__': w = INotifyTreeWatcher(sys.argv[-1]) w() print('Monitoring', sys.argv[-1], 'press Ctrl-C to stop') try: while w.wait(): modified = w() for path in modified: print(path or sys.argv[-1], 'changed') raise SystemExit('inotify flaked out') except KeyboardInterrupt: pass