1#!/usr/local/bin/python3.8 2# vim:fileencoding=utf-8 3 4 5__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' 6__docformat__ = 'restructuredtext en' 7 8import sys, os, errno, select 9 10 11class INotifyError(Exception): 12 pass 13 14 15class NoSuchDir(ValueError): 16 pass 17 18 19class BaseDirChanged(ValueError): 20 pass 21 22 23class DirTooLarge(ValueError): 24 25 def __init__(self, bdir): 26 ValueError.__init__(self, 'The directory {} is too large to monitor. Try increasing the value in /proc/sys/fs/inotify/max_user_watches'.format(bdir)) 27 28 29_inotify = None 30 31 32def load_inotify(): # {{{ 33 ''' Initialize the inotify ctypes wrapper ''' 34 global _inotify 35 if _inotify is None: 36 if hasattr(sys, 'getwindowsversion'): 37 # On windows abort before loading the C library. Windows has 38 # multiple, incompatible C runtimes, and we have no way of knowing 39 # if the one chosen by ctypes is compatible with the currently 40 # loaded one. 41 raise INotifyError('INotify not available on windows') 42 if sys.platform == 'darwin': 43 raise INotifyError('INotify not available on OS X') 44 import ctypes 45 if not hasattr(ctypes, 'c_ssize_t'): 46 raise INotifyError('You need python >= 2.7 to use inotify') 47 libc = ctypes.CDLL(None, use_errno=True) 48 for function in ("inotify_add_watch", "inotify_init1", "inotify_rm_watch"): 49 if not hasattr(libc, function): 50 raise INotifyError('libc is too old') 51 # inotify_init1() 52 prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, use_errno=True) 53 init1 = prototype(('inotify_init1', libc), ((1, "flags", 0),)) 54 55 # inotify_add_watch() 56 prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32, use_errno=True) 57 add_watch = prototype(('inotify_add_watch', libc), ( 58 (1, "fd"), (1, "pathname"), (1, "mask")), use_errno=True) 59 60 # inotify_rm_watch() 61 prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int, use_errno=True) 62 rm_watch = prototype(('inotify_rm_watch', libc), ( 63 (1, "fd"), (1, "wd")), use_errno=True) 64 65 # read() 66 prototype = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, use_errno=True) 67 read = prototype(('read', libc), ( 68 (1, "fd"), (1, "buf"), (1, "count")), use_errno=True) 69 _inotify = (init1, add_watch, rm_watch, read) 70 return _inotify 71# }}} 72 73 74class INotify: 75 76 # See <sys/inotify.h> for the flags defined below 77 78 # Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH. 79 ACCESS = 0x00000001 # File was accessed. 80 MODIFY = 0x00000002 # File was modified. 81 ATTRIB = 0x00000004 # Metadata changed. 82 CLOSE_WRITE = 0x00000008 # Writtable file was closed. 83 CLOSE_NOWRITE = 0x00000010 # Unwrittable file closed. 84 OPEN = 0x00000020 # File was opened. 85 MOVED_FROM = 0x00000040 # File was moved from X. 86 MOVED_TO = 0x00000080 # File was moved to Y. 87 CREATE = 0x00000100 # Subfile was created. 88 DELETE = 0x00000200 # Subfile was deleted. 89 DELETE_SELF = 0x00000400 # Self was deleted. 90 MOVE_SELF = 0x00000800 # Self was moved. 91 92 # Events sent by the kernel. 93 UNMOUNT = 0x00002000 # Backing fs was unmounted. 94 Q_OVERFLOW = 0x00004000 # Event queued overflowed. 95 IGNORED = 0x00008000 # File was ignored. 96 97 # Helper events. 98 CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close. 99 MOVE = (MOVED_FROM | MOVED_TO) # Moves. 100 101 # Special flags. 102 ONLYDIR = 0x01000000 # Only watch the path if it is a directory. 103 DONT_FOLLOW = 0x02000000 # Do not follow a sym link. 104 EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects. 105 MASK_ADD = 0x20000000 # Add to the mask of an already existing watch. 106 ISDIR = 0x40000000 # Event occurred against dir. 107 ONESHOT = 0x80000000 # Only send event once. 108 109 # All events which a program can wait on. 110 ALL_EVENTS = (ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE | 111 OPEN | MOVED_FROM | MOVED_TO | CREATE | DELETE | 112 DELETE_SELF | MOVE_SELF) 113 114 # See <bits/inotify.h> 115 CLOEXEC = 0x80000 116 NONBLOCK = 0x800 117 118 def __init__(self, cloexec=True, nonblock=True): 119 import ctypes, struct 120 self._init1, self._add_watch, self._rm_watch, self._read = load_inotify() 121 flags = 0 122 if cloexec: 123 flags |= self.CLOEXEC 124 if nonblock: 125 flags |= self.NONBLOCK 126 self._inotify_fd = self._init1(flags) 127 if self._inotify_fd == -1: 128 raise INotifyError(os.strerror(ctypes.get_errno())) 129 130 self._buf = ctypes.create_string_buffer(5120) 131 self.fenc = sys.getfilesystemencoding() or 'utf-8' 132 self.hdr = struct.Struct(b'iIII') 133 if self.fenc == 'ascii': 134 self.fenc = 'utf-8' 135 # We keep a reference to os to prevent it from being deleted 136 # during interpreter shutdown, which would lead to errors in the 137 # __del__ method 138 self.os = os 139 140 def handle_error(self): 141 import ctypes 142 eno = ctypes.get_errno() 143 extra = '' 144 if eno == errno.ENOSPC: 145 extra = 'You may need to increase the inotify limits on your system, via /proc/sys/inotify/max_user_*' 146 raise OSError(eno, self.os.strerror(eno) + extra) 147 148 def __del__(self): 149 # This method can be called during interpreter shutdown, which means we 150 # must do the absolute minimum here. Note that there could be running 151 # daemon threads that are trying to call other methods on this object. 152 try: 153 self.os.close(self._inotify_fd) 154 except (AttributeError, TypeError): 155 pass 156 157 def close(self): 158 if hasattr(self, '_inotify_fd'): 159 self.os.close(self._inotify_fd) 160 del self.os 161 del self._add_watch 162 del self._rm_watch 163 del self._inotify_fd 164 165 def __enter__(self): 166 return self 167 168 def __exit__(self, *args): 169 self.close() 170 171 def read(self, get_name=True): 172 import ctypes 173 buf = [] 174 while True: 175 num = self._read(self._inotify_fd, self._buf, len(self._buf)) 176 if num == 0: 177 break 178 if num < 0: 179 en = ctypes.get_errno() 180 if en == errno.EAGAIN: 181 break # No more data 182 if en == errno.EINTR: 183 continue # Interrupted, try again 184 raise OSError(en, self.os.strerror(en)) 185 buf.append(self._buf.raw[:num]) 186 raw = b''.join(buf) 187 pos = 0 188 lraw = len(raw) 189 while lraw - pos >= self.hdr.size: 190 wd, mask, cookie, name_len = self.hdr.unpack_from(raw, pos) 191 pos += self.hdr.size 192 name = None 193 if get_name: 194 name = raw[pos:pos+name_len].rstrip(b'\0').decode(self.fenc) 195 pos += name_len 196 self.process_event(wd, mask, cookie, name) 197 198 def process_event(self, *args): 199 raise NotImplementedError() 200 201 def wait(self, timeout=None): 202 'Return True iff there are events waiting to be read. Blocks if timeout is None. Polls if timeout is 0.' 203 return len((select.select([self._inotify_fd], [], []) if timeout is None else select.select([self._inotify_fd], [], [], timeout))[0]) > 0 204 205 206def realpath(path): 207 return os.path.abspath(os.path.realpath(path)) 208 209 210class INotifyTreeWatcher(INotify): 211 212 is_dummy = False 213 214 def __init__(self, basedir, ignore_event=None): 215 super().__init__() 216 self.basedir = realpath(basedir) 217 self.watch_tree() 218 self.modified = set() 219 self.ignore_event = (lambda path, name: False) if ignore_event is None else ignore_event 220 221 def watch_tree(self): 222 self.watched_dirs = {} 223 self.watched_rmap = {} 224 try: 225 self.add_watches(self.basedir) 226 except OSError as e: 227 if e.errno == errno.ENOSPC: 228 raise DirTooLarge(self.basedir) 229 230 def add_watches(self, base, top_level=True): 231 ''' Add watches for this directory and all its descendant directories, 232 recursively. ''' 233 base = realpath(base) 234 # There may exist a link which leads to an endless 235 # add_watches loop or to maximum recursion depth exceeded 236 if not top_level and base in self.watched_dirs: 237 return 238 try: 239 is_dir = self.add_watch(base) 240 except OSError as e: 241 if e.errno == errno.ENOENT: 242 # The entry could have been deleted between listdir() and 243 # add_watch(). 244 if top_level: 245 raise NoSuchDir('The dir {} does not exist'.format(base)) 246 return 247 if e.errno == errno.EACCES: 248 # We silently ignore entries for which we dont have permission, 249 # unless they are the top level dir 250 if top_level: 251 raise NoSuchDir('You do not have permission to monitor {}'.format(base)) 252 return 253 raise 254 else: 255 if is_dir: 256 try: 257 files = os.listdir(base) 258 except OSError as e: 259 if e.errno in (errno.ENOTDIR, errno.ENOENT): 260 # The dir was deleted/replaced between the add_watch() 261 # and listdir() 262 if top_level: 263 raise NoSuchDir('The dir {} does not exist'.format(base)) 264 return 265 raise 266 for x in files: 267 self.add_watches(os.path.join(base, x), top_level=False) 268 elif top_level: 269 # The top level dir is a file, not good. 270 raise NoSuchDir('The dir {} does not exist'.format(base)) 271 272 def add_watch(self, path): 273 import ctypes 274 bpath = path if isinstance(path, bytes) else path.encode(self.fenc) 275 wd = self._add_watch(self._inotify_fd, ctypes.c_char_p(bpath), 276 # Ignore symlinks and watch only directories 277 self.DONT_FOLLOW | self.ONLYDIR | 278 279 self.MODIFY | self.CREATE | self.DELETE | 280 self.MOVE_SELF | self.MOVED_FROM | self.MOVED_TO | 281 self.ATTRIB | self.DELETE_SELF) 282 if wd == -1: 283 eno = ctypes.get_errno() 284 if eno == errno.ENOTDIR: 285 return False 286 raise OSError(eno, 'Failed to add watch for: {}: {}'.format(path, self.os.strerror(eno))) 287 self.watched_dirs[path] = wd 288 self.watched_rmap[wd] = path 289 return True 290 291 def process_event(self, wd, mask, cookie, name): 292 if wd == -1 and (mask & self.Q_OVERFLOW): 293 # We missed some INOTIFY events, so we dont 294 # know the state of any tracked dirs. 295 self.watch_tree() 296 self.modified.add(None) 297 return 298 path = self.watched_rmap.get(wd, None) 299 if path is not None: 300 if not self.ignore_event(path, name): 301 self.modified.add(os.path.join(path, name or '')) 302 if mask & self.CREATE: 303 # A new sub-directory might have been created, monitor it. 304 try: 305 self.add_watch(os.path.join(path, name)) 306 except OSError as e: 307 if e.errno == errno.ENOENT: 308 # Deleted before add_watch() 309 pass 310 elif e.errno == errno.ENOSPC: 311 raise DirTooLarge(self.basedir) 312 else: 313 raise 314 if (mask & self.DELETE_SELF or mask & self.MOVE_SELF) and path == self.basedir: 315 raise BaseDirChanged('The directory %s was moved/deleted' % path) 316 317 def __call__(self): 318 self.read() 319 ret = self.modified 320 self.modified = set() 321 return ret 322 323 324if __name__ == '__main__': 325 w = INotifyTreeWatcher(sys.argv[-1]) 326 w() 327 print('Monitoring', sys.argv[-1], 'press Ctrl-C to stop') 328 try: 329 while w.wait(): 330 modified = w() 331 for path in modified: 332 print(path or sys.argv[-1], 'changed') 333 raise SystemExit('inotify flaked out') 334 except KeyboardInterrupt: 335 pass 336