| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python | 
 | 2 |  | 
 | 3 | # pyinotify.py - python interface to inotify | 
 | 4 | # Copyright (c) 2005-2015 Sebastien Martini <seb@dbzteam.org> | 
 | 5 | # | 
 | 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy | 
 | 7 | # of this software and associated documentation files (the "Software"), to deal | 
 | 8 | # in the Software without restriction, including without limitation the rights | 
 | 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | 
 | 10 | # copies of the Software, and to permit persons to whom the Software is | 
 | 11 | # furnished to do so, subject to the following conditions: | 
 | 12 | # | 
 | 13 | # The above copyright notice and this permission notice shall be included in | 
 | 14 | # all copies or substantial portions of the Software. | 
 | 15 | # | 
 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | 
 | 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | 
 | 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | 
 | 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | 
 | 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 
 | 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | 
 | 22 | # THE SOFTWARE. | 
 | 23 | """ | 
 | 24 | pyinotify | 
 | 25 |  | 
 | 26 | @author: Sebastien Martini | 
 | 27 | @license: MIT License | 
 | 28 | @contact: seb@dbzteam.org | 
 | 29 | """ | 
 | 30 |  | 
 | 31 | class PyinotifyError(Exception): | 
 | 32 |     """Indicates exceptions raised by a Pyinotify class.""" | 
 | 33 |     pass | 
 | 34 |  | 
 | 35 |  | 
 | 36 | class UnsupportedPythonVersionError(PyinotifyError): | 
 | 37 |     """ | 
 | 38 |     Raised on unsupported Python versions. | 
 | 39 |     """ | 
 | 40 |     def __init__(self, version): | 
 | 41 |         """ | 
 | 42 |         @param version: Current Python version | 
 | 43 |         @type version: string | 
 | 44 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 45 |         PyinotifyError.__init__(self, | 
 | 46 |                                 ('Python %s is unsupported, requires ' | 
 | 47 |                                  'at least Python 3.0') % version) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 48 |  | 
 | 49 |  | 
 | 50 | # Check Python version | 
 | 51 | import sys | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 52 | if sys.version_info < (3, 0): | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 53 |     raise UnsupportedPythonVersionError(sys.version) | 
 | 54 |  | 
 | 55 |  | 
 | 56 | # Import directives | 
 | 57 | import threading | 
 | 58 | import os | 
 | 59 | import select | 
 | 60 | import struct | 
 | 61 | import fcntl | 
 | 62 | import errno | 
 | 63 | import termios | 
 | 64 | import array | 
 | 65 | import logging | 
 | 66 | import atexit | 
 | 67 | from collections import deque | 
 | 68 | from datetime import datetime, timedelta | 
 | 69 | import time | 
 | 70 | import re | 
 | 71 | import asyncore | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 72 | import glob | 
 | 73 | import locale | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 74 | import subprocess | 
 | 75 |  | 
 | 76 | try: | 
 | 77 |     from functools import reduce | 
 | 78 | except ImportError: | 
 | 79 |     pass  # Will fail on Python 2.4 which has reduce() builtin anyway. | 
 | 80 |  | 
 | 81 | try: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 82 |     import ctypes | 
 | 83 |     import ctypes.util | 
 | 84 | except ImportError: | 
 | 85 |     ctypes = None | 
 | 86 |  | 
 | 87 | try: | 
 | 88 |     import inotify_syscalls | 
 | 89 | except ImportError: | 
 | 90 |     inotify_syscalls = None | 
 | 91 |  | 
 | 92 |  | 
 | 93 | __author__ = "seb@dbzteam.org (Sebastien Martini)" | 
 | 94 |  | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 95 | __version__ = "0.9.6" | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 96 |  | 
 | 97 |  | 
 | 98 | # Compatibity mode: set to True to improve compatibility with | 
 | 99 | # Pyinotify 0.7.1. Do not set this variable yourself, call the | 
 | 100 | # function compatibility_mode() instead. | 
 | 101 | COMPATIBILITY_MODE = False | 
 | 102 |  | 
 | 103 |  | 
 | 104 | class InotifyBindingNotFoundError(PyinotifyError): | 
 | 105 |     """ | 
 | 106 |     Raised when no inotify support couldn't be found. | 
 | 107 |     """ | 
 | 108 |     def __init__(self): | 
 | 109 |         err = "Couldn't find any inotify binding" | 
 | 110 |         PyinotifyError.__init__(self, err) | 
 | 111 |  | 
 | 112 |  | 
 | 113 | class INotifyWrapper: | 
 | 114 |     """ | 
 | 115 |     Abstract class wrapping access to inotify's functions. This is an | 
 | 116 |     internal class. | 
 | 117 |     """ | 
 | 118 |     @staticmethod | 
 | 119 |     def create(): | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 120 |         """ | 
 | 121 |         Factory method instanciating and returning the right wrapper. | 
 | 122 |         """ | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 123 |         # First, try to use ctypes. | 
 | 124 |         if ctypes: | 
 | 125 |             inotify = _CtypesLibcINotifyWrapper() | 
 | 126 |             if inotify.init(): | 
 | 127 |                 return inotify | 
 | 128 |         # Second, see if C extension is compiled. | 
 | 129 |         if inotify_syscalls: | 
 | 130 |             inotify = _INotifySyscallsWrapper() | 
 | 131 |             if inotify.init(): | 
 | 132 |                 return inotify | 
 | 133 |  | 
 | 134 |     def get_errno(self): | 
 | 135 |         """ | 
 | 136 |         Return None is no errno code is available. | 
 | 137 |         """ | 
 | 138 |         return self._get_errno() | 
 | 139 |  | 
 | 140 |     def str_errno(self): | 
 | 141 |         code = self.get_errno() | 
 | 142 |         if code is None: | 
 | 143 |             return 'Errno: no errno support' | 
 | 144 |         return 'Errno=%s (%s)' % (os.strerror(code), errno.errorcode[code]) | 
 | 145 |  | 
 | 146 |     def inotify_init(self): | 
 | 147 |         return self._inotify_init() | 
 | 148 |  | 
 | 149 |     def inotify_add_watch(self, fd, pathname, mask): | 
 | 150 |         # Unicode strings must be encoded to string prior to calling this | 
 | 151 |         # method. | 
 | 152 |         assert isinstance(pathname, str) | 
 | 153 |         return self._inotify_add_watch(fd, pathname, mask) | 
 | 154 |  | 
 | 155 |     def inotify_rm_watch(self, fd, wd): | 
 | 156 |         return self._inotify_rm_watch(fd, wd) | 
 | 157 |  | 
 | 158 |  | 
 | 159 | class _INotifySyscallsWrapper(INotifyWrapper): | 
 | 160 |     def __init__(self): | 
 | 161 |         # Stores the last errno value. | 
 | 162 |         self._last_errno = None | 
 | 163 |  | 
 | 164 |     def init(self): | 
 | 165 |         assert inotify_syscalls | 
 | 166 |         return True | 
 | 167 |  | 
 | 168 |     def _get_errno(self): | 
 | 169 |         return self._last_errno | 
 | 170 |  | 
 | 171 |     def _inotify_init(self): | 
 | 172 |         try: | 
 | 173 |             fd = inotify_syscalls.inotify_init() | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 174 |         except IOError as err: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 175 |             self._last_errno = err.errno | 
 | 176 |             return -1 | 
 | 177 |         return fd | 
 | 178 |  | 
 | 179 |     def _inotify_add_watch(self, fd, pathname, mask): | 
 | 180 |         try: | 
 | 181 |             wd = inotify_syscalls.inotify_add_watch(fd, pathname, mask) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 182 |         except IOError as err: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 183 |             self._last_errno = err.errno | 
 | 184 |             return -1 | 
 | 185 |         return wd | 
 | 186 |  | 
 | 187 |     def _inotify_rm_watch(self, fd, wd): | 
 | 188 |         try: | 
 | 189 |             ret = inotify_syscalls.inotify_rm_watch(fd, wd) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 190 |         except IOError as err: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 191 |             self._last_errno = err.errno | 
 | 192 |             return -1 | 
 | 193 |         return ret | 
 | 194 |  | 
 | 195 |  | 
 | 196 | class _CtypesLibcINotifyWrapper(INotifyWrapper): | 
 | 197 |     def __init__(self): | 
 | 198 |         self._libc = None | 
 | 199 |         self._get_errno_func = None | 
 | 200 |  | 
 | 201 |     def init(self): | 
 | 202 |         assert ctypes | 
 | 203 |  | 
 | 204 |         try_libc_name = 'c' | 
 | 205 |         if sys.platform.startswith('freebsd'): | 
 | 206 |             try_libc_name = 'inotify' | 
 | 207 |  | 
 | 208 |         libc_name = None | 
 | 209 |         try: | 
 | 210 |             libc_name = ctypes.util.find_library(try_libc_name) | 
 | 211 |         except (OSError, IOError): | 
 | 212 |             pass  # Will attemp to load it with None anyway. | 
 | 213 |  | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 214 |         self._libc = ctypes.CDLL(libc_name, use_errno=True) | 
 | 215 |         self._get_errno_func = ctypes.get_errno | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 216 |  | 
 | 217 |         # Eventually check that libc has needed inotify bindings. | 
 | 218 |         if (not hasattr(self._libc, 'inotify_init') or | 
 | 219 |             not hasattr(self._libc, 'inotify_add_watch') or | 
 | 220 |             not hasattr(self._libc, 'inotify_rm_watch')): | 
 | 221 |             return False | 
 | 222 |  | 
 | 223 |         self._libc.inotify_init.argtypes = [] | 
 | 224 |         self._libc.inotify_init.restype = ctypes.c_int | 
 | 225 |         self._libc.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, | 
 | 226 |                                                  ctypes.c_uint32] | 
 | 227 |         self._libc.inotify_add_watch.restype = ctypes.c_int | 
 | 228 |         self._libc.inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int] | 
 | 229 |         self._libc.inotify_rm_watch.restype = ctypes.c_int | 
 | 230 |         return True | 
 | 231 |  | 
 | 232 |     def _get_errno(self): | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 233 |         assert self._get_errno_func | 
 | 234 |         return self._get_errno_func() | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 235 |  | 
 | 236 |     def _inotify_init(self): | 
 | 237 |         assert self._libc is not None | 
 | 238 |         return self._libc.inotify_init() | 
 | 239 |  | 
 | 240 |     def _inotify_add_watch(self, fd, pathname, mask): | 
 | 241 |         assert self._libc is not None | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 242 |         # Encodes path to a bytes string. This conversion seems required because | 
 | 243 |         # ctypes.create_string_buffer seems to manipulate bytes internally. | 
 | 244 |         # Moreover it seems that inotify_add_watch does not work very well when | 
 | 245 |         # it receives an ctypes.create_unicode_buffer instance as argument. | 
 | 246 |         pathname = pathname.encode(sys.getfilesystemencoding()) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 247 |         pathname = ctypes.create_string_buffer(pathname) | 
 | 248 |         return self._libc.inotify_add_watch(fd, pathname, mask) | 
 | 249 |  | 
 | 250 |     def _inotify_rm_watch(self, fd, wd): | 
 | 251 |         assert self._libc is not None | 
 | 252 |         return self._libc.inotify_rm_watch(fd, wd) | 
 | 253 |  | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 254 |  | 
 | 255 | # Logging | 
 | 256 | def logger_init(): | 
 | 257 |     """Initialize logger instance.""" | 
 | 258 |     log = logging.getLogger("pyinotify") | 
 | 259 |     console_handler = logging.StreamHandler() | 
 | 260 |     console_handler.setFormatter( | 
 | 261 |         logging.Formatter("[%(asctime)s %(name)s %(levelname)s] %(message)s")) | 
 | 262 |     log.addHandler(console_handler) | 
 | 263 |     log.setLevel(20) | 
 | 264 |     return log | 
 | 265 |  | 
 | 266 | log = logger_init() | 
 | 267 |  | 
 | 268 |  | 
 | 269 | # inotify's variables | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 270 | class ProcINotify: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 271 |     """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 272 |     Access (read, write) inotify's variables through /proc/sys/. Note that | 
 | 273 |     usually it requires administrator rights to update them. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 274 |  | 
 | 275 |     Examples: | 
 | 276 |       - Read max_queued_events attribute: myvar = max_queued_events.value | 
 | 277 |       - Update max_queued_events attribute: max_queued_events.value = 42 | 
 | 278 |     """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 279 |     def __init__(self, attr): | 
 | 280 |         self._base = "/proc/sys/fs/inotify" | 
 | 281 |         self._attr = attr | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 282 |  | 
 | 283 |     def get_val(self): | 
 | 284 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 285 |         Gets attribute's value. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 286 |  | 
 | 287 |         @return: stored value. | 
 | 288 |         @rtype: int | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 289 |         @raise IOError: if corresponding file in /proc/sys cannot be read. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 290 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 291 |         with open(os.path.join(self._base, self._attr), 'r') as file_obj: | 
 | 292 |             return int(file_obj.readline()) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 293 |  | 
 | 294 |     def set_val(self, nval): | 
 | 295 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 296 |         Sets new attribute's value. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 297 |  | 
 | 298 |         @param nval: replaces current value by nval. | 
 | 299 |         @type nval: int | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 300 |         @raise IOError: if corresponding file in /proc/sys cannot be written. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 301 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 302 |         with open(os.path.join(self._base, self._attr), 'w') as file_obj: | 
 | 303 |             file_obj.write(str(nval) + '\n') | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 304 |  | 
 | 305 |     value = property(get_val, set_val) | 
 | 306 |  | 
 | 307 |     def __repr__(self): | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 308 |         return '<%s=%d>' % (self._attr, self.get_val()) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 309 |  | 
 | 310 |  | 
 | 311 | # Inotify's variables | 
 | 312 | # | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 313 | # Note: may raise IOError if the corresponding value in /proc/sys | 
 | 314 | #       cannot be accessed. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 315 | # | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 316 | # Examples: | 
 | 317 | #  - read: myvar = max_queued_events.value | 
 | 318 | #  - update: max_queued_events.value = 42 | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 319 | # | 
 | 320 | for attrname in ('max_queued_events', 'max_user_instances', 'max_user_watches'): | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 321 |     globals()[attrname] = ProcINotify(attrname) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 322 |  | 
 | 323 |  | 
 | 324 | class EventsCodes: | 
 | 325 |     """ | 
 | 326 |     Set of codes corresponding to each kind of events. | 
 | 327 |     Some of these flags are used to communicate with inotify, whereas | 
 | 328 |     the others are sent to userspace by inotify notifying some events. | 
 | 329 |  | 
 | 330 |     @cvar IN_ACCESS: File was accessed. | 
 | 331 |     @type IN_ACCESS: int | 
 | 332 |     @cvar IN_MODIFY: File was modified. | 
 | 333 |     @type IN_MODIFY: int | 
 | 334 |     @cvar IN_ATTRIB: Metadata changed. | 
 | 335 |     @type IN_ATTRIB: int | 
 | 336 |     @cvar IN_CLOSE_WRITE: Writtable file was closed. | 
 | 337 |     @type IN_CLOSE_WRITE: int | 
 | 338 |     @cvar IN_CLOSE_NOWRITE: Unwrittable file closed. | 
 | 339 |     @type IN_CLOSE_NOWRITE: int | 
 | 340 |     @cvar IN_OPEN: File was opened. | 
 | 341 |     @type IN_OPEN: int | 
 | 342 |     @cvar IN_MOVED_FROM: File was moved from X. | 
 | 343 |     @type IN_MOVED_FROM: int | 
 | 344 |     @cvar IN_MOVED_TO: File was moved to Y. | 
 | 345 |     @type IN_MOVED_TO: int | 
 | 346 |     @cvar IN_CREATE: Subfile was created. | 
 | 347 |     @type IN_CREATE: int | 
 | 348 |     @cvar IN_DELETE: Subfile was deleted. | 
 | 349 |     @type IN_DELETE: int | 
 | 350 |     @cvar IN_DELETE_SELF: Self (watched item itself) was deleted. | 
 | 351 |     @type IN_DELETE_SELF: int | 
 | 352 |     @cvar IN_MOVE_SELF: Self (watched item itself) was moved. | 
 | 353 |     @type IN_MOVE_SELF: int | 
 | 354 |     @cvar IN_UNMOUNT: Backing fs was unmounted. | 
 | 355 |     @type IN_UNMOUNT: int | 
 | 356 |     @cvar IN_Q_OVERFLOW: Event queued overflowed. | 
 | 357 |     @type IN_Q_OVERFLOW: int | 
 | 358 |     @cvar IN_IGNORED: File was ignored. | 
 | 359 |     @type IN_IGNORED: int | 
 | 360 |     @cvar IN_ONLYDIR: only watch the path if it is a directory (new | 
 | 361 |                       in kernel 2.6.15). | 
 | 362 |     @type IN_ONLYDIR: int | 
 | 363 |     @cvar IN_DONT_FOLLOW: don't follow a symlink (new in kernel 2.6.15). | 
 | 364 |                           IN_ONLYDIR we can make sure that we don't watch | 
 | 365 |                           the target of symlinks. | 
 | 366 |     @type IN_DONT_FOLLOW: int | 
 | 367 |     @cvar IN_EXCL_UNLINK: Events are not generated for children after they | 
 | 368 |                           have been unlinked from the watched directory. | 
 | 369 |                           (new in kernel 2.6.36). | 
 | 370 |     @type IN_EXCL_UNLINK: int | 
 | 371 |     @cvar IN_MASK_ADD: add to the mask of an already existing watch (new | 
 | 372 |                        in kernel 2.6.14). | 
 | 373 |     @type IN_MASK_ADD: int | 
 | 374 |     @cvar IN_ISDIR: Event occurred against dir. | 
 | 375 |     @type IN_ISDIR: int | 
 | 376 |     @cvar IN_ONESHOT: Only send event once. | 
 | 377 |     @type IN_ONESHOT: int | 
 | 378 |     @cvar ALL_EVENTS: Alias for considering all of the events. | 
 | 379 |     @type ALL_EVENTS: int | 
 | 380 |     """ | 
 | 381 |  | 
 | 382 |     # The idea here is 'configuration-as-code' - this way, we get our nice class | 
 | 383 |     # constants, but we also get nice human-friendly text mappings to do lookups | 
 | 384 |     # against as well, for free: | 
 | 385 |     FLAG_COLLECTIONS = {'OP_FLAGS': { | 
 | 386 |         'IN_ACCESS'        : 0x00000001,  # File was accessed | 
 | 387 |         'IN_MODIFY'        : 0x00000002,  # File was modified | 
 | 388 |         'IN_ATTRIB'        : 0x00000004,  # Metadata changed | 
 | 389 |         'IN_CLOSE_WRITE'   : 0x00000008,  # Writable file was closed | 
 | 390 |         'IN_CLOSE_NOWRITE' : 0x00000010,  # Unwritable file closed | 
 | 391 |         'IN_OPEN'          : 0x00000020,  # File was opened | 
 | 392 |         'IN_MOVED_FROM'    : 0x00000040,  # File was moved from X | 
 | 393 |         'IN_MOVED_TO'      : 0x00000080,  # File was moved to Y | 
 | 394 |         'IN_CREATE'        : 0x00000100,  # Subfile was created | 
 | 395 |         'IN_DELETE'        : 0x00000200,  # Subfile was deleted | 
 | 396 |         'IN_DELETE_SELF'   : 0x00000400,  # Self (watched item itself) | 
 | 397 |                                           # was deleted | 
 | 398 |         'IN_MOVE_SELF'     : 0x00000800,  # Self (watched item itself) was moved | 
 | 399 |         }, | 
 | 400 |                         'EVENT_FLAGS': { | 
 | 401 |         'IN_UNMOUNT'       : 0x00002000,  # Backing fs was unmounted | 
 | 402 |         'IN_Q_OVERFLOW'    : 0x00004000,  # Event queued overflowed | 
 | 403 |         'IN_IGNORED'       : 0x00008000,  # File was ignored | 
 | 404 |         }, | 
 | 405 |                         'SPECIAL_FLAGS': { | 
 | 406 |         'IN_ONLYDIR'       : 0x01000000,  # only watch the path if it is a | 
 | 407 |                                           # directory | 
 | 408 |         'IN_DONT_FOLLOW'   : 0x02000000,  # don't follow a symlink | 
 | 409 |         'IN_EXCL_UNLINK'   : 0x04000000,  # exclude events on unlinked objects | 
 | 410 |         'IN_MASK_ADD'      : 0x20000000,  # add to the mask of an already | 
 | 411 |                                           # existing watch | 
 | 412 |         'IN_ISDIR'         : 0x40000000,  # event occurred against dir | 
 | 413 |         'IN_ONESHOT'       : 0x80000000,  # only send event once | 
 | 414 |         }, | 
 | 415 |                         } | 
 | 416 |  | 
 | 417 |     def maskname(mask): | 
 | 418 |         """ | 
 | 419 |         Returns the event name associated to mask. IN_ISDIR is appended to | 
 | 420 |         the result when appropriate. Note: only one event is returned, because | 
 | 421 |         only one event can be raised at a given time. | 
 | 422 |  | 
 | 423 |         @param mask: mask. | 
 | 424 |         @type mask: int | 
 | 425 |         @return: event name. | 
 | 426 |         @rtype: str | 
 | 427 |         """ | 
 | 428 |         ms = mask | 
 | 429 |         name = '%s' | 
 | 430 |         if mask & IN_ISDIR: | 
 | 431 |             ms = mask - IN_ISDIR | 
 | 432 |             name = '%s|IN_ISDIR' | 
 | 433 |         return name % EventsCodes.ALL_VALUES[ms] | 
 | 434 |  | 
 | 435 |     maskname = staticmethod(maskname) | 
 | 436 |  | 
 | 437 |  | 
 | 438 | # So let's now turn the configuration into code | 
 | 439 | EventsCodes.ALL_FLAGS = {} | 
 | 440 | EventsCodes.ALL_VALUES = {} | 
 | 441 | for flagc, valc in EventsCodes.FLAG_COLLECTIONS.items(): | 
 | 442 |     # Make the collections' members directly accessible through the | 
 | 443 |     # class dictionary | 
 | 444 |     setattr(EventsCodes, flagc, valc) | 
 | 445 |  | 
 | 446 |     # Collect all the flags under a common umbrella | 
 | 447 |     EventsCodes.ALL_FLAGS.update(valc) | 
 | 448 |  | 
 | 449 |     # Make the individual masks accessible as 'constants' at globals() scope | 
 | 450 |     # and masknames accessible by values. | 
 | 451 |     for name, val in valc.items(): | 
 | 452 |         globals()[name] = val | 
 | 453 |         EventsCodes.ALL_VALUES[val] = name | 
 | 454 |  | 
 | 455 |  | 
 | 456 | # all 'normal' events | 
 | 457 | ALL_EVENTS = reduce(lambda x, y: x | y, EventsCodes.OP_FLAGS.values()) | 
 | 458 | EventsCodes.ALL_FLAGS['ALL_EVENTS'] = ALL_EVENTS | 
 | 459 | EventsCodes.ALL_VALUES[ALL_EVENTS] = 'ALL_EVENTS' | 
 | 460 |  | 
 | 461 |  | 
 | 462 | class _Event: | 
 | 463 |     """ | 
 | 464 |     Event structure, represent events raised by the system. This | 
 | 465 |     is the base class and should be subclassed. | 
 | 466 |  | 
 | 467 |     """ | 
 | 468 |     def __init__(self, dict_): | 
 | 469 |         """ | 
 | 470 |         Attach attributes (contained in dict_) to self. | 
 | 471 |  | 
 | 472 |         @param dict_: Set of attributes. | 
 | 473 |         @type dict_: dictionary | 
 | 474 |         """ | 
 | 475 |         for tpl in dict_.items(): | 
 | 476 |             setattr(self, *tpl) | 
 | 477 |  | 
 | 478 |     def __repr__(self): | 
 | 479 |         """ | 
 | 480 |         @return: Generic event string representation. | 
 | 481 |         @rtype: str | 
 | 482 |         """ | 
 | 483 |         s = '' | 
 | 484 |         for attr, value in sorted(self.__dict__.items(), key=lambda x: x[0]): | 
 | 485 |             if attr.startswith('_'): | 
 | 486 |                 continue | 
 | 487 |             if attr == 'mask': | 
 | 488 |                 value = hex(getattr(self, attr)) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 489 |             elif isinstance(value, str) and not value: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 490 |                 value = "''" | 
 | 491 |             s += ' %s%s%s' % (output_format.field_name(attr), | 
 | 492 |                               output_format.punctuation('='), | 
 | 493 |                               output_format.field_value(value)) | 
 | 494 |  | 
 | 495 |         s = '%s%s%s %s' % (output_format.punctuation('<'), | 
 | 496 |                            output_format.class_name(self.__class__.__name__), | 
 | 497 |                            s, | 
 | 498 |                            output_format.punctuation('>')) | 
 | 499 |         return s | 
 | 500 |  | 
 | 501 |     def __str__(self): | 
 | 502 |         return repr(self) | 
 | 503 |  | 
 | 504 |  | 
 | 505 | class _RawEvent(_Event): | 
 | 506 |     """ | 
 | 507 |     Raw event, it contains only the informations provided by the system. | 
 | 508 |     It doesn't infer anything. | 
 | 509 |     """ | 
 | 510 |     def __init__(self, wd, mask, cookie, name): | 
 | 511 |         """ | 
 | 512 |         @param wd: Watch Descriptor. | 
 | 513 |         @type wd: int | 
 | 514 |         @param mask: Bitmask of events. | 
 | 515 |         @type mask: int | 
 | 516 |         @param cookie: Cookie. | 
 | 517 |         @type cookie: int | 
 | 518 |         @param name: Basename of the file or directory against which the | 
 | 519 |                      event was raised in case where the watched directory | 
 | 520 |                      is the parent directory. None if the event was raised | 
 | 521 |                      on the watched item itself. | 
 | 522 |         @type name: string or None | 
 | 523 |         """ | 
 | 524 |         # Use this variable to cache the result of str(self), this object | 
 | 525 |         # is immutable. | 
 | 526 |         self._str = None | 
 | 527 |         # name: remove trailing '\0' | 
 | 528 |         d = {'wd': wd, | 
 | 529 |              'mask': mask, | 
 | 530 |              'cookie': cookie, | 
 | 531 |              'name': name.rstrip('\0')} | 
 | 532 |         _Event.__init__(self, d) | 
 | 533 |         log.debug(str(self)) | 
 | 534 |  | 
 | 535 |     def __str__(self): | 
 | 536 |         if self._str is None: | 
 | 537 |             self._str = _Event.__str__(self) | 
 | 538 |         return self._str | 
 | 539 |  | 
 | 540 |  | 
 | 541 | class Event(_Event): | 
 | 542 |     """ | 
 | 543 |     This class contains all the useful informations about the observed | 
 | 544 |     event. However, the presence of each field is not guaranteed and | 
 | 545 |     depends on the type of event. In effect, some fields are irrelevant | 
 | 546 |     for some kind of event (for example 'cookie' is meaningless for | 
 | 547 |     IN_CREATE whereas it is mandatory for IN_MOVE_TO). | 
 | 548 |  | 
 | 549 |     The possible fields are: | 
 | 550 |       - wd (int): Watch Descriptor. | 
 | 551 |       - mask (int): Mask. | 
 | 552 |       - maskname (str): Readable event name. | 
 | 553 |       - path (str): path of the file or directory being watched. | 
 | 554 |       - name (str): Basename of the file or directory against which the | 
 | 555 |               event was raised in case where the watched directory | 
 | 556 |               is the parent directory. None if the event was raised | 
 | 557 |               on the watched item itself. This field is always provided | 
 | 558 |               even if the string is ''. | 
 | 559 |       - pathname (str): Concatenation of 'path' and 'name'. | 
 | 560 |       - src_pathname (str): Only present for IN_MOVED_TO events and only in | 
 | 561 |               the case where IN_MOVED_FROM events are watched too. Holds the | 
 | 562 |               source pathname from where pathname was moved from. | 
 | 563 |       - cookie (int): Cookie. | 
 | 564 |       - dir (bool): True if the event was raised against a directory. | 
 | 565 |  | 
 | 566 |     """ | 
 | 567 |     def __init__(self, raw): | 
 | 568 |         """ | 
 | 569 |         Concretely, this is the raw event plus inferred infos. | 
 | 570 |         """ | 
 | 571 |         _Event.__init__(self, raw) | 
 | 572 |         self.maskname = EventsCodes.maskname(self.mask) | 
 | 573 |         if COMPATIBILITY_MODE: | 
 | 574 |             self.event_name = self.maskname | 
 | 575 |         try: | 
 | 576 |             if self.name: | 
 | 577 |                 self.pathname = os.path.abspath(os.path.join(self.path, | 
 | 578 |                                                              self.name)) | 
 | 579 |             else: | 
 | 580 |                 self.pathname = os.path.abspath(self.path) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 581 |         except AttributeError as err: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 582 |             # Usually it is not an error some events are perfectly valids | 
 | 583 |             # despite the lack of these attributes. | 
 | 584 |             log.debug(err) | 
 | 585 |  | 
 | 586 |  | 
 | 587 | class ProcessEventError(PyinotifyError): | 
 | 588 |     """ | 
 | 589 |     ProcessEventError Exception. Raised on ProcessEvent error. | 
 | 590 |     """ | 
 | 591 |     def __init__(self, err): | 
 | 592 |         """ | 
 | 593 |         @param err: Exception error description. | 
 | 594 |         @type err: string | 
 | 595 |         """ | 
 | 596 |         PyinotifyError.__init__(self, err) | 
 | 597 |  | 
 | 598 |  | 
 | 599 | class _ProcessEvent: | 
 | 600 |     """ | 
 | 601 |     Abstract processing event class. | 
 | 602 |     """ | 
 | 603 |     def __call__(self, event): | 
 | 604 |         """ | 
 | 605 |         To behave like a functor the object must be callable. | 
 | 606 |         This method is a dispatch method. Its lookup order is: | 
 | 607 |           1. process_MASKNAME method | 
 | 608 |           2. process_FAMILY_NAME method | 
 | 609 |           3. otherwise calls process_default | 
 | 610 |  | 
 | 611 |         @param event: Event to be processed. | 
 | 612 |         @type event: Event object | 
 | 613 |         @return: By convention when used from the ProcessEvent class: | 
 | 614 |                  - Returning False or None (default value) means keep on | 
 | 615 |                  executing next chained functors (see chain.py example). | 
 | 616 |                  - Returning True instead means do not execute next | 
 | 617 |                    processing functions. | 
 | 618 |         @rtype: bool | 
 | 619 |         @raise ProcessEventError: Event object undispatchable, | 
 | 620 |                                   unknown event. | 
 | 621 |         """ | 
 | 622 |         stripped_mask = event.mask - (event.mask & IN_ISDIR) | 
 | 623 |         maskname = EventsCodes.ALL_VALUES.get(stripped_mask) | 
 | 624 |         if maskname is None: | 
 | 625 |             raise ProcessEventError("Unknown mask 0x%08x" % stripped_mask) | 
 | 626 |  | 
 | 627 |         # 1- look for process_MASKNAME | 
 | 628 |         meth = getattr(self, 'process_' + maskname, None) | 
 | 629 |         if meth is not None: | 
 | 630 |             return meth(event) | 
 | 631 |         # 2- look for process_FAMILY_NAME | 
 | 632 |         meth = getattr(self, 'process_IN_' + maskname.split('_')[1], None) | 
 | 633 |         if meth is not None: | 
 | 634 |             return meth(event) | 
 | 635 |         # 3- default call method process_default | 
 | 636 |         return self.process_default(event) | 
 | 637 |  | 
 | 638 |     def __repr__(self): | 
 | 639 |         return '<%s>' % self.__class__.__name__ | 
 | 640 |  | 
 | 641 |  | 
 | 642 | class _SysProcessEvent(_ProcessEvent): | 
 | 643 |     """ | 
 | 644 |     There is three kind of processing according to each event: | 
 | 645 |  | 
 | 646 |       1. special handling (deletion from internal container, bug, ...). | 
 | 647 |       2. default treatment: which is applied to the majority of events. | 
 | 648 |       3. IN_ISDIR is never sent alone, he is piggybacked with a standard | 
 | 649 |          event, he is not processed as the others events, instead, its | 
 | 650 |          value is captured and appropriately aggregated to dst event. | 
 | 651 |     """ | 
 | 652 |     def __init__(self, wm, notifier): | 
 | 653 |         """ | 
 | 654 |  | 
 | 655 |         @param wm: Watch Manager. | 
 | 656 |         @type wm: WatchManager instance | 
 | 657 |         @param notifier: Notifier. | 
 | 658 |         @type notifier: Notifier instance | 
 | 659 |         """ | 
 | 660 |         self._watch_manager = wm  # watch manager | 
 | 661 |         self._notifier = notifier  # notifier | 
 | 662 |         self._mv_cookie = {}  # {cookie(int): (src_path(str), date), ...} | 
 | 663 |         self._mv = {}  # {src_path(str): (dst_path(str), date), ...} | 
 | 664 |  | 
 | 665 |     def cleanup(self): | 
 | 666 |         """ | 
 | 667 |         Cleanup (delete) old (>1mn) records contained in self._mv_cookie | 
 | 668 |         and self._mv. | 
 | 669 |         """ | 
 | 670 |         date_cur_ = datetime.now() | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 671 |         for seq in (self._mv_cookie, self._mv): | 
 | 672 |             for k in list(seq.keys()): | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 673 |                 if (date_cur_ - seq[k][1]) > timedelta(minutes=1): | 
 | 674 |                     log.debug('Cleanup: deleting entry %s', seq[k][0]) | 
 | 675 |                     del seq[k] | 
 | 676 |  | 
 | 677 |     def process_IN_CREATE(self, raw_event): | 
 | 678 |         """ | 
 | 679 |         If the event affects a directory and the auto_add flag of the | 
 | 680 |         targetted watch is set to True, a new watch is added on this | 
 | 681 |         new directory, with the same attribute values than those of | 
 | 682 |         this watch. | 
 | 683 |         """ | 
 | 684 |         if raw_event.mask & IN_ISDIR: | 
 | 685 |             watch_ = self._watch_manager.get_watch(raw_event.wd) | 
 | 686 |             created_dir = os.path.join(watch_.path, raw_event.name) | 
 | 687 |             if watch_.auto_add and not watch_.exclude_filter(created_dir): | 
 | 688 |                 addw = self._watch_manager.add_watch | 
 | 689 |                 # The newly monitored directory inherits attributes from its | 
 | 690 |                 # parent directory. | 
 | 691 |                 addw_ret = addw(created_dir, watch_.mask, | 
 | 692 |                                 proc_fun=watch_.proc_fun, | 
 | 693 |                                 rec=False, auto_add=watch_.auto_add, | 
 | 694 |                                 exclude_filter=watch_.exclude_filter) | 
 | 695 |  | 
 | 696 |                 # Trick to handle mkdir -p /d1/d2/t3 where d1 is watched and | 
 | 697 |                 # d2 and t3 (directory or file) are created. | 
 | 698 |                 # Since the directory d2 is new, then everything inside it must | 
 | 699 |                 # also be new. | 
 | 700 |                 created_dir_wd = addw_ret.get(created_dir) | 
 | 701 |                 if ((created_dir_wd is not None) and (created_dir_wd > 0) and | 
 | 702 |                     os.path.isdir(created_dir)): | 
 | 703 |                     try: | 
 | 704 |                         for name in os.listdir(created_dir): | 
 | 705 |                             inner = os.path.join(created_dir, name) | 
 | 706 |                             if self._watch_manager.get_wd(inner) is not None: | 
 | 707 |                                 continue | 
 | 708 |                             # Generate (simulate) creation events for sub- | 
 | 709 |                             # directories and files. | 
 | 710 |                             if os.path.isfile(inner): | 
 | 711 |                                 # symlinks are handled as files. | 
 | 712 |                                 flags = IN_CREATE | 
 | 713 |                             elif os.path.isdir(inner): | 
 | 714 |                                 flags = IN_CREATE | IN_ISDIR | 
 | 715 |                             else: | 
 | 716 |                                 # This path should not be taken. | 
 | 717 |                                 continue | 
 | 718 |                             rawevent = _RawEvent(created_dir_wd, flags, 0, name) | 
 | 719 |                             self._notifier.append_event(rawevent) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 720 |                     except OSError as err: | 
 | 721 |                         msg = "process_IN_CREATE, invalid directory: %s" | 
 | 722 |                         log.debug(msg % str(err)) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 723 |         return self.process_default(raw_event) | 
 | 724 |  | 
 | 725 |     def process_IN_MOVED_FROM(self, raw_event): | 
 | 726 |         """ | 
 | 727 |         Map the cookie with the source path (+ date for cleaning). | 
 | 728 |         """ | 
 | 729 |         watch_ = self._watch_manager.get_watch(raw_event.wd) | 
 | 730 |         path_ = watch_.path | 
 | 731 |         src_path = os.path.normpath(os.path.join(path_, raw_event.name)) | 
 | 732 |         self._mv_cookie[raw_event.cookie] = (src_path, datetime.now()) | 
 | 733 |         return self.process_default(raw_event, {'cookie': raw_event.cookie}) | 
 | 734 |  | 
 | 735 |     def process_IN_MOVED_TO(self, raw_event): | 
 | 736 |         """ | 
 | 737 |         Map the source path with the destination path (+ date for | 
 | 738 |         cleaning). | 
 | 739 |         """ | 
 | 740 |         watch_ = self._watch_manager.get_watch(raw_event.wd) | 
 | 741 |         path_ = watch_.path | 
 | 742 |         dst_path = os.path.normpath(os.path.join(path_, raw_event.name)) | 
 | 743 |         mv_ = self._mv_cookie.get(raw_event.cookie) | 
 | 744 |         to_append = {'cookie': raw_event.cookie} | 
 | 745 |         if mv_ is not None: | 
 | 746 |             self._mv[mv_[0]] = (dst_path, datetime.now()) | 
 | 747 |             # Let's assume that IN_MOVED_FROM event is always queued before | 
 | 748 |             # that its associated (they share a common cookie) IN_MOVED_TO | 
 | 749 |             # event is queued itself. It is then possible in that scenario | 
 | 750 |             # to provide as additional information to the IN_MOVED_TO event | 
 | 751 |             # the original pathname of the moved file/directory. | 
 | 752 |             to_append['src_pathname'] = mv_[0] | 
 | 753 |         elif (raw_event.mask & IN_ISDIR and watch_.auto_add and | 
 | 754 |               not watch_.exclude_filter(dst_path)): | 
 | 755 |             # We got a diretory that's "moved in" from an unknown source and | 
 | 756 |             # auto_add is enabled. Manually add watches to the inner subtrees. | 
 | 757 |             # The newly monitored directory inherits attributes from its | 
 | 758 |             # parent directory. | 
 | 759 |             self._watch_manager.add_watch(dst_path, watch_.mask, | 
 | 760 |                                           proc_fun=watch_.proc_fun, | 
 | 761 |                                           rec=True, auto_add=True, | 
 | 762 |                                           exclude_filter=watch_.exclude_filter) | 
 | 763 |         return self.process_default(raw_event, to_append) | 
 | 764 |  | 
 | 765 |     def process_IN_MOVE_SELF(self, raw_event): | 
 | 766 |         """ | 
 | 767 |         STATUS: the following bug has been fixed in recent kernels (FIXME: | 
 | 768 |         which version ?). Now it raises IN_DELETE_SELF instead. | 
 | 769 |  | 
 | 770 |         Old kernels were bugged, this event raised when the watched item | 
 | 771 |         were moved, so we had to update its path, but under some circumstances | 
 | 772 |         it was impossible: if its parent directory and its destination | 
 | 773 |         directory wasn't watched. The kernel (see include/linux/fsnotify.h) | 
 | 774 |         doesn't bring us enough informations like the destination path of | 
 | 775 |         moved items. | 
 | 776 |         """ | 
 | 777 |         watch_ = self._watch_manager.get_watch(raw_event.wd) | 
 | 778 |         src_path = watch_.path | 
 | 779 |         mv_ = self._mv.get(src_path) | 
 | 780 |         if mv_: | 
 | 781 |             dest_path = mv_[0] | 
 | 782 |             watch_.path = dest_path | 
 | 783 |             # add the separator to the source path to avoid overlapping | 
 | 784 |             # path issue when testing with startswith() | 
 | 785 |             src_path += os.path.sep | 
 | 786 |             src_path_len = len(src_path) | 
 | 787 |             # The next loop renames all watches with src_path as base path. | 
 | 788 |             # It seems that IN_MOVE_SELF does not provide IN_ISDIR information | 
 | 789 |             # therefore the next loop is iterated even if raw_event is a file. | 
 | 790 |             for w in self._watch_manager.watches.values(): | 
 | 791 |                 if w.path.startswith(src_path): | 
 | 792 |                     # Note that dest_path is a normalized path. | 
 | 793 |                     w.path = os.path.join(dest_path, w.path[src_path_len:]) | 
 | 794 |         else: | 
 | 795 |             log.error("The pathname '%s' of this watch %s has probably changed " | 
 | 796 |                       "and couldn't be updated, so it cannot be trusted " | 
 | 797 |                       "anymore. To fix this error move directories/files only " | 
 | 798 |                       "between watched parents directories, in this case e.g. " | 
 | 799 |                       "put a watch on '%s'.", | 
 | 800 |                       watch_.path, watch_, | 
 | 801 |                       os.path.normpath(os.path.join(watch_.path, | 
 | 802 |                                                     os.path.pardir))) | 
 | 803 |             if not watch_.path.endswith('-unknown-path'): | 
 | 804 |                 watch_.path += '-unknown-path' | 
 | 805 |         return self.process_default(raw_event) | 
 | 806 |  | 
 | 807 |     def process_IN_Q_OVERFLOW(self, raw_event): | 
 | 808 |         """ | 
 | 809 |         Only signal an overflow, most of the common flags are irrelevant | 
 | 810 |         for this event (path, wd, name). | 
 | 811 |         """ | 
 | 812 |         return Event({'mask': raw_event.mask}) | 
 | 813 |  | 
 | 814 |     def process_IN_IGNORED(self, raw_event): | 
 | 815 |         """ | 
 | 816 |         The watch descriptor raised by this event is now ignored (forever), | 
 | 817 |         it can be safely deleted from the watch manager dictionary. | 
 | 818 |         After this event we can be sure that neither the event queue nor | 
 | 819 |         the system will raise an event associated to this wd again. | 
 | 820 |         """ | 
 | 821 |         event_ = self.process_default(raw_event) | 
 | 822 |         self._watch_manager.del_watch(raw_event.wd) | 
 | 823 |         return event_ | 
 | 824 |  | 
 | 825 |     def process_default(self, raw_event, to_append=None): | 
 | 826 |         """ | 
 | 827 |         Commons handling for the followings events: | 
 | 828 |  | 
 | 829 |         IN_ACCESS, IN_MODIFY, IN_ATTRIB, IN_CLOSE_WRITE, IN_CLOSE_NOWRITE, | 
 | 830 |         IN_OPEN, IN_DELETE, IN_DELETE_SELF, IN_UNMOUNT. | 
 | 831 |         """ | 
 | 832 |         watch_ = self._watch_manager.get_watch(raw_event.wd) | 
 | 833 |         if raw_event.mask & (IN_DELETE_SELF | IN_MOVE_SELF): | 
 | 834 |             # Unfornulately this information is not provided by the kernel | 
 | 835 |             dir_ = watch_.dir | 
 | 836 |         else: | 
 | 837 |             dir_ = bool(raw_event.mask & IN_ISDIR) | 
 | 838 |         dict_ = {'wd': raw_event.wd, | 
 | 839 |                  'mask': raw_event.mask, | 
 | 840 |                  'path': watch_.path, | 
 | 841 |                  'name': raw_event.name, | 
 | 842 |                  'dir': dir_} | 
 | 843 |         if COMPATIBILITY_MODE: | 
 | 844 |             dict_['is_dir'] = dir_ | 
 | 845 |         if to_append is not None: | 
 | 846 |             dict_.update(to_append) | 
 | 847 |         return Event(dict_) | 
 | 848 |  | 
 | 849 |  | 
 | 850 | class ProcessEvent(_ProcessEvent): | 
 | 851 |     """ | 
 | 852 |     Process events objects, can be specialized via subclassing, thus its | 
 | 853 |     behavior can be overriden: | 
 | 854 |  | 
 | 855 |     Note: you should not override __init__ in your subclass instead define | 
 | 856 |     a my_init() method, this method will be called automatically from the | 
 | 857 |     constructor of this class with its optionals parameters. | 
 | 858 |  | 
 | 859 |       1. Provide specialized individual methods, e.g. process_IN_DELETE for | 
 | 860 |          processing a precise type of event (e.g. IN_DELETE in this case). | 
 | 861 |       2. Or/and provide methods for processing events by 'family', e.g. | 
 | 862 |          process_IN_CLOSE method will process both IN_CLOSE_WRITE and | 
 | 863 |          IN_CLOSE_NOWRITE events (if process_IN_CLOSE_WRITE and | 
 | 864 |          process_IN_CLOSE_NOWRITE aren't defined though). | 
 | 865 |       3. Or/and override process_default for catching and processing all | 
 | 866 |          the remaining types of events. | 
 | 867 |     """ | 
 | 868 |     pevent = None | 
 | 869 |  | 
 | 870 |     def __init__(self, pevent=None, **kargs): | 
 | 871 |         """ | 
 | 872 |         Enable chaining of ProcessEvent instances. | 
 | 873 |  | 
 | 874 |         @param pevent: Optional callable object, will be called on event | 
 | 875 |                        processing (before self). | 
 | 876 |         @type pevent: callable | 
 | 877 |         @param kargs: This constructor is implemented as a template method | 
 | 878 |                       delegating its optionals keyworded arguments to the | 
 | 879 |                       method my_init(). | 
 | 880 |         @type kargs: dict | 
 | 881 |         """ | 
 | 882 |         self.pevent = pevent | 
 | 883 |         self.my_init(**kargs) | 
 | 884 |  | 
 | 885 |     def my_init(self, **kargs): | 
 | 886 |         """ | 
 | 887 |         This method is called from ProcessEvent.__init__(). This method is | 
 | 888 |         empty here and must be redefined to be useful. In effect, if you | 
 | 889 |         need to specifically initialize your subclass' instance then you | 
 | 890 |         just have to override this method in your subclass. Then all the | 
 | 891 |         keyworded arguments passed to ProcessEvent.__init__() will be | 
 | 892 |         transmitted as parameters to this method. Beware you MUST pass | 
 | 893 |         keyword arguments though. | 
 | 894 |  | 
 | 895 |         @param kargs: optional delegated arguments from __init__(). | 
 | 896 |         @type kargs: dict | 
 | 897 |         """ | 
 | 898 |         pass | 
 | 899 |  | 
 | 900 |     def __call__(self, event): | 
 | 901 |         stop_chaining = False | 
 | 902 |         if self.pevent is not None: | 
 | 903 |             # By default methods return None so we set as guideline | 
 | 904 |             # that methods asking for stop chaining must explicitely | 
 | 905 |             # return non None or non False values, otherwise the default | 
 | 906 |             # behavior will be to accept chain call to the corresponding | 
 | 907 |             # local method. | 
 | 908 |             stop_chaining = self.pevent(event) | 
 | 909 |         if not stop_chaining: | 
 | 910 |             return _ProcessEvent.__call__(self, event) | 
 | 911 |  | 
 | 912 |     def nested_pevent(self): | 
 | 913 |         return self.pevent | 
 | 914 |  | 
 | 915 |     def process_IN_Q_OVERFLOW(self, event): | 
 | 916 |         """ | 
 | 917 |         By default this method only reports warning messages, you can overredide | 
 | 918 |         it by subclassing ProcessEvent and implement your own | 
 | 919 |         process_IN_Q_OVERFLOW method. The actions you can take on receiving this | 
 | 920 |         event is either to update the variable max_queued_events in order to | 
 | 921 |         handle more simultaneous events or to modify your code in order to | 
 | 922 |         accomplish a better filtering diminishing the number of raised events. | 
 | 923 |         Because this method is defined, IN_Q_OVERFLOW will never get | 
 | 924 |         transmitted as arguments to process_default calls. | 
 | 925 |  | 
 | 926 |         @param event: IN_Q_OVERFLOW event. | 
 | 927 |         @type event: dict | 
 | 928 |         """ | 
 | 929 |         log.warning('Event queue overflowed.') | 
 | 930 |  | 
 | 931 |     def process_default(self, event): | 
 | 932 |         """ | 
 | 933 |         Default processing event method. By default does nothing. Subclass | 
 | 934 |         ProcessEvent and redefine this method in order to modify its behavior. | 
 | 935 |  | 
 | 936 |         @param event: Event to be processed. Can be of any type of events but | 
 | 937 |                       IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW). | 
 | 938 |         @type event: Event instance | 
 | 939 |         """ | 
 | 940 |         pass | 
 | 941 |  | 
 | 942 |  | 
 | 943 | class PrintAllEvents(ProcessEvent): | 
 | 944 |     """ | 
 | 945 |     Dummy class used to print events strings representations. For instance this | 
 | 946 |     class is used from command line to print all received events to stdout. | 
 | 947 |     """ | 
 | 948 |     def my_init(self, out=None): | 
 | 949 |         """ | 
 | 950 |         @param out: Where events will be written. | 
 | 951 |         @type out: Object providing a valid file object interface. | 
 | 952 |         """ | 
 | 953 |         if out is None: | 
 | 954 |             out = sys.stdout | 
 | 955 |         self._out = out | 
 | 956 |  | 
 | 957 |     def process_default(self, event): | 
 | 958 |         """ | 
 | 959 |         Writes event string representation to file object provided to | 
 | 960 |         my_init(). | 
 | 961 |  | 
 | 962 |         @param event: Event to be processed. Can be of any type of events but | 
 | 963 |                       IN_Q_OVERFLOW events (see method process_IN_Q_OVERFLOW). | 
 | 964 |         @type event: Event instance | 
 | 965 |         """ | 
 | 966 |         self._out.write(str(event)) | 
 | 967 |         self._out.write('\n') | 
 | 968 |         self._out.flush() | 
 | 969 |  | 
 | 970 |  | 
 | 971 | class ChainIfTrue(ProcessEvent): | 
 | 972 |     """ | 
 | 973 |     Makes conditional chaining depending on the result of the nested | 
 | 974 |     processing instance. | 
 | 975 |     """ | 
 | 976 |     def my_init(self, func): | 
 | 977 |         """ | 
 | 978 |         Method automatically called from base class constructor. | 
 | 979 |         """ | 
 | 980 |         self._func = func | 
 | 981 |  | 
 | 982 |     def process_default(self, event): | 
 | 983 |         return not self._func(event) | 
 | 984 |  | 
 | 985 |  | 
 | 986 | class Stats(ProcessEvent): | 
 | 987 |     """ | 
 | 988 |     Compute and display trivial statistics about processed events. | 
 | 989 |     """ | 
 | 990 |     def my_init(self): | 
 | 991 |         """ | 
 | 992 |         Method automatically called from base class constructor. | 
 | 993 |         """ | 
 | 994 |         self._start_time = time.time() | 
 | 995 |         self._stats = {} | 
 | 996 |         self._stats_lock = threading.Lock() | 
 | 997 |  | 
 | 998 |     def process_default(self, event): | 
 | 999 |         """ | 
 | 1000 |         Processes |event|. | 
 | 1001 |         """ | 
 | 1002 |         self._stats_lock.acquire() | 
 | 1003 |         try: | 
 | 1004 |             events = event.maskname.split('|') | 
 | 1005 |             for event_name in events: | 
 | 1006 |                 count = self._stats.get(event_name, 0) | 
 | 1007 |                 self._stats[event_name] = count + 1 | 
 | 1008 |         finally: | 
 | 1009 |             self._stats_lock.release() | 
 | 1010 |  | 
 | 1011 |     def _stats_copy(self): | 
 | 1012 |         self._stats_lock.acquire() | 
 | 1013 |         try: | 
 | 1014 |             return self._stats.copy() | 
 | 1015 |         finally: | 
 | 1016 |             self._stats_lock.release() | 
 | 1017 |  | 
 | 1018 |     def __repr__(self): | 
 | 1019 |         stats = self._stats_copy() | 
 | 1020 |  | 
 | 1021 |         elapsed = int(time.time() - self._start_time) | 
 | 1022 |         elapsed_str = '' | 
 | 1023 |         if elapsed < 60: | 
 | 1024 |             elapsed_str = str(elapsed) + 'sec' | 
 | 1025 |         elif 60 <= elapsed < 3600: | 
 | 1026 |             elapsed_str = '%dmn%dsec' % (elapsed / 60, elapsed % 60) | 
 | 1027 |         elif 3600 <= elapsed < 86400: | 
 | 1028 |             elapsed_str = '%dh%dmn' % (elapsed / 3600, (elapsed % 3600) / 60) | 
 | 1029 |         elif elapsed >= 86400: | 
 | 1030 |             elapsed_str = '%dd%dh' % (elapsed / 86400, (elapsed % 86400) / 3600) | 
 | 1031 |         stats['ElapsedTime'] = elapsed_str | 
 | 1032 |  | 
 | 1033 |         l = [] | 
 | 1034 |         for ev, value in sorted(stats.items(), key=lambda x: x[0]): | 
 | 1035 |             l.append(' %s=%s' % (output_format.field_name(ev), | 
 | 1036 |                                  output_format.field_value(value))) | 
 | 1037 |         s = '<%s%s >' % (output_format.class_name(self.__class__.__name__), | 
 | 1038 |                          ''.join(l)) | 
 | 1039 |         return s | 
 | 1040 |  | 
 | 1041 |     def dump(self, filename): | 
 | 1042 |         """ | 
 | 1043 |         Dumps statistics. | 
 | 1044 |  | 
 | 1045 |         @param filename: filename where stats will be dumped, filename is | 
 | 1046 |                          created and must not exist prior to this call. | 
 | 1047 |         @type filename: string | 
 | 1048 |         """ | 
 | 1049 |         flags = os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW|os.O_EXCL | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1050 |         fd = os.open(filename, flags, 0o0600) | 
 | 1051 |         os.write(fd, bytes(self.__str__(), locale.getpreferredencoding())) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1052 |         os.close(fd) | 
 | 1053 |  | 
 | 1054 |     def __str__(self, scale=45): | 
 | 1055 |         stats = self._stats_copy() | 
 | 1056 |         if not stats: | 
 | 1057 |             return '' | 
 | 1058 |  | 
 | 1059 |         m = max(stats.values()) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1060 |         unity = scale / m | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1061 |         fmt = '%%-26s%%-%ds%%s' % (len(output_format.field_value('@' * scale)) | 
 | 1062 |                                    + 1) | 
 | 1063 |         def func(x): | 
 | 1064 |             return fmt % (output_format.field_name(x[0]), | 
 | 1065 |                           output_format.field_value('@' * int(x[1] * unity)), | 
 | 1066 |                           output_format.simple('%d' % x[1], 'yellow')) | 
 | 1067 |         s = '\n'.join(map(func, sorted(stats.items(), key=lambda x: x[0]))) | 
 | 1068 |         return s | 
 | 1069 |  | 
 | 1070 |  | 
 | 1071 | class NotifierError(PyinotifyError): | 
 | 1072 |     """ | 
 | 1073 |     Notifier Exception. Raised on Notifier error. | 
 | 1074 |  | 
 | 1075 |     """ | 
 | 1076 |     def __init__(self, err): | 
 | 1077 |         """ | 
 | 1078 |         @param err: Exception string's description. | 
 | 1079 |         @type err: string | 
 | 1080 |         """ | 
 | 1081 |         PyinotifyError.__init__(self, err) | 
 | 1082 |  | 
 | 1083 |  | 
 | 1084 | class Notifier: | 
 | 1085 |     """ | 
 | 1086 |     Read notifications, process events. | 
 | 1087 |  | 
 | 1088 |     """ | 
 | 1089 |     def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, | 
 | 1090 |                  threshold=0, timeout=None): | 
 | 1091 |         """ | 
 | 1092 |         Initialization. read_freq, threshold and timeout parameters are used | 
 | 1093 |         when looping. | 
 | 1094 |  | 
 | 1095 |         @param watch_manager: Watch Manager. | 
 | 1096 |         @type watch_manager: WatchManager instance | 
 | 1097 |         @param default_proc_fun: Default processing method. If None, a new | 
 | 1098 |                                  instance of PrintAllEvents will be assigned. | 
 | 1099 |         @type default_proc_fun: instance of ProcessEvent | 
 | 1100 |         @param read_freq: if read_freq == 0, events are read asap, | 
 | 1101 |                           if read_freq is > 0, this thread sleeps | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1102 |                           max(0, read_freq - (timeout / 1000)) seconds. But if | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1103 |                           timeout is None it may be different because | 
 | 1104 |                           poll is blocking waiting for something to read. | 
 | 1105 |         @type read_freq: int | 
 | 1106 |         @param threshold: File descriptor will be read only if the accumulated | 
 | 1107 |                           size to read becomes >= threshold. If != 0, you likely | 
 | 1108 |                           want to use it in combination with an appropriate | 
 | 1109 |                           value for read_freq because without that you would | 
 | 1110 |                           keep looping without really reading anything and that | 
 | 1111 |                           until the amount of events to read is >= threshold. | 
 | 1112 |                           At least with read_freq set you might sleep. | 
 | 1113 |         @type threshold: int | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1114 |         @param timeout: see read_freq above. If provided, it must be set in | 
 | 1115 |                         milliseconds. See | 
 | 1116 |                         https://docs.python.org/3/library/select.html#select.poll.poll | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1117 |         @type timeout: int | 
 | 1118 |         """ | 
 | 1119 |         # Watch Manager instance | 
 | 1120 |         self._watch_manager = watch_manager | 
 | 1121 |         # File descriptor | 
 | 1122 |         self._fd = self._watch_manager.get_fd() | 
 | 1123 |         # Poll object and registration | 
 | 1124 |         self._pollobj = select.poll() | 
 | 1125 |         self._pollobj.register(self._fd, select.POLLIN) | 
 | 1126 |         # This pipe is correctely initialized and used by ThreadedNotifier | 
 | 1127 |         self._pipe = (-1, -1) | 
 | 1128 |         # Event queue | 
 | 1129 |         self._eventq = deque() | 
 | 1130 |         # System processing functor, common to all events | 
 | 1131 |         self._sys_proc_fun = _SysProcessEvent(self._watch_manager, self) | 
 | 1132 |         # Default processing method | 
 | 1133 |         self._default_proc_fun = default_proc_fun | 
 | 1134 |         if default_proc_fun is None: | 
 | 1135 |             self._default_proc_fun = PrintAllEvents() | 
 | 1136 |         # Loop parameters | 
 | 1137 |         self._read_freq = read_freq | 
 | 1138 |         self._threshold = threshold | 
 | 1139 |         self._timeout = timeout | 
 | 1140 |         # Coalesce events option | 
 | 1141 |         self._coalesce = False | 
 | 1142 |         # set of str(raw_event), only used when coalesce option is True | 
 | 1143 |         self._eventset = set() | 
 | 1144 |  | 
 | 1145 |     def append_event(self, event): | 
 | 1146 |         """ | 
 | 1147 |         Append a raw event to the event queue. | 
 | 1148 |  | 
 | 1149 |         @param event: An event. | 
 | 1150 |         @type event: _RawEvent instance. | 
 | 1151 |         """ | 
 | 1152 |         self._eventq.append(event) | 
 | 1153 |  | 
 | 1154 |     def proc_fun(self): | 
 | 1155 |         return self._default_proc_fun | 
 | 1156 |  | 
 | 1157 |     def coalesce_events(self, coalesce=True): | 
 | 1158 |         """ | 
 | 1159 |         Coalescing events. Events are usually processed by batchs, their size | 
 | 1160 |         depend on various factors. Thus, before processing them, events received | 
 | 1161 |         from inotify are aggregated in a fifo queue. If this coalescing | 
 | 1162 |         option is enabled events are filtered based on their unicity, only | 
 | 1163 |         unique events are enqueued, doublons are discarded. An event is unique | 
 | 1164 |         when the combination of its fields (wd, mask, cookie, name) is unique | 
 | 1165 |         among events of a same batch. After a batch of events is processed any | 
 | 1166 |         events is accepted again. By default this option is disabled, you have | 
 | 1167 |         to explictly call this function to turn it on. | 
 | 1168 |  | 
 | 1169 |         @param coalesce: Optional new coalescing value. True by default. | 
 | 1170 |         @type coalesce: Bool | 
 | 1171 |         """ | 
 | 1172 |         self._coalesce = coalesce | 
 | 1173 |         if not coalesce: | 
 | 1174 |             self._eventset.clear() | 
 | 1175 |  | 
 | 1176 |     def check_events(self, timeout=None): | 
 | 1177 |         """ | 
 | 1178 |         Check for new events available to read, blocks up to timeout | 
 | 1179 |         milliseconds. | 
 | 1180 |  | 
 | 1181 |         @param timeout: If specified it overrides the corresponding instance | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1182 |                         attribute _timeout. timeout must be sepcified in | 
 | 1183 |                         milliseconds. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1184 |         @type timeout: int | 
 | 1185 |  | 
 | 1186 |         @return: New events to read. | 
 | 1187 |         @rtype: bool | 
 | 1188 |         """ | 
 | 1189 |         while True: | 
 | 1190 |             try: | 
 | 1191 |                 # blocks up to 'timeout' milliseconds | 
 | 1192 |                 if timeout is None: | 
 | 1193 |                     timeout = self._timeout | 
 | 1194 |                 ret = self._pollobj.poll(timeout) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1195 |             except select.error as err: | 
 | 1196 |                 if err.args[0] == errno.EINTR: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1197 |                     continue # interrupted, retry | 
 | 1198 |                 else: | 
 | 1199 |                     raise | 
 | 1200 |             else: | 
 | 1201 |                 break | 
 | 1202 |  | 
 | 1203 |         if not ret or (self._pipe[0] == ret[0][0]): | 
 | 1204 |             return False | 
 | 1205 |         # only one fd is polled | 
 | 1206 |         return ret[0][1] & select.POLLIN | 
 | 1207 |  | 
 | 1208 |     def read_events(self): | 
 | 1209 |         """ | 
 | 1210 |         Read events from device, build _RawEvents, and enqueue them. | 
 | 1211 |         """ | 
 | 1212 |         buf_ = array.array('i', [0]) | 
 | 1213 |         # get event queue size | 
 | 1214 |         if fcntl.ioctl(self._fd, termios.FIONREAD, buf_, 1) == -1: | 
 | 1215 |             return | 
 | 1216 |         queue_size = buf_[0] | 
 | 1217 |         if queue_size < self._threshold: | 
 | 1218 |             log.debug('(fd: %d) %d bytes available to read but threshold is ' | 
 | 1219 |                       'fixed to %d bytes', self._fd, queue_size, | 
 | 1220 |                       self._threshold) | 
 | 1221 |             return | 
 | 1222 |  | 
 | 1223 |         try: | 
 | 1224 |             # Read content from file | 
 | 1225 |             r = os.read(self._fd, queue_size) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1226 |         except Exception as msg: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1227 |             raise NotifierError(msg) | 
 | 1228 |         log.debug('Event queue size: %d', queue_size) | 
 | 1229 |         rsum = 0  # counter | 
 | 1230 |         while rsum < queue_size: | 
 | 1231 |             s_size = 16 | 
 | 1232 |             # Retrieve wd, mask, cookie and fname_len | 
 | 1233 |             wd, mask, cookie, fname_len = struct.unpack('iIII', | 
 | 1234 |                                                         r[rsum:rsum+s_size]) | 
 | 1235 |             # Retrieve name | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1236 |             bname, = struct.unpack('%ds' % fname_len, | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1237 |                                    r[rsum + s_size:rsum + s_size + fname_len]) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1238 |             # FIXME: should we explictly call sys.getdefaultencoding() here ?? | 
 | 1239 |             uname = bname.decode() | 
 | 1240 |             rawevent = _RawEvent(wd, mask, cookie, uname) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1241 |             if self._coalesce: | 
 | 1242 |                 # Only enqueue new (unique) events. | 
 | 1243 |                 raweventstr = str(rawevent) | 
 | 1244 |                 if raweventstr not in self._eventset: | 
 | 1245 |                     self._eventset.add(raweventstr) | 
 | 1246 |                     self._eventq.append(rawevent) | 
 | 1247 |             else: | 
 | 1248 |                 self._eventq.append(rawevent) | 
 | 1249 |             rsum += s_size + fname_len | 
 | 1250 |  | 
 | 1251 |     def process_events(self): | 
 | 1252 |         """ | 
 | 1253 |         Routine for processing events from queue by calling their | 
 | 1254 |         associated proccessing method (an instance of ProcessEvent). | 
 | 1255 |         It also does internal processings, to keep the system updated. | 
 | 1256 |         """ | 
 | 1257 |         while self._eventq: | 
 | 1258 |             raw_event = self._eventq.popleft()  # pop next event | 
 | 1259 |             if self._watch_manager.ignore_events: | 
 | 1260 |                 log.debug("Event ignored: %s" % repr(raw_event)) | 
 | 1261 |                 continue | 
 | 1262 |             watch_ = self._watch_manager.get_watch(raw_event.wd) | 
 | 1263 |             if (watch_ is None) and not (raw_event.mask & IN_Q_OVERFLOW): | 
 | 1264 |                 if not (raw_event.mask & IN_IGNORED): | 
 | 1265 |                     # Not really sure how we ended up here, nor how we should | 
 | 1266 |                     # handle these types of events and if it is appropriate to | 
 | 1267 |                     # completly skip them (like we are doing here). | 
 | 1268 |                     log.warning("Unable to retrieve Watch object associated to %s", | 
 | 1269 |                                 repr(raw_event)) | 
 | 1270 |                 continue | 
 | 1271 |             revent = self._sys_proc_fun(raw_event)  # system processings | 
 | 1272 |             if watch_ and watch_.proc_fun: | 
 | 1273 |                 watch_.proc_fun(revent)  # user processings | 
 | 1274 |             else: | 
 | 1275 |                 self._default_proc_fun(revent) | 
 | 1276 |         self._sys_proc_fun.cleanup()  # remove olds MOVED_* events records | 
 | 1277 |         if self._coalesce: | 
 | 1278 |             self._eventset.clear() | 
 | 1279 |  | 
 | 1280 |     def __daemonize(self, pid_file=None, stdin=os.devnull, stdout=os.devnull, | 
 | 1281 |                     stderr=os.devnull): | 
 | 1282 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1283 |         pid_file: file where the pid will be written. If pid_file=None the pid | 
 | 1284 |                   is written to /var/run/<sys.argv[0]|pyinotify>.pid, if | 
 | 1285 |                   pid_file=False no pid_file is written. | 
 | 1286 |         stdin, stdout, stderr: files associated to common streams. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1287 |         """ | 
 | 1288 |         if pid_file is None: | 
 | 1289 |             dirname = '/var/run/' | 
 | 1290 |             basename = os.path.basename(sys.argv[0]) or 'pyinotify' | 
 | 1291 |             pid_file = os.path.join(dirname, basename + '.pid') | 
 | 1292 |  | 
 | 1293 |         if pid_file != False and os.path.lexists(pid_file): | 
 | 1294 |             err = 'Cannot daemonize: pid file %s already exists.' % pid_file | 
 | 1295 |             raise NotifierError(err) | 
 | 1296 |  | 
 | 1297 |         def fork_daemon(): | 
 | 1298 |             # Adapted from Chad J. Schroeder's recipe | 
 | 1299 |             # @see http://code.activestate.com/recipes/278731/ | 
 | 1300 |             pid = os.fork() | 
 | 1301 |             if (pid == 0): | 
 | 1302 |                 # parent 2 | 
 | 1303 |                 os.setsid() | 
 | 1304 |                 pid = os.fork() | 
 | 1305 |                 if (pid == 0): | 
 | 1306 |                     # child | 
 | 1307 |                     os.chdir('/') | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1308 |                     os.umask(0o022) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1309 |                 else: | 
 | 1310 |                     # parent 2 | 
 | 1311 |                     os._exit(0) | 
 | 1312 |             else: | 
 | 1313 |                 # parent 1 | 
 | 1314 |                 os._exit(0) | 
 | 1315 |  | 
 | 1316 |             fd_inp = os.open(stdin, os.O_RDONLY) | 
 | 1317 |             os.dup2(fd_inp, 0) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1318 |             fd_out = os.open(stdout, os.O_WRONLY|os.O_CREAT, 0o0600) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1319 |             os.dup2(fd_out, 1) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1320 |             fd_err = os.open(stderr, os.O_WRONLY|os.O_CREAT, 0o0600) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1321 |             os.dup2(fd_err, 2) | 
 | 1322 |  | 
 | 1323 |         # Detach task | 
 | 1324 |         fork_daemon() | 
 | 1325 |  | 
 | 1326 |         # Write pid | 
 | 1327 |         if pid_file != False: | 
 | 1328 |             flags = os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW|os.O_EXCL | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1329 |             fd_pid = os.open(pid_file, flags, 0o0600) | 
 | 1330 |             os.write(fd_pid,  bytes(str(os.getpid()) + '\n', | 
 | 1331 |                                     locale.getpreferredencoding())) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1332 |             os.close(fd_pid) | 
 | 1333 |             # Register unlink function | 
 | 1334 |             atexit.register(lambda : os.unlink(pid_file)) | 
 | 1335 |  | 
 | 1336 |     def _sleep(self, ref_time): | 
 | 1337 |         # Only consider sleeping if read_freq is > 0 | 
 | 1338 |         if self._read_freq > 0: | 
 | 1339 |             cur_time = time.time() | 
 | 1340 |             sleep_amount = self._read_freq - (cur_time - ref_time) | 
 | 1341 |             if sleep_amount > 0: | 
 | 1342 |                 log.debug('Now sleeping %d seconds', sleep_amount) | 
 | 1343 |                 time.sleep(sleep_amount) | 
 | 1344 |  | 
 | 1345 |     def loop(self, callback=None, daemonize=False, **args): | 
 | 1346 |         """ | 
 | 1347 |         Events are read only one time every min(read_freq, timeout) | 
 | 1348 |         seconds at best and only if the size to read is >= threshold. | 
 | 1349 |         After this method returns it must not be called again for the same | 
 | 1350 |         instance. | 
 | 1351 |  | 
 | 1352 |         @param callback: Functor called after each event processing iteration. | 
 | 1353 |                          Expects to receive the notifier object (self) as first | 
 | 1354 |                          parameter. If this function returns True the loop is | 
 | 1355 |                          immediately terminated otherwise the loop method keeps | 
 | 1356 |                          looping. | 
 | 1357 |         @type callback: callable object or function | 
 | 1358 |         @param daemonize: This thread is daemonized if set to True. | 
 | 1359 |         @type daemonize: boolean | 
 | 1360 |         @param args: Optional and relevant only if daemonize is True. Remaining | 
 | 1361 |                      keyworded arguments are directly passed to daemonize see | 
 | 1362 |                      __daemonize() method. If pid_file=None or is set to a | 
 | 1363 |                      pathname the caller must ensure the file does not exist | 
 | 1364 |                      before this method is called otherwise an exception | 
 | 1365 |                      pyinotify.NotifierError will be raised. If pid_file=False | 
 | 1366 |                      it is still daemonized but the pid is not written in any | 
 | 1367 |                      file. | 
 | 1368 |         @type args: various | 
 | 1369 |         """ | 
 | 1370 |         if daemonize: | 
 | 1371 |             self.__daemonize(**args) | 
 | 1372 |  | 
 | 1373 |         # Read and process events forever | 
 | 1374 |         while 1: | 
 | 1375 |             try: | 
 | 1376 |                 self.process_events() | 
 | 1377 |                 if (callback is not None) and (callback(self) is True): | 
 | 1378 |                     break | 
 | 1379 |                 ref_time = time.time() | 
 | 1380 |                 # check_events is blocking | 
 | 1381 |                 if self.check_events(): | 
 | 1382 |                     self._sleep(ref_time) | 
 | 1383 |                     self.read_events() | 
 | 1384 |             except KeyboardInterrupt: | 
 | 1385 |                 # Stop monitoring if sigint is caught (Control-C). | 
 | 1386 |                 log.debug('Pyinotify stops monitoring.') | 
 | 1387 |                 break | 
 | 1388 |         # Close internals | 
 | 1389 |         self.stop() | 
 | 1390 |  | 
 | 1391 |     def stop(self): | 
 | 1392 |         """ | 
 | 1393 |         Close inotify's instance (close its file descriptor). | 
 | 1394 |         It destroys all existing watches, pending events,... | 
 | 1395 |         This method is automatically called at the end of loop(). | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1396 |         Afterward it is invalid to access this instance. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1397 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1398 |         if self._fd is not None: | 
 | 1399 |             self._pollobj.unregister(self._fd) | 
 | 1400 |             os.close(self._fd) | 
 | 1401 |             self._fd = None | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1402 |         self._sys_proc_fun = None | 
 | 1403 |  | 
 | 1404 |  | 
 | 1405 | class ThreadedNotifier(threading.Thread, Notifier): | 
 | 1406 |     """ | 
 | 1407 |     This notifier inherits from threading.Thread for instanciating a separate | 
 | 1408 |     thread, and also inherits from Notifier, because it is a threaded notifier. | 
 | 1409 |  | 
 | 1410 |     Note that every functionality provided by this class is also provided | 
 | 1411 |     through Notifier class. Moreover Notifier should be considered first because | 
 | 1412 |     it is not threaded and could be easily daemonized. | 
 | 1413 |     """ | 
 | 1414 |     def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, | 
 | 1415 |                  threshold=0, timeout=None): | 
 | 1416 |         """ | 
 | 1417 |         Initialization, initialize base classes. read_freq, threshold and | 
 | 1418 |         timeout parameters are used when looping. | 
 | 1419 |  | 
 | 1420 |         @param watch_manager: Watch Manager. | 
 | 1421 |         @type watch_manager: WatchManager instance | 
 | 1422 |         @param default_proc_fun: Default processing method. See base class. | 
 | 1423 |         @type default_proc_fun: instance of ProcessEvent | 
 | 1424 |         @param read_freq: if read_freq == 0, events are read asap, | 
 | 1425 |                           if read_freq is > 0, this thread sleeps | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1426 |                           max(0, read_freq - (timeout / 1000)) seconds. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1427 |         @type read_freq: int | 
 | 1428 |         @param threshold: File descriptor will be read only if the accumulated | 
 | 1429 |                           size to read becomes >= threshold. If != 0, you likely | 
 | 1430 |                           want to use it in combination with an appropriate | 
 | 1431 |                           value set for read_freq because without that you would | 
 | 1432 |                           keep looping without really reading anything and that | 
 | 1433 |                           until the amount of events to read is >= threshold. At | 
 | 1434 |                           least with read_freq you might sleep. | 
 | 1435 |         @type threshold: int | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1436 |         @param timeout: see read_freq above. If provided, it must be set in | 
 | 1437 |                         milliseconds. See | 
 | 1438 |                         https://docs.python.org/3/library/select.html#select.poll.poll | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1439 |         @type timeout: int | 
 | 1440 |         """ | 
 | 1441 |         # Init threading base class | 
 | 1442 |         threading.Thread.__init__(self) | 
 | 1443 |         # Stop condition | 
 | 1444 |         self._stop_event = threading.Event() | 
 | 1445 |         # Init Notifier base class | 
 | 1446 |         Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, | 
 | 1447 |                           threshold, timeout) | 
 | 1448 |         # Create a new pipe used for thread termination | 
 | 1449 |         self._pipe = os.pipe() | 
 | 1450 |         self._pollobj.register(self._pipe[0], select.POLLIN) | 
 | 1451 |  | 
 | 1452 |     def stop(self): | 
 | 1453 |         """ | 
 | 1454 |         Stop notifier's loop. Stop notification. Join the thread. | 
 | 1455 |         """ | 
 | 1456 |         self._stop_event.set() | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1457 |         os.write(self._pipe[1], b'stop') | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1458 |         threading.Thread.join(self) | 
 | 1459 |         Notifier.stop(self) | 
 | 1460 |         self._pollobj.unregister(self._pipe[0]) | 
 | 1461 |         os.close(self._pipe[0]) | 
 | 1462 |         os.close(self._pipe[1]) | 
 | 1463 |  | 
 | 1464 |     def loop(self): | 
 | 1465 |         """ | 
 | 1466 |         Thread's main loop. Don't meant to be called by user directly. | 
 | 1467 |         Call inherited start() method instead. | 
 | 1468 |  | 
 | 1469 |         Events are read only once time every min(read_freq, timeout) | 
 | 1470 |         seconds at best and only if the size of events to read is >= threshold. | 
 | 1471 |         """ | 
 | 1472 |         # When the loop must be terminated .stop() is called, 'stop' | 
 | 1473 |         # is written to pipe fd so poll() returns and .check_events() | 
 | 1474 |         # returns False which make evaluate the While's stop condition | 
 | 1475 |         # ._stop_event.isSet() wich put an end to the thread's execution. | 
 | 1476 |         while not self._stop_event.isSet(): | 
 | 1477 |             self.process_events() | 
 | 1478 |             ref_time = time.time() | 
 | 1479 |             if self.check_events(): | 
 | 1480 |                 self._sleep(ref_time) | 
 | 1481 |                 self.read_events() | 
 | 1482 |  | 
 | 1483 |     def run(self): | 
 | 1484 |         """ | 
 | 1485 |         Start thread's loop: read and process events until the method | 
 | 1486 |         stop() is called. | 
 | 1487 |         Never call this method directly, instead call the start() method | 
 | 1488 |         inherited from threading.Thread, which then will call run() in | 
 | 1489 |         its turn. | 
 | 1490 |         """ | 
 | 1491 |         self.loop() | 
 | 1492 |  | 
 | 1493 |  | 
 | 1494 | class AsyncNotifier(asyncore.file_dispatcher, Notifier): | 
 | 1495 |     """ | 
 | 1496 |     This notifier inherits from asyncore.file_dispatcher in order to be able to | 
 | 1497 |     use pyinotify along with the asyncore framework. | 
 | 1498 |  | 
 | 1499 |     """ | 
 | 1500 |     def __init__(self, watch_manager, default_proc_fun=None, read_freq=0, | 
 | 1501 |                  threshold=0, timeout=None, channel_map=None): | 
 | 1502 |         """ | 
 | 1503 |         Initializes the async notifier. The only additional parameter is | 
 | 1504 |         'channel_map' which is the optional asyncore private map. See | 
 | 1505 |         Notifier class for the meaning of the others parameters. | 
 | 1506 |  | 
 | 1507 |         """ | 
 | 1508 |         Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, | 
 | 1509 |                           threshold, timeout) | 
 | 1510 |         asyncore.file_dispatcher.__init__(self, self._fd, channel_map) | 
 | 1511 |  | 
 | 1512 |     def handle_read(self): | 
 | 1513 |         """ | 
 | 1514 |         When asyncore tells us we can read from the fd, we proceed processing | 
 | 1515 |         events. This method can be overridden for handling a notification | 
 | 1516 |         differently. | 
 | 1517 |  | 
 | 1518 |         """ | 
 | 1519 |         self.read_events() | 
 | 1520 |         self.process_events() | 
 | 1521 |  | 
 | 1522 |  | 
 | 1523 | class TornadoAsyncNotifier(Notifier): | 
 | 1524 |     """ | 
 | 1525 |     Tornado ioloop adapter. | 
 | 1526 |  | 
 | 1527 |     """ | 
 | 1528 |     def __init__(self, watch_manager, ioloop, callback=None, | 
 | 1529 |                  default_proc_fun=None, read_freq=0, threshold=0, timeout=None, | 
 | 1530 |                  channel_map=None): | 
 | 1531 |         """ | 
 | 1532 |         Note that if later you must call ioloop.close() be sure to let the | 
 | 1533 |         default parameter to all_fds=False. | 
 | 1534 |  | 
 | 1535 |         See example tornado_notifier.py for an example using this notifier. | 
 | 1536 |  | 
 | 1537 |         @param ioloop: Tornado's IO loop. | 
 | 1538 |         @type ioloop: tornado.ioloop.IOLoop instance. | 
 | 1539 |         @param callback: Functor called at the end of each call to handle_read | 
 | 1540 |                          (IOLoop's read handler). Expects to receive the | 
 | 1541 |                          notifier object (self) as single parameter. | 
 | 1542 |         @type callback: callable object or function | 
 | 1543 |         """ | 
 | 1544 |         self.io_loop = ioloop | 
 | 1545 |         self.handle_read_callback = callback | 
 | 1546 |         Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, | 
 | 1547 |                           threshold, timeout) | 
 | 1548 |         ioloop.add_handler(self._fd, self.handle_read, ioloop.READ) | 
 | 1549 |  | 
 | 1550 |     def stop(self): | 
 | 1551 |         self.io_loop.remove_handler(self._fd) | 
 | 1552 |         Notifier.stop(self) | 
 | 1553 |  | 
 | 1554 |     def handle_read(self, *args, **kwargs): | 
 | 1555 |         """ | 
 | 1556 |         See comment in AsyncNotifier. | 
 | 1557 |  | 
 | 1558 |         """ | 
 | 1559 |         self.read_events() | 
 | 1560 |         self.process_events() | 
 | 1561 |         if self.handle_read_callback is not None: | 
 | 1562 |             self.handle_read_callback(self) | 
 | 1563 |  | 
 | 1564 |  | 
 | 1565 | class AsyncioNotifier(Notifier): | 
 | 1566 |     """ | 
 | 1567 |  | 
 | 1568 |     asyncio/trollius event loop adapter. | 
 | 1569 |  | 
 | 1570 |     """ | 
 | 1571 |     def __init__(self, watch_manager, loop, callback=None, | 
 | 1572 |                  default_proc_fun=None, read_freq=0, threshold=0, timeout=None): | 
 | 1573 |         """ | 
 | 1574 |  | 
 | 1575 |         See examples/asyncio_notifier.py for an example usage. | 
 | 1576 |  | 
 | 1577 |         @param loop: asyncio or trollius event loop instance. | 
 | 1578 |         @type loop: asyncio.BaseEventLoop or trollius.BaseEventLoop instance. | 
 | 1579 |         @param callback: Functor called at the end of each call to handle_read. | 
 | 1580 |                          Expects to receive the notifier object (self) as | 
 | 1581 |                          single parameter. | 
 | 1582 |         @type callback: callable object or function | 
 | 1583 |  | 
 | 1584 |         """ | 
 | 1585 |         self.loop = loop | 
 | 1586 |         self.handle_read_callback = callback | 
 | 1587 |         Notifier.__init__(self, watch_manager, default_proc_fun, read_freq, | 
 | 1588 |                           threshold, timeout) | 
 | 1589 |         loop.add_reader(self._fd, self.handle_read) | 
 | 1590 |  | 
 | 1591 |     def stop(self): | 
 | 1592 |         self.loop.remove_reader(self._fd) | 
 | 1593 |         Notifier.stop(self) | 
 | 1594 |  | 
 | 1595 |     def handle_read(self, *args, **kwargs): | 
 | 1596 |         self.read_events() | 
 | 1597 |         self.process_events() | 
 | 1598 |         if self.handle_read_callback is not None: | 
 | 1599 |             self.handle_read_callback(self) | 
 | 1600 |  | 
 | 1601 |  | 
 | 1602 | class Watch: | 
 | 1603 |     """ | 
 | 1604 |     Represent a watch, i.e. a file or directory being watched. | 
 | 1605 |  | 
 | 1606 |     """ | 
 | 1607 |     __slots__ = ('wd', 'path', 'mask', 'proc_fun', 'auto_add', | 
 | 1608 |                  'exclude_filter', 'dir') | 
 | 1609 |  | 
 | 1610 |     def __init__(self, wd, path, mask, proc_fun, auto_add, exclude_filter): | 
 | 1611 |         """ | 
 | 1612 |         Initializations. | 
 | 1613 |  | 
 | 1614 |         @param wd: Watch descriptor. | 
 | 1615 |         @type wd: int | 
 | 1616 |         @param path: Path of the file or directory being watched. | 
 | 1617 |         @type path: str | 
 | 1618 |         @param mask: Mask. | 
 | 1619 |         @type mask: int | 
 | 1620 |         @param proc_fun: Processing callable object. | 
 | 1621 |         @type proc_fun: | 
 | 1622 |         @param auto_add: Automatically add watches on new directories. | 
 | 1623 |         @type auto_add: bool | 
 | 1624 |         @param exclude_filter: Boolean function, used to exclude new | 
 | 1625 |                                directories from being automatically watched. | 
 | 1626 |                                See WatchManager.__init__ | 
 | 1627 |         @type exclude_filter: callable object | 
 | 1628 |         """ | 
 | 1629 |         self.wd = wd | 
 | 1630 |         self.path = path | 
 | 1631 |         self.mask = mask | 
 | 1632 |         self.proc_fun = proc_fun | 
 | 1633 |         self.auto_add = auto_add | 
 | 1634 |         self.exclude_filter = exclude_filter | 
 | 1635 |         self.dir = os.path.isdir(self.path) | 
 | 1636 |  | 
 | 1637 |     def __repr__(self): | 
 | 1638 |         """ | 
 | 1639 |         @return: String representation. | 
 | 1640 |         @rtype: str | 
 | 1641 |         """ | 
 | 1642 |         s = ' '.join(['%s%s%s' % (output_format.field_name(attr), | 
 | 1643 |                                   output_format.punctuation('='), | 
 | 1644 |                                   output_format.field_value(getattr(self, | 
 | 1645 |                                                                     attr))) \ | 
 | 1646 |                       for attr in self.__slots__ if not attr.startswith('_')]) | 
 | 1647 |  | 
 | 1648 |         s = '%s%s %s %s' % (output_format.punctuation('<'), | 
 | 1649 |                             output_format.class_name(self.__class__.__name__), | 
 | 1650 |                             s, | 
 | 1651 |                             output_format.punctuation('>')) | 
 | 1652 |         return s | 
 | 1653 |  | 
 | 1654 |  | 
 | 1655 | class ExcludeFilter: | 
 | 1656 |     """ | 
 | 1657 |     ExcludeFilter is an exclusion filter. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1658 |     """ | 
 | 1659 |     def __init__(self, arg_lst): | 
 | 1660 |         """ | 
 | 1661 |         Examples: | 
 | 1662 |           ef1 = ExcludeFilter(["/etc/rc.*", "/etc/hostname"]) | 
 | 1663 |           ef2 = ExcludeFilter("/my/path/exclude.lst") | 
 | 1664 |           Where exclude.lst contains: | 
 | 1665 |           /etc/rc.* | 
 | 1666 |           /etc/hostname | 
 | 1667 |  | 
 | 1668 |         Note: it is not possible to exclude a file if its encapsulating | 
 | 1669 |               directory is itself watched. See this issue for more details | 
 | 1670 |               https://github.com/seb-m/pyinotify/issues/31 | 
 | 1671 |  | 
 | 1672 |         @param arg_lst: is either a list of patterns or a filename from which | 
 | 1673 |                         patterns will be loaded. | 
 | 1674 |         @type arg_lst: list of str or str | 
 | 1675 |         """ | 
 | 1676 |         if isinstance(arg_lst, str): | 
 | 1677 |             lst = self._load_patterns_from_file(arg_lst) | 
 | 1678 |         elif isinstance(arg_lst, list): | 
 | 1679 |             lst = arg_lst | 
 | 1680 |         else: | 
 | 1681 |             raise TypeError | 
 | 1682 |  | 
 | 1683 |         self._lregex = [] | 
 | 1684 |         for regex in lst: | 
 | 1685 |             self._lregex.append(re.compile(regex, re.UNICODE)) | 
 | 1686 |  | 
 | 1687 |     def _load_patterns_from_file(self, filename): | 
 | 1688 |         lst = [] | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1689 |         with open(filename, 'r') as file_obj: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1690 |             for line in file_obj.readlines(): | 
 | 1691 |                 # Trim leading an trailing whitespaces | 
 | 1692 |                 pattern = line.strip() | 
 | 1693 |                 if not pattern or pattern.startswith('#'): | 
 | 1694 |                     continue | 
 | 1695 |                 lst.append(pattern) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1696 |         return lst | 
 | 1697 |  | 
 | 1698 |     def _match(self, regex, path): | 
 | 1699 |         return regex.match(path) is not None | 
 | 1700 |  | 
 | 1701 |     def __call__(self, path): | 
 | 1702 |         """ | 
 | 1703 |         @param path: Path to match against provided regexps. | 
 | 1704 |         @type path: str | 
 | 1705 |         @return: Return True if path has been matched and should | 
 | 1706 |                  be excluded, False otherwise. | 
 | 1707 |         @rtype: bool | 
 | 1708 |         """ | 
 | 1709 |         for regex in self._lregex: | 
 | 1710 |             if self._match(regex, path): | 
 | 1711 |                 return True | 
 | 1712 |         return False | 
 | 1713 |  | 
 | 1714 |  | 
 | 1715 | class WatchManagerError(Exception): | 
 | 1716 |     """ | 
 | 1717 |     WatchManager Exception. Raised on error encountered on watches | 
 | 1718 |     operations. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1719 |     """ | 
 | 1720 |     def __init__(self, msg, wmd): | 
 | 1721 |         """ | 
 | 1722 |         @param msg: Exception string's description. | 
 | 1723 |         @type msg: string | 
 | 1724 |         @param wmd: This dictionary contains the wd assigned to paths of the | 
 | 1725 |                     same call for which watches were successfully added. | 
 | 1726 |         @type wmd: dict | 
 | 1727 |         """ | 
 | 1728 |         self.wmd = wmd | 
 | 1729 |         Exception.__init__(self, msg) | 
 | 1730 |  | 
 | 1731 |  | 
 | 1732 | class WatchManager: | 
 | 1733 |     """ | 
 | 1734 |     Provide operations for watching files and directories. Its internal | 
 | 1735 |     dictionary is used to reference watched items. When used inside | 
 | 1736 |     threaded code, one must instanciate as many WatchManager instances as | 
 | 1737 |     there are ThreadedNotifier instances. | 
 | 1738 |  | 
 | 1739 |     """ | 
 | 1740 |     def __init__(self, exclude_filter=lambda path: False): | 
 | 1741 |         """ | 
 | 1742 |         Initialization: init inotify, init watch manager dictionary. | 
 | 1743 |         Raise OSError if initialization fails, raise InotifyBindingNotFoundError | 
 | 1744 |         if no inotify binding was found (through ctypes or from direct access to | 
 | 1745 |         syscalls). | 
 | 1746 |  | 
 | 1747 |         @param exclude_filter: boolean function, returns True if current | 
 | 1748 |                                path must be excluded from being watched. | 
 | 1749 |                                Convenient for providing a common exclusion | 
 | 1750 |                                filter for every call to add_watch. | 
 | 1751 |         @type exclude_filter: callable object | 
 | 1752 |         """ | 
 | 1753 |         self._ignore_events = False | 
 | 1754 |         self._exclude_filter = exclude_filter | 
 | 1755 |         self._wmd = {}  # watch dict key: watch descriptor, value: watch | 
 | 1756 |  | 
 | 1757 |         self._inotify_wrapper = INotifyWrapper.create() | 
 | 1758 |         if self._inotify_wrapper is None: | 
 | 1759 |             raise InotifyBindingNotFoundError() | 
 | 1760 |  | 
 | 1761 |         self._fd = self._inotify_wrapper.inotify_init() # file descriptor | 
 | 1762 |         if self._fd < 0: | 
 | 1763 |             err = 'Cannot initialize new instance of inotify, %s' | 
 | 1764 |             raise OSError(err % self._inotify_wrapper.str_errno()) | 
 | 1765 |  | 
 | 1766 |     def close(self): | 
 | 1767 |         """ | 
 | 1768 |         Close inotify's file descriptor, this action will also automatically | 
 | 1769 |         remove (i.e. stop watching) all its associated watch descriptors. | 
 | 1770 |         After a call to this method the WatchManager's instance become useless | 
 | 1771 |         and cannot be reused, a new instance must then be instanciated. It | 
 | 1772 |         makes sense to call this method in few situations for instance if | 
 | 1773 |         several independant WatchManager must be instanciated or if all watches | 
 | 1774 |         must be removed and no other watches need to be added. | 
 | 1775 |         """ | 
 | 1776 |         os.close(self._fd) | 
 | 1777 |  | 
 | 1778 |     def get_fd(self): | 
 | 1779 |         """ | 
 | 1780 |         Return assigned inotify's file descriptor. | 
 | 1781 |  | 
 | 1782 |         @return: File descriptor. | 
 | 1783 |         @rtype: int | 
 | 1784 |         """ | 
 | 1785 |         return self._fd | 
 | 1786 |  | 
 | 1787 |     def get_watch(self, wd): | 
 | 1788 |         """ | 
 | 1789 |         Get watch from provided watch descriptor wd. | 
 | 1790 |  | 
 | 1791 |         @param wd: Watch descriptor. | 
 | 1792 |         @type wd: int | 
 | 1793 |         """ | 
 | 1794 |         return self._wmd.get(wd) | 
 | 1795 |  | 
 | 1796 |     def del_watch(self, wd): | 
 | 1797 |         """ | 
 | 1798 |         Remove watch entry associated to watch descriptor wd. | 
 | 1799 |  | 
 | 1800 |         @param wd: Watch descriptor. | 
 | 1801 |         @type wd: int | 
 | 1802 |         """ | 
 | 1803 |         try: | 
 | 1804 |             del self._wmd[wd] | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1805 |         except KeyError as err: | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1806 |             log.error('Cannot delete unknown watch descriptor %s' % str(err)) | 
 | 1807 |  | 
 | 1808 |     @property | 
 | 1809 |     def watches(self): | 
 | 1810 |         """ | 
 | 1811 |         Get a reference on the internal watch manager dictionary. | 
 | 1812 |  | 
 | 1813 |         @return: Internal watch manager dictionary. | 
 | 1814 |         @rtype: dict | 
 | 1815 |         """ | 
 | 1816 |         return self._wmd | 
 | 1817 |  | 
 | 1818 |     def __format_path(self, path): | 
 | 1819 |         """ | 
 | 1820 |         Format path to its internal (stored in watch manager) representation. | 
 | 1821 |         """ | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1822 |         # path must be a unicode string (str) and is just normalized. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1823 |         return os.path.normpath(path) | 
 | 1824 |  | 
 | 1825 |     def __add_watch(self, path, mask, proc_fun, auto_add, exclude_filter): | 
 | 1826 |         """ | 
 | 1827 |         Add a watch on path, build a Watch object and insert it in the | 
 | 1828 |         watch manager dictionary. Return the wd value. | 
 | 1829 |         """ | 
 | 1830 |         path = self.__format_path(path) | 
 | 1831 |         if auto_add and not mask & IN_CREATE: | 
 | 1832 |             mask |= IN_CREATE | 
 | 1833 |         wd = self._inotify_wrapper.inotify_add_watch(self._fd, path, mask) | 
 | 1834 |         if wd < 0: | 
 | 1835 |             return wd | 
 | 1836 |         watch = Watch(wd=wd, path=path, mask=mask, proc_fun=proc_fun, | 
 | 1837 |                       auto_add=auto_add, exclude_filter=exclude_filter) | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1838 |         # wd are _always_ indexed with their original unicode paths in wmd. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1839 |         self._wmd[wd] = watch | 
 | 1840 |         log.debug('New %s', watch) | 
 | 1841 |         return wd | 
 | 1842 |  | 
 | 1843 |     def __glob(self, path, do_glob): | 
 | 1844 |         if do_glob: | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1845 |             return glob.iglob(path) | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1846 |         else: | 
 | 1847 |             return [path] | 
 | 1848 |  | 
 | 1849 |     def add_watch(self, path, mask, proc_fun=None, rec=False, | 
 | 1850 |                   auto_add=False, do_glob=False, quiet=True, | 
 | 1851 |                   exclude_filter=None): | 
 | 1852 |         """ | 
 | 1853 |         Add watch(s) on the provided |path|(s) with associated |mask| flag | 
 | 1854 |         value and optionally with a processing |proc_fun| function and | 
 | 1855 |         recursive flag |rec| set to True. | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1856 |         All |path| components _must_ be str (i.e. unicode) objects. | 
 | 1857 |         If |path| is already watched it is ignored, but if it is called with | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1858 |         option rec=True a watch is put on each one of its not-watched | 
 | 1859 |         subdirectory. | 
 | 1860 |  | 
 | 1861 |         @param path: Path to watch, the path can either be a file or a | 
 | 1862 |                      directory. Also accepts a sequence (list) of paths. | 
 | 1863 |         @type path: string or list of strings | 
 | 1864 |         @param mask: Bitmask of events. | 
 | 1865 |         @type mask: int | 
 | 1866 |         @param proc_fun: Processing object. | 
 | 1867 |         @type proc_fun: function or ProcessEvent instance or instance of | 
 | 1868 |                         one of its subclasses or callable object. | 
 | 1869 |         @param rec: Recursively add watches from path on all its | 
 | 1870 |                     subdirectories, set to False by default (doesn't | 
 | 1871 |                     follows symlinks in any case). | 
 | 1872 |         @type rec: bool | 
 | 1873 |         @param auto_add: Automatically add watches on newly created | 
 | 1874 |                          directories in watched parent |path| directory. | 
 | 1875 |                          If |auto_add| is True, IN_CREATE is ored with |mask| | 
 | 1876 |                          when the watch is added. | 
 | 1877 |         @type auto_add: bool | 
 | 1878 |         @param do_glob: Do globbing on pathname (see standard globbing | 
 | 1879 |                         module for more informations). | 
 | 1880 |         @type do_glob: bool | 
 | 1881 |         @param quiet: if False raises a WatchManagerError exception on | 
 | 1882 |                       error. See example not_quiet.py. | 
 | 1883 |         @type quiet: bool | 
 | 1884 |         @param exclude_filter: predicate (boolean function), which returns | 
 | 1885 |                                True if the current path must be excluded | 
 | 1886 |                                from being watched. This argument has | 
 | 1887 |                                precedence over exclude_filter passed to | 
 | 1888 |                                the class' constructor. | 
 | 1889 |         @type exclude_filter: callable object | 
 | 1890 |         @return: dict of paths associated to watch descriptors. A wd value | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1891 |                  is positive if the watch was added sucessfully, otherwise | 
 | 1892 |                  the value is negative. If the path was invalid or was already | 
 | 1893 |                  watched it is not included into this returned dictionary. | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1894 |         @rtype: dict of {str: int} | 
 | 1895 |         """ | 
 | 1896 |         ret_ = {} # return {path: wd, ...} | 
 | 1897 |  | 
 | 1898 |         if exclude_filter is None: | 
 | 1899 |             exclude_filter = self._exclude_filter | 
 | 1900 |  | 
 | 1901 |         # normalize args as list elements | 
 | 1902 |         for npath in self.__format_param(path): | 
| Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 1903 |             # Require that path be a unicode string | 
 | 1904 |             if not isinstance(npath, str): | 
 | 1905 |                 ret_[path] = -3 | 
 | 1906 |                 continue | 
 | 1907 |  | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1908 |             # unix pathname pattern expansion | 
 | 1909 |             for apath in self.__glob(npath, do_glob): | 
 | 1910 |                 # recursively list subdirs according to rec param | 
 | 1911 |                 for rpath in self.__walk_rec(apath, rec): | 
 | 1912 |                     if not exclude_filter(rpath): | 
 | 1913 |                         wd = ret_[rpath] = self.__add_watch(rpath, mask, | 
 | 1914 |                                                             proc_fun, | 
 | 1915 |                                                             auto_add, | 
 | 1916 |                                                             exclude_filter) | 
 | 1917 |                         if wd < 0: | 
 | 1918 |                             err = ('add_watch: cannot watch %s WD=%d, %s' % \ | 
 | 1919 |                                        (rpath, wd, | 
 | 1920 |                                         self._inotify_wrapper.str_errno())) | 
 | 1921 |                             if quiet: | 
 | 1922 |                                 log.error(err) | 
 | 1923 |                             else: | 
 | 1924 |                                 raise WatchManagerError(err, ret_) | 
 | 1925 |                     else: | 
 | 1926 |                         # Let's say -2 means 'explicitely excluded | 
 | 1927 |                         # from watching'. | 
 | 1928 |                         ret_[rpath] = -2 | 
 | 1929 |         return ret_ | 
 | 1930 |  | 
 | 1931 |     def __get_sub_rec(self, lpath): | 
 | 1932 |         """ | 
 | 1933 |         Get every wd from self._wmd if its path is under the path of | 
 | 1934 |         one (at least) of those in lpath. Doesn't follow symlinks. | 
 | 1935 |  | 
 | 1936 |         @param lpath: list of watch descriptor | 
 | 1937 |         @type lpath: list of int | 
 | 1938 |         @return: list of watch descriptor | 
 | 1939 |         @rtype: list of int | 
 | 1940 |         """ | 
 | 1941 |         for d in lpath: | 
 | 1942 |             root = self.get_path(d) | 
 | 1943 |             if root is not None: | 
 | 1944 |                 # always keep root | 
 | 1945 |                 yield d | 
 | 1946 |             else: | 
 | 1947 |                 # if invalid | 
 | 1948 |                 continue | 
 | 1949 |  | 
 | 1950 |             # nothing else to expect | 
 | 1951 |             if not os.path.isdir(root): | 
 | 1952 |                 continue | 
 | 1953 |  | 
 | 1954 |             # normalization | 
 | 1955 |             root = os.path.normpath(root) | 
 | 1956 |             # recursion | 
 | 1957 |             lend = len(root) | 
 | 1958 |             for iwd in self._wmd.items(): | 
 | 1959 |                 cur = iwd[1].path | 
 | 1960 |                 pref = os.path.commonprefix([root, cur]) | 
 | 1961 |                 if root == os.sep or (len(pref) == lend and \ | 
 | 1962 |                                       len(cur) > lend and \ | 
 | 1963 |                                       cur[lend] == os.sep): | 
 | 1964 |                     yield iwd[1].wd | 
 | 1965 |  | 
 | 1966 |     def update_watch(self, wd, mask=None, proc_fun=None, rec=False, | 
 | 1967 |                      auto_add=False, quiet=True): | 
 | 1968 |         """ | 
 | 1969 |         Update existing watch descriptors |wd|. The |mask| value, the | 
 | 1970 |         processing object |proc_fun|, the recursive param |rec| and the | 
 | 1971 |         |auto_add| and |quiet| flags can all be updated. | 
 | 1972 |  | 
 | 1973 |         @param wd: Watch Descriptor to update. Also accepts a list of | 
 | 1974 |                    watch descriptors. | 
 | 1975 |         @type wd: int or list of int | 
 | 1976 |         @param mask: Optional new bitmask of events. | 
 | 1977 |         @type mask: int | 
 | 1978 |         @param proc_fun: Optional new processing function. | 
 | 1979 |         @type proc_fun: function or ProcessEvent instance or instance of | 
 | 1980 |                         one of its subclasses or callable object. | 
 | 1981 |         @param rec: Optionally adds watches recursively on all | 
 | 1982 |                     subdirectories contained into |wd| directory. | 
 | 1983 |         @type rec: bool | 
 | 1984 |         @param auto_add: Automatically adds watches on newly created | 
 | 1985 |                          directories in the watch's path corresponding to |wd|. | 
 | 1986 |                          If |auto_add| is True, IN_CREATE is ored with |mask| | 
 | 1987 |                          when the watch is updated. | 
 | 1988 |         @type auto_add: bool | 
 | 1989 |         @param quiet: If False raises a WatchManagerError exception on | 
 | 1990 |                       error. See example not_quiet.py | 
 | 1991 |         @type quiet: bool | 
 | 1992 |         @return: dict of watch descriptors associated to booleans values. | 
 | 1993 |                  True if the corresponding wd has been successfully | 
 | 1994 |                  updated, False otherwise. | 
 | 1995 |         @rtype: dict of {int: bool} | 
 | 1996 |         """ | 
 | 1997 |         lwd = self.__format_param(wd) | 
 | 1998 |         if rec: | 
 | 1999 |             lwd = self.__get_sub_rec(lwd) | 
 | 2000 |  | 
 | 2001 |         ret_ = {}  # return {wd: bool, ...} | 
 | 2002 |         for awd in lwd: | 
 | 2003 |             apath = self.get_path(awd) | 
 | 2004 |             if not apath or awd < 0: | 
 | 2005 |                 err = 'update_watch: invalid WD=%d' % awd | 
 | 2006 |                 if quiet: | 
 | 2007 |                     log.error(err) | 
 | 2008 |                     continue | 
 | 2009 |                 raise WatchManagerError(err, ret_) | 
 | 2010 |  | 
 | 2011 |             if mask: | 
 | 2012 |                 wd_ = self._inotify_wrapper.inotify_add_watch(self._fd, apath, | 
 | 2013 |                                                               mask) | 
 | 2014 |                 if wd_ < 0: | 
 | 2015 |                     ret_[awd] = False | 
 | 2016 |                     err = ('update_watch: cannot update %s WD=%d, %s' % \ | 
 | 2017 |                                (apath, wd_, self._inotify_wrapper.str_errno())) | 
 | 2018 |                     if quiet: | 
 | 2019 |                         log.error(err) | 
 | 2020 |                         continue | 
 | 2021 |                     raise WatchManagerError(err, ret_) | 
 | 2022 |  | 
 | 2023 |                 assert(awd == wd_) | 
 | 2024 |  | 
 | 2025 |             if proc_fun or auto_add: | 
 | 2026 |                 watch_ = self._wmd[awd] | 
 | 2027 |  | 
 | 2028 |             if proc_fun: | 
 | 2029 |                 watch_.proc_fun = proc_fun | 
 | 2030 |  | 
 | 2031 |             if auto_add: | 
 | 2032 |                 watch_.auto_add = auto_add | 
 | 2033 |  | 
 | 2034 |             ret_[awd] = True | 
 | 2035 |             log.debug('Updated watch - %s', self._wmd[awd]) | 
 | 2036 |         return ret_ | 
 | 2037 |  | 
 | 2038 |     def __format_param(self, param): | 
 | 2039 |         """ | 
 | 2040 |         @param param: Parameter. | 
 | 2041 |         @type param: string or int | 
 | 2042 |         @return: wrap param. | 
 | 2043 |         @rtype: list of type(param) | 
 | 2044 |         """ | 
 | 2045 |         if isinstance(param, list): | 
 | 2046 |             for p_ in param: | 
 | 2047 |                 yield p_ | 
 | 2048 |         else: | 
 | 2049 |             yield param | 
 | 2050 |  | 
 | 2051 |     def get_wd(self, path): | 
 | 2052 |         """ | 
 | 2053 |         Returns the watch descriptor associated to path. This method | 
 | 2054 |         presents a prohibitive cost, always prefer to keep the WD | 
 | 2055 |         returned by add_watch(). If the path is unknown it returns None. | 
 | 2056 |  | 
 | 2057 |         @param path: Path. | 
 | 2058 |         @type path: str | 
 | 2059 |         @return: WD or None. | 
 | 2060 |         @rtype: int or None | 
 | 2061 |         """ | 
 | 2062 |         path = self.__format_path(path) | 
 | 2063 |         for iwd in self._wmd.items(): | 
 | 2064 |             if iwd[1].path == path: | 
 | 2065 |                 return iwd[0] | 
 | 2066 |  | 
 | 2067 |     def get_path(self, wd): | 
 | 2068 |         """ | 
 | 2069 |         Returns the path associated to WD, if WD is unknown it returns None. | 
 | 2070 |  | 
 | 2071 |         @param wd: Watch descriptor. | 
 | 2072 |         @type wd: int | 
 | 2073 |         @return: Path or None. | 
 | 2074 |         @rtype: string or None | 
 | 2075 |         """ | 
 | 2076 |         watch_ = self._wmd.get(wd) | 
 | 2077 |         if watch_ is not None: | 
 | 2078 |             return watch_.path | 
 | 2079 |  | 
 | 2080 |     def __walk_rec(self, top, rec): | 
 | 2081 |         """ | 
 | 2082 |         Yields each subdirectories of top, doesn't follow symlinks. | 
 | 2083 |         If rec is false, only yield top. | 
 | 2084 |  | 
 | 2085 |         @param top: root directory. | 
 | 2086 |         @type top: string | 
 | 2087 |         @param rec: recursive flag. | 
 | 2088 |         @type rec: bool | 
 | 2089 |         @return: path of one subdirectory. | 
 | 2090 |         @rtype: string | 
 | 2091 |         """ | 
 | 2092 |         if not rec or os.path.islink(top) or not os.path.isdir(top): | 
 | 2093 |             yield top | 
 | 2094 |         else: | 
 | 2095 |             for root, dirs, files in os.walk(top): | 
 | 2096 |                 yield root | 
 | 2097 |  | 
 | 2098 |     def rm_watch(self, wd, rec=False, quiet=True): | 
 | 2099 |         """ | 
 | 2100 |         Removes watch(s). | 
 | 2101 |  | 
 | 2102 |         @param wd: Watch Descriptor of the file or directory to unwatch. | 
 | 2103 |                    Also accepts a list of WDs. | 
 | 2104 |         @type wd: int or list of int. | 
 | 2105 |         @param rec: Recursively removes watches on every already watched | 
 | 2106 |                     subdirectories and subfiles. | 
 | 2107 |         @type rec: bool | 
 | 2108 |         @param quiet: If False raises a WatchManagerError exception on | 
 | 2109 |                       error. See example not_quiet.py | 
 | 2110 |         @type quiet: bool | 
 | 2111 |         @return: dict of watch descriptors associated to booleans values. | 
 | 2112 |                  True if the corresponding wd has been successfully | 
 | 2113 |                  removed, False otherwise. | 
 | 2114 |         @rtype: dict of {int: bool} | 
 | 2115 |         """ | 
 | 2116 |         lwd = self.__format_param(wd) | 
 | 2117 |         if rec: | 
 | 2118 |             lwd = self.__get_sub_rec(lwd) | 
 | 2119 |  | 
 | 2120 |         ret_ = {}  # return {wd: bool, ...} | 
 | 2121 |         for awd in lwd: | 
 | 2122 |             # remove watch | 
 | 2123 |             wd_ = self._inotify_wrapper.inotify_rm_watch(self._fd, awd) | 
 | 2124 |             if wd_ < 0: | 
 | 2125 |                 ret_[awd] = False | 
 | 2126 |                 err = ('rm_watch: cannot remove WD=%d, %s' % \ | 
 | 2127 |                            (awd, self._inotify_wrapper.str_errno())) | 
 | 2128 |                 if quiet: | 
 | 2129 |                     log.error(err) | 
 | 2130 |                     continue | 
 | 2131 |                 raise WatchManagerError(err, ret_) | 
 | 2132 |  | 
 | 2133 |             # Remove watch from our dictionary | 
 | 2134 |             if awd in self._wmd: | 
 | 2135 |                 del self._wmd[awd] | 
 | 2136 |             ret_[awd] = True | 
 | 2137 |             log.debug('Watch WD=%d (%s) removed', awd, self.get_path(awd)) | 
 | 2138 |         return ret_ | 
 | 2139 |  | 
 | 2140 |  | 
 | 2141 |     def watch_transient_file(self, filename, mask, proc_class): | 
 | 2142 |         """ | 
 | 2143 |         Watch a transient file, which will be created and deleted frequently | 
 | 2144 |         over time (e.g. pid file). | 
 | 2145 |  | 
 | 2146 |         @attention: Currently under the call to this function it is not | 
 | 2147 |         possible to correctly watch the events triggered into the same | 
 | 2148 |         base directory than the directory where is located this watched | 
 | 2149 |         transient file. For instance it would be wrong to make these | 
 | 2150 |         two successive calls: wm.watch_transient_file('/var/run/foo.pid', ...) | 
 | 2151 |         and wm.add_watch('/var/run/', ...) | 
 | 2152 |  | 
 | 2153 |         @param filename: Filename. | 
 | 2154 |         @type filename: string | 
 | 2155 |         @param mask: Bitmask of events, should contain IN_CREATE and IN_DELETE. | 
 | 2156 |         @type mask: int | 
 | 2157 |         @param proc_class: ProcessEvent (or of one of its subclass), beware of | 
 | 2158 |                            accepting a ProcessEvent's instance as argument into | 
 | 2159 |                            __init__, see transient_file.py example for more | 
 | 2160 |                            details. | 
 | 2161 |         @type proc_class: ProcessEvent's instance or of one of its subclasses. | 
 | 2162 |         @return: Same as add_watch(). | 
 | 2163 |         @rtype: Same as add_watch(). | 
 | 2164 |         """ | 
 | 2165 |         dirname = os.path.dirname(filename) | 
 | 2166 |         if dirname == '': | 
 | 2167 |             return {}  # Maintains coherence with add_watch() | 
 | 2168 |         basename = os.path.basename(filename) | 
 | 2169 |         # Assuming we are watching at least for IN_CREATE and IN_DELETE | 
 | 2170 |         mask |= IN_CREATE | IN_DELETE | 
 | 2171 |  | 
 | 2172 |         def cmp_name(event): | 
 | 2173 |             if getattr(event, 'name') is None: | 
 | 2174 |                 return False | 
 | 2175 |             return basename == event.name | 
 | 2176 |         return self.add_watch(dirname, mask, | 
 | 2177 |                               proc_fun=proc_class(ChainIfTrue(func=cmp_name)), | 
 | 2178 |                               rec=False, | 
 | 2179 |                               auto_add=False, do_glob=False, | 
 | 2180 |                               exclude_filter=lambda path: False) | 
 | 2181 |  | 
 | 2182 |     def get_ignore_events(self): | 
 | 2183 |         return self._ignore_events | 
 | 2184 |  | 
 | 2185 |     def set_ignore_events(self, nval): | 
 | 2186 |         self._ignore_events = nval | 
 | 2187 |  | 
 | 2188 |     ignore_events = property(get_ignore_events, set_ignore_events, | 
 | 2189 |                              "Make watch manager ignoring new events.") | 
 | 2190 |  | 
 | 2191 |  | 
| Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 2192 | class RawOutputFormat: | 
 | 2193 |     """ | 
 | 2194 |     Format string representations. | 
 | 2195 |     """ | 
 | 2196 |     def __init__(self, format=None): | 
 | 2197 |         self.format = format or {} | 
 | 2198 |  | 
 | 2199 |     def simple(self, s, attribute): | 
 | 2200 |         if not isinstance(s, str): | 
 | 2201 |             s = str(s) | 
 | 2202 |         return (self.format.get(attribute, '') + s + | 
 | 2203 |                 self.format.get('normal', '')) | 
 | 2204 |  | 
 | 2205 |     def punctuation(self, s): | 
 | 2206 |         """Punctuation color.""" | 
 | 2207 |         return self.simple(s, 'normal') | 
 | 2208 |  | 
 | 2209 |     def field_value(self, s): | 
 | 2210 |         """Field value color.""" | 
 | 2211 |         return self.simple(s, 'purple') | 
 | 2212 |  | 
 | 2213 |     def field_name(self, s): | 
 | 2214 |         """Field name color.""" | 
 | 2215 |         return self.simple(s, 'blue') | 
 | 2216 |  | 
 | 2217 |     def class_name(self, s): | 
 | 2218 |         """Class name color.""" | 
 | 2219 |         return self.format.get('red', '') + self.simple(s, 'bold') | 
 | 2220 |  | 
 | 2221 | output_format = RawOutputFormat() | 
 | 2222 |  | 
 | 2223 | class ColoredOutputFormat(RawOutputFormat): | 
 | 2224 |     """ | 
 | 2225 |     Format colored string representations. | 
 | 2226 |     """ | 
 | 2227 |     def __init__(self): | 
 | 2228 |         f = {'normal': '\033[0m', | 
 | 2229 |              'black': '\033[30m', | 
 | 2230 |              'red': '\033[31m', | 
 | 2231 |              'green': '\033[32m', | 
 | 2232 |              'yellow': '\033[33m', | 
 | 2233 |              'blue': '\033[34m', | 
 | 2234 |              'purple': '\033[35m', | 
 | 2235 |              'cyan': '\033[36m', | 
 | 2236 |              'bold': '\033[1m', | 
 | 2237 |              'uline': '\033[4m', | 
 | 2238 |              'blink': '\033[5m', | 
 | 2239 |              'invert': '\033[7m'} | 
 | 2240 |         RawOutputFormat.__init__(self, f) | 
 | 2241 |  | 
 | 2242 |  | 
 | 2243 | def compatibility_mode(): | 
 | 2244 |     """ | 
 | 2245 |     Use this function to turn on the compatibility mode. The compatibility | 
 | 2246 |     mode is used to improve compatibility with Pyinotify 0.7.1 (or older) | 
 | 2247 |     programs. The compatibility mode provides additional variables 'is_dir', | 
 | 2248 |     'event_name', 'EventsCodes.IN_*' and 'EventsCodes.ALL_EVENTS' as | 
 | 2249 |     Pyinotify 0.7.1 provided. Do not call this function from new programs!! | 
 | 2250 |     Especially if there are developped for Pyinotify >= 0.8.x. | 
 | 2251 |     """ | 
 | 2252 |     setattr(EventsCodes, 'ALL_EVENTS', ALL_EVENTS) | 
 | 2253 |     for evname in globals(): | 
 | 2254 |         if evname.startswith('IN_'): | 
 | 2255 |             setattr(EventsCodes, evname, globals()[evname]) | 
 | 2256 |     global COMPATIBILITY_MODE | 
 | 2257 |     COMPATIBILITY_MODE = True | 
 | 2258 |  | 
 | 2259 |  | 
 | 2260 | def command_line(): | 
 | 2261 |     """ | 
 | 2262 |     By default the watched path is '/tmp' and all types of events are | 
 | 2263 |     monitored. Events monitoring serves forever, type c^c to stop it. | 
 | 2264 |     """ | 
 | 2265 |     from optparse import OptionParser | 
 | 2266 |  | 
 | 2267 |     usage = "usage: %prog [options] [path1] [path2] [pathn]" | 
 | 2268 |  | 
 | 2269 |     parser = OptionParser(usage=usage) | 
 | 2270 |     parser.add_option("-v", "--verbose", action="store_true", | 
 | 2271 |                       dest="verbose", help="Verbose mode") | 
 | 2272 |     parser.add_option("-r", "--recursive", action="store_true", | 
 | 2273 |                       dest="recursive", | 
 | 2274 |                       help="Add watches recursively on paths") | 
 | 2275 |     parser.add_option("-a", "--auto_add", action="store_true", | 
 | 2276 |                       dest="auto_add", | 
 | 2277 |                       help="Automatically add watches on new directories") | 
 | 2278 |     parser.add_option("-g", "--glob", action="store_true", | 
 | 2279 |                       dest="glob", | 
 | 2280 |                       help="Treat paths as globs") | 
 | 2281 |     parser.add_option("-e", "--events-list", metavar="EVENT[,...]", | 
 | 2282 |                       dest="events_list", | 
 | 2283 |                       help=("A comma-separated list of events to watch for - " | 
 | 2284 |                            "see the documentation for valid options (defaults" | 
 | 2285 |                            " to everything)")) | 
 | 2286 |     parser.add_option("-s", "--stats", action="store_true", | 
 | 2287 |                       dest="stats", | 
 | 2288 |                       help="Display dummy statistics") | 
 | 2289 |     parser.add_option("-V", "--version", action="store_true", | 
 | 2290 |                       dest="version",  help="Pyinotify version") | 
 | 2291 |     parser.add_option("-f", "--raw-format", action="store_true", | 
 | 2292 |                       dest="raw_format", | 
 | 2293 |                       help="Disable enhanced output format.") | 
 | 2294 |     parser.add_option("-c", "--command", action="store", | 
 | 2295 |                       dest="command", | 
 | 2296 |                       help="Shell command to run upon event") | 
 | 2297 |  | 
 | 2298 |     (options, args) = parser.parse_args() | 
 | 2299 |  | 
 | 2300 |     if options.verbose: | 
 | 2301 |         log.setLevel(10) | 
 | 2302 |  | 
 | 2303 |     if options.version: | 
 | 2304 |         print(__version__) | 
 | 2305 |  | 
 | 2306 |     if not options.raw_format: | 
 | 2307 |         global output_format | 
 | 2308 |         output_format = ColoredOutputFormat() | 
 | 2309 |  | 
 | 2310 |     if len(args) < 1: | 
 | 2311 |         path = '/tmp'  # default watched path | 
 | 2312 |     else: | 
 | 2313 |         path = args | 
 | 2314 |  | 
 | 2315 |     # watch manager instance | 
 | 2316 |     wm = WatchManager() | 
 | 2317 |     # notifier instance and init | 
 | 2318 |     if options.stats: | 
 | 2319 |         notifier = Notifier(wm, default_proc_fun=Stats(), read_freq=5) | 
 | 2320 |     else: | 
 | 2321 |         notifier = Notifier(wm, default_proc_fun=PrintAllEvents()) | 
 | 2322 |  | 
 | 2323 |     # What mask to apply | 
 | 2324 |     mask = 0 | 
 | 2325 |     if options.events_list: | 
 | 2326 |         events_list = options.events_list.split(',') | 
 | 2327 |         for ev in events_list: | 
 | 2328 |             evcode = EventsCodes.ALL_FLAGS.get(ev, 0) | 
 | 2329 |             if evcode: | 
 | 2330 |                 mask |= evcode | 
 | 2331 |             else: | 
 | 2332 |                 parser.error("The event '%s' specified with option -e" | 
 | 2333 |                              " is not valid" % ev) | 
 | 2334 |     else: | 
 | 2335 |         mask = ALL_EVENTS | 
 | 2336 |  | 
 | 2337 |     # stats | 
 | 2338 |     cb_fun = None | 
 | 2339 |     if options.stats: | 
 | 2340 |         def cb(s): | 
 | 2341 |             sys.stdout.write(repr(s.proc_fun())) | 
 | 2342 |             sys.stdout.write('\n') | 
 | 2343 |             sys.stdout.write(str(s.proc_fun())) | 
 | 2344 |             sys.stdout.write('\n') | 
 | 2345 |             sys.stdout.flush() | 
 | 2346 |         cb_fun = cb | 
 | 2347 |  | 
 | 2348 |     # External command | 
 | 2349 |     if options.command: | 
 | 2350 |         def cb(s): | 
 | 2351 |             subprocess.Popen(options.command, shell=True) | 
 | 2352 |         cb_fun = cb | 
 | 2353 |  | 
 | 2354 |     log.debug('Start monitoring %s, (press c^c to halt pyinotify)' % path) | 
 | 2355 |  | 
 | 2356 |     wm.add_watch(path, mask, rec=options.recursive, auto_add=options.auto_add, do_glob=options.glob) | 
 | 2357 |     # Loop forever (until sigint signal get caught) | 
 | 2358 |     notifier.loop(callback=cb_fun) | 
 | 2359 |  | 
 | 2360 |  | 
 | 2361 | if __name__ == '__main__': | 
 | 2362 |     command_line() |