panzi_inotify
Source: GitHub
Example
from panzi_inotify import Inotify, get_inotify_event_names
import sys
with Inotify() as inotify:
for filename in sys.argv[1:]:
inotify.add_watch(filename)
for event in inotify:
print(f'{event.full_path()}: {", ".join(get_inotify_event_names(event.mask))}')
See examples for more.
You can also run a basic command line too to listen for events on a set of paths like this:
python -m panzi_inotify [--mask=MASK] <path>...
For more on this see:
python -m panzi_inotify --help
See Also
1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 5""" 6**Source: [GitHub](https://github.com/panzi/panzi-inotify/)** 7 8### Example 9 10```Python 11from panzi_inotify import Inotify, get_inotify_event_names 12 13import sys 14 15with Inotify() as inotify: 16 for filename in sys.argv[1:]: 17 inotify.add_watch(filename) 18 19 for event in inotify: 20 print(f'{event.full_path()}: {", ".join(get_inotify_event_names(event.mask))}') 21``` 22 23See [examples](https://github.com/panzi/panzi-inotify/tree/main/examples) for more. 24 25You can also run a basic command line too to listen for events on a set of paths 26like this: 27 28```bash 29python -m panzi_inotify [--mask=MASK] <path>... 30``` 31 32For more on this see: 33 34```bash 35python -m panzi_inotify --help 36``` 37 38### See Also 39 40[inotify(7)](https://linux.die.net/man/7/inotify) 41""" 42 43from typing import Optional, NamedTuple, Callable, Any, Self, Mapping, Final 44 45import os 46import select 47import ctypes 48import ctypes.util 49import logging 50 51from os import fsencode, fsdecode, readinto 52from os.path import join as join_path 53from struct import Struct 54from errno import ( 55 EINTR, ENOENT, EEXIST, ENOTDIR, EISDIR, 56 EACCES, EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS, 57 ECHILD, EPERM, ETIMEDOUT, EPIPE, ECONNABORTED, 58 ECONNREFUSED, ECONNRESET, ENOSYS, 59) 60 61_logger = logging.getLogger(__name__) 62 63_LIBC_PATH: Optional[str] = None 64 65try: 66 # ctypes.util.find_library() runs multiple external programs (ldconfig, ld, 67 # gcc etc.) to find the library! So try to use dlopen(NULL) first. 68 _LIBC: Optional[ctypes.CDLL] = ctypes.CDLL(None, use_errno=True) 69 70 if not hasattr(_LIBC, 'inotify_init1'): 71 _LIBC_PATH = ctypes.util.find_library('c') or 'libc.so.6' 72 _LIBC = ctypes.CDLL(_LIBC_PATH, use_errno=True) 73 74except OSError as exc: 75 _LIBC = None 76 if _LIBC_PATH is None: 77 _logger.debug('Loading C library: %s', exc, exc_info=exc) 78 else: 79 _logger.debug('Loading %r: %s', _LIBC_PATH, exc, exc_info=exc) 80 81__all__ = ( 82 'InotifyEvent', 83 'Inotify', 84 'PollInotify', 85 'TerminalEventException', 86 'get_inotify_event_names', 87 'HAS_INOTIFY', 88 'IN_CLOEXEC', 89 'IN_NONBLOCK', 90 'IN_ACCESS', 91 'IN_MODIFY', 92 'IN_ATTRIB', 93 'IN_CLOSE_WRITE', 94 'IN_CLOSE_NOWRITE', 95 'IN_OPEN', 96 'IN_MOVED_FROM', 97 'IN_MOVED_TO', 98 'IN_CREATE', 99 'IN_DELETE', 100 'IN_DELETE_SELF', 101 'IN_MOVE_SELF', 102 'IN_UNMOUNT', 103 'IN_Q_OVERFLOW', 104 'IN_IGNORED', 105 'IN_CLOSE', 106 'IN_MOVE', 107 'IN_ONLYDIR', 108 'IN_DONT_FOLLOW', 109 'IN_EXCL_UNLINK', 110 'IN_MASK_CREATE', 111 'IN_MASK_ADD', 112 'IN_ISDIR', 113 'IN_ONESHOT', 114 'IN_ALL_EVENTS', 115 'INOTIFY_MASK_CODES', 116) 117 118_HEADER_STRUCT: Final[Struct] = Struct('iIII') 119_HEADER_STRUCT_unpack_from: Final = _HEADER_STRUCT.unpack_from 120_HEADER_STRUCT_size: Final[int] = _HEADER_STRUCT.size 121 122# sizeof(struct inotify_event) + NAME_MAX + 1 123_EVENT_SIZE_MAX: Final[int] = _HEADER_STRUCT_size + 256 124 125# From linux/inotify.h 126 127# Flags for sys_inotify_init1 128 129IN_CLOEXEC : Final[int] = os.O_CLOEXEC ; "Close inotify file descriptor on exec.\n\n**See Also:** `Inotify.__init__()`" 130IN_NONBLOCK: Final[int] = os.O_NONBLOCK; "Open inotify file descriptor as non-blocking.\n\n**See Also:** `Inotify.__init__()`" 131 132# the following are legal, implemented events that user-space can watch for 133IN_ACCESS : Final[int] = 0x00000001; "File was accessed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 134IN_MODIFY : Final[int] = 0x00000002; "File was modified.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 135IN_ATTRIB : Final[int] = 0x00000004; "Metadata changed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 136IN_CLOSE_WRITE : Final[int] = 0x00000008; "Writtable file was closed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 137IN_CLOSE_NOWRITE: Final[int] = 0x00000010; "Unwrittable file closed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 138IN_OPEN : Final[int] = 0x00000020; "File was opened.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 139IN_MOVED_FROM : Final[int] = 0x00000040; "File was moved from X.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 140IN_MOVED_TO : Final[int] = 0x00000080; "File was moved to Y.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 141IN_CREATE : Final[int] = 0x00000100; "Subfile was created.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 142IN_DELETE : Final[int] = 0x00000200; "Subfile was deleted.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 143IN_DELETE_SELF : Final[int] = 0x00000400; "Self was deleted.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 144IN_MOVE_SELF : Final[int] = 0x00000800; "Self was moved.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 145 146# the following are legal events. they are sent as needed to any watch 147IN_UNMOUNT : Final[int] = 0x00002000; "Backing file system was unmounted.\n\n**See Also:** `InotifyEvent.mask`" 148IN_Q_OVERFLOW: Final[int] = 0x00004000; "Event queued overflowed.\n\n**See Also:** `InotifyEvent.mask`" 149IN_IGNORED : Final[int] = 0x00008000; "File was ignored.\n\n**See Also:** `InotifyEvent.mask`" 150 151# helper events 152IN_CLOSE: Final[int] = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE; "All close events.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 153IN_MOVE : Final[int] = IN_MOVED_FROM | IN_MOVED_TO ; "All move events.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 154 155# special flags 156IN_ONLYDIR : Final[int] = 0x01000000; "Only watch the path if it is a directory.\n\n**See Also:** `Inotify.add_watch()`" 157IN_DONT_FOLLOW: Final[int] = 0x02000000; "Don't follow symbolic links.\n\n**See Also:** `Inotify.add_watch()`" 158IN_EXCL_UNLINK: Final[int] = 0x04000000; "Exclude events on unlinked objects.\n\n**See Also:** `Inotify.add_watch()`" 159IN_MASK_CREATE: Final[int] = 0x10000000; "Only create watches.\n\n**See Also:** `Inotify.add_watch()`" 160IN_MASK_ADD : Final[int] = 0x20000000; "Add to the mask of an already existing watch.\n\n**See Also:** `Inotify.add_watch()`" 161IN_ISDIR : Final[int] = 0x40000000; "Event occurred against directory.\n\n**See Also:** `InotifyEvent.mask`" 162IN_ONESHOT : Final[int] = 0x80000000; "Only send event once.\n\n**See Also:** `Inotify.add_watch()`" 163 164IN_ALL_EVENTS: Final[int] = ( 165 IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | 166 IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | 167 IN_MOVED_TO | IN_DELETE | IN_CREATE | IN_DELETE_SELF | 168 IN_MOVE_SELF 169); "All of the events.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`" 170 171INOTIFY_MASK_CODES: Final[Mapping[int, str]] = { 172 globals()[_key]: _key.removeprefix('IN_') 173 for _key in ( 174 'IN_ACCESS', 175 'IN_MODIFY', 176 'IN_ATTRIB', 177 'IN_CLOSE_WRITE', 178 'IN_CLOSE_NOWRITE', 179 'IN_OPEN', 180 'IN_MOVED_FROM', 181 'IN_MOVED_TO', 182 'IN_CREATE', 183 'IN_DELETE', 184 'IN_DELETE_SELF', 185 'IN_MOVE_SELF', 186 187 'IN_UNMOUNT', 188 'IN_Q_OVERFLOW', 189 'IN_IGNORED', 190 191 'IN_ISDIR', 192 ) 193}; "Mapping from inotify event mask flag to it's name.\n\n**See Also:** `InotifyEvent.mask`" 194 195def get_inotify_event_names(mask: int) -> list[str]: 196 """ 197 Get a list of event names from an event mask as returned by inotify. 198 199 If there is a flag set that isn't a know name the hexadecimal representation 200 of that flag is also returned. 201 """ 202 names: list[str] = [] 203 for code, name in INOTIFY_MASK_CODES.items(): 204 if mask & code: 205 names.append(name) 206 mask &= ~code 207 if not mask: 208 break 209 210 if mask: 211 for bit in range(0, 32): 212 if bit & mask: 213 names.append('0x%x' % bit) 214 mask &= ~bit 215 if not mask: 216 break 217 218 return names 219 220def _check_return(value: int, filename: Optional[str] = None) -> int: 221 if value < 0: 222 errnum = ctypes.get_errno() 223 message = os.strerror(errnum) 224 225 if errnum == ENOENT: 226 raise FileNotFoundError(errnum, message, filename) 227 228 if errnum in (EACCES, EPERM): 229 raise PermissionError(errnum, message, filename) 230 231 if errnum == EINTR: 232 raise InterruptedError(errnum, message, filename) 233 234 if errnum == ENOTDIR: 235 raise NotADirectoryError(errnum, message, filename) 236 237 if errnum == EEXIST: 238 raise FileExistsError(errnum, message, filename) 239 240 if errnum == EISDIR: 241 raise IsADirectoryError(errnum, message, filename) 242 243 if errnum in (EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS): 244 raise BlockingIOError(errnum, message, filename) 245 246 if errnum == ETIMEDOUT: 247 raise TimeoutError(errnum, message, filename) 248 249 if errnum == ECHILD: 250 raise ChildProcessError(errnum, message, filename) 251 252 if errnum == EPIPE: 253 raise BrokenPipeError(errnum, message, filename) 254 255 if errnum == ECONNABORTED: 256 raise ConnectionAbortedError(errnum, message, filename) 257 258 if errnum == ECONNREFUSED: 259 raise ConnectionRefusedError(errnum, message, filename) 260 261 if errnum == ECONNRESET: 262 raise ConnectionResetError(errnum, message, filename) 263 264 raise OSError(errnum, message, filename) 265 266 return value 267 268HAS_INOTIFY = True; "`True` if your libc exports `inotify_init1`, `inotify_add_watch`, and `inotify_rm_watch`, otherwise `False`." 269 270_CDataType = type[ctypes.c_int]|type[ctypes.c_char_p]|type[ctypes.c_uint32] 271 272def _load_sym(name: str, argtypes: tuple[_CDataType, ...], restype: _CDataType|Callable[[int], Any]) -> Callable: 273 global HAS_INOTIFY 274 275 if sym := getattr(_LIBC, name, None): 276 sym.argtypes = argtypes 277 sym.restype = restype 278 return sym 279 280 else: 281 HAS_INOTIFY = False 282 283 def symbol_not_found(*args, **kwargs): 284 raise OSError(ENOSYS, f'{name} is not supported') 285 symbol_not_found.__name__ = name 286 287 return symbol_not_found 288 289inotify_init1 = _load_sym('inotify_init1', (ctypes.c_int,), _check_return) 290inotify_add_watch = _load_sym('inotify_add_watch', ( 291 ctypes.c_int, 292 ctypes.c_char_p, 293 ctypes.c_uint32, 294), ctypes.c_int) 295inotify_rm_watch = _load_sym('inotify_rm_watch', (ctypes.c_int, ctypes.c_int), ctypes.c_int) 296 297class TerminalEventException(Exception): 298 """ 299 Exception raised by `Inotify.read_events()` when an event mask contains one 300 of the specified `terminal_events`. 301 """ 302 303 __slots__ = ( 304 'wd', 305 'mask', 306 'watch_path', 307 'filename', 308 ) 309 310 wd: int; "Inotify watch descriptor." 311 mask: int; "Bitset of the events that occured." 312 watch_path: Optional[str]; "Path of the watched file, `None` if the Python code doesn't know about the watch." 313 filename: Optional[str]; "If the event is about the child of a watched directory, this is the name of that file, otherwise `None`." 314 315 def __init__(self, wd: int, mask: int, watch_path: Optional[str], filename: Optional[str]) -> None: 316 super().__init__(wd, mask, watch_path, filename) 317 self.wd = wd 318 self.mask = mask 319 self.watch_path = watch_path 320 self.filename = filename 321 322class InotifyEvent(NamedTuple): 323 """ 324 Inotify event as read from the inotify file handle. 325 """ 326 wd: int; "Inotify watch descriptor." 327 mask: int; """ 328 Bit set of the events that occured and other information. 329 330 The flags can be: 331 - `IN_ACCESS` - File was accessed. 332 - `IN_MODIFY` - File was modified. 333 - `IN_ATTRIB` - Metadata changed. 334 - `IN_CLOSE_WRITE` - Writtable file was closed. 335 - `IN_CLOSE_NOWRITE` - Unwrittable file closed. 336 - `IN_OPEN` - File was opened. 337 - `IN_MOVED_FROM` - File was moved from X. 338 - `IN_MOVED_TO` - File was moved to Y. 339 - `IN_CREATE` - Subfile was created. 340 - `IN_DELETE` - Subfile was deleted. 341 - `IN_DELETE_SELF` - Self was deleted. 342 - `IN_MOVE_SELF` - Self was moved. 343 - `IN_UNMOUNT` - Backing file system was unmounted. 344 - `IN_Q_OVERFLOW` - Event queued overflowed. 345 - `IN_IGNORED` - File was ignored. 346 - `IN_ISDIR` - Event occurred against directory. 347 """ 348 cookie: int; """\ 349 Unique cookie associating related events (for 350 [rename(2)](https://linux.die.net/man/2/rename)). 351 """ 352 filename_len: int; "Original byte-size of the filename field." 353 watch_path: str; """\ 354 Path of the watched file as it was registered. 355 356 **NOTE:** If the watched file or directory itself is moved/renamed 357 `watch_path` for any further events will *still* be the orignial path 358 that was registered. This is because it is not possible to determine 359 the new file name in a `IN_MOVE_SELF` event and thus this cannot be 360 updated. 361 """ 362 filename: Optional[str]; """\ 363 If the event is about the child of a watched directory, this is the name 364 of that file, otherwise `None`. 365 """ 366 367 def full_path(self) -> str: 368 """ 369 Join `watch_path` and `filename`, or only `watch_path` if `filename` is `None`. 370 """ 371 filename = self.filename 372 return join_path(self.watch_path, filename) if filename is not None else self.watch_path 373 374class Inotify: 375 """ 376 Listen for inotify events. 377 378 Supports the context manager and iterator protocols. 379 """ 380 __slots__ = ( 381 '_inotify_fd', 382 '_buffer', 383 '_offset', 384 '_size', 385 '_path_to_wd', 386 '_wd_to_path', 387 ) 388 389 _inotify_fd: int 390 _buffer: bytearray 391 _offset: int 392 _size: int 393 _path_to_wd: dict[str, int] 394 _wd_to_path: dict[int, str] 395 396 def __init__(self, flags: int = IN_CLOEXEC, buffer_size: int = 4096) -> None: 397 """ 398 `flags` is a bit set of these values: 399 - `IN_CLOEXEC` - Close inotify file descriptor on exec. 400 - `IN_NONBLOCK` - Open inotify file descriptor as non-blocking. 401 402 It's recommended to pass the `IN_CLOEXEC` flag (default) to close the 403 file descriptor on [exec*(2)](https://linux.die.net/man/3/execl). 404 405 `buffer_size` is the size of the used input buffer and needs to be at 406 least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`). 407 408 This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 409 and thus might raise an `OSError` with one of these `errno` values: 410 - `EINVAL` 411 - `EMFILE` 412 - `ENOMEM` 413 - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 414 """ 415 self._inotify_fd = -1 416 self._offset = 0 417 self._size = 0 418 self._path_to_wd = {} 419 self._wd_to_path = {} 420 421 if buffer_size < _EVENT_SIZE_MAX: 422 raise ValueError(f'buffer_size too small: {buffer_size} < {_EVENT_SIZE_MAX}') 423 424 self._buffer = bytearray(buffer_size) 425 self._inotify_fd = inotify_init1(flags) 426 427 def fileno(self) -> int: 428 """ 429 The inotify file descriptor. 430 431 You can use this to call `poll()` or similar yourself 432 instead of using `PollInotify.wait()`. 433 """ 434 return self._inotify_fd 435 436 @property 437 def closed(self) -> bool: 438 """ 439 `True` if the inotify file descriptor was closed. 440 """ 441 return self._inotify_fd == -1 442 443 def close(self) -> None: 444 """ 445 Close the inotify handle. 446 447 Can safely be called multiple times, but you 448 can't call any other methods once closed. 449 """ 450 try: 451 if self._inotify_fd != -1: 452 # A crash during inotify_init1() in __init__() means 453 # self._inotify_stream is not assigned, but self._inotify_fd 454 # is initialized with -1. 455 # Since this method is called in __del__() this state needs 456 # be handled. 457 os.close(self._inotify_fd) 458 finally: 459 self._inotify_fd = -1 460 461 def __enter__(self) -> Self: 462 return self 463 464 def __exit__(self, exc_type, exc_value, traceback) -> None: 465 self.close() 466 467 def __del__(self) -> None: 468 self.close() 469 470 def add_watch(self, path: str, mask: int = IN_ALL_EVENTS) -> int: 471 """ 472 Add a watch path. 473 474 `mask` is a bit set of these event flags: 475 - `IN_ACCESS` - File was accessed. 476 - `IN_MODIFY` - File was modified. 477 - `IN_ATTRIB` - Metadata changed. 478 - `IN_CLOSE_WRITE` - Writtable file was closed. 479 - `IN_CLOSE_NOWRITE` - Unwrittable file closed. 480 - `IN_OPEN` - File was opened. 481 - `IN_MOVED_FROM` - File was moved from X. 482 - `IN_MOVED_TO` - File was moved to Y. 483 - `IN_CREATE` - Subfile was created. 484 - `IN_DELETE` - Subfile was deleted. 485 - `IN_DELETE_SELF` - Self was deleted. 486 - `IN_MOVE_SELF` - Self was moved. 487 488 And these additional flags: 489 - `IN_ONLYDIR` - Only watch the path if it is a directory. 490 - `IN_DONT_FOLLOW` - Don't follow symbolic links. 491 - `IN_EXCL_UNLINK` - Exclude events on unlinked objects. 492 - `IN_MASK_CREATE` - Only create watches. 493 - `IN_MASK_ADD` - Add to the mask of an already existing watch. 494 - `IN_ONESHOT` - Only send event once. 495 496 This calls [inotify_add_watch(2)](https://linux.die.net/man/2/inotify_add_watch) 497 and thus might raise one of these exceptions: 498 - `PermissionError` (`EACCES`) 499 - `FileExistsError` (`EEXISTS`) 500 - `FileNotFoundError` (`ENOENT`) 501 - `NotADirectoryError` (`ENOTDIR`) 502 - `OSError` (`WBADF`, `EFAULT`, `EINVAL`, `ENAMETOOLONG`, 503 `ENOMEM`, `ENOSPC`, `ENOSYS` if your libc doesn't support 504 `inotify_rm_watch()`) 505 """ 506 path_bytes = fsencode(path) 507 508 wd = inotify_add_watch(self._inotify_fd, path_bytes, mask) 509 _check_return(wd, path) 510 511 self._path_to_wd[path] = wd 512 self._wd_to_path[wd] = path 513 514 return wd 515 516 def remove_watch(self, path: str) -> None: 517 """ 518 Remove watch by path. 519 520 Does nothing if the path is not watched. 521 522 This calls [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch) 523 and this might raise an `OSError` with one of these `errno` values: 524 - `EBADF` 525 - `EINVAL` 526 - `ENOSYS` if your libc doesn't support [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch) 527 """ 528 wd = self._path_to_wd.get(path) 529 if wd is None: 530 _logger.debug('%s: Path is not watched', path) 531 return 532 533 try: 534 res = inotify_rm_watch(self._inotify_fd, wd) 535 _check_return(res, path) 536 finally: 537 del self._path_to_wd[path] 538 del self._wd_to_path[wd] 539 540 def remove_watch_with_id(self, wd: int) -> None: 541 """ 542 Remove watch by handle. 543 544 Does nothing if the handle is invalid. 545 546 This calls `inotify_rm_watch()` and this might raise an 547 `OSError` with one of these `errno` values: 548 - `EBADF` 549 - `EINVAL` 550 - `ENOSYS` if your libc doesn't support `inotify_rm_watch()` 551 """ 552 path = self._wd_to_path.get(wd) 553 if path is None: 554 _logger.debug('%d: Invalid handle', wd) 555 return 556 557 try: 558 res = inotify_rm_watch(self._inotify_fd, wd) 559 _check_return(res, path) 560 finally: 561 del self._wd_to_path[wd] 562 del self._path_to_wd[path] 563 564 def watch_paths(self) -> set[str]: 565 """ 566 Get the set of the watched paths. 567 """ 568 return set(self._path_to_wd) 569 570 def get_watch_id(self, path: str) -> Optional[int]: 571 """ 572 Get the watch id to a path, if the path is watched. 573 """ 574 return self._path_to_wd.get(path) 575 576 def get_watch_path(self, wd: int) -> Optional[str]: 577 """ 578 Get the path to a watch id, if the watch id is valid. 579 """ 580 return self._wd_to_path.get(wd) 581 582 def read_event(self) -> Optional[InotifyEvent]: 583 """ 584 Read a single event. Might return `None` if there is none avaialbe. 585 """ 586 buffer = self._buffer 587 offset = self._offset 588 wd_to_path = self._wd_to_path 589 590 while True: 591 if offset >= self._size: 592 # Only whole records are ever returned by a read() on an inotify file descriptor. 593 # If the file descriptor is blocking a read() only blocks until the first event 594 # is available. 595 self._offset = offset = 0 596 try: 597 self._size = readinto(self._inotify_fd, buffer) 598 except BlockingIOError: 599 self._size = 0 600 return None 601 602 if self._size == 0: 603 return None 604 605 wd, mask, cookie, filename_len = _HEADER_STRUCT_unpack_from(buffer, offset) 606 offset += _HEADER_STRUCT_size 607 self._offset = filename_end_offset = offset + filename_len 608 609 if filename_len: 610 filename_bytes = buffer[offset:filename_end_offset] 611 index = filename_bytes.find(b'\0') 612 # I don't like the need for bytes() here, fsdecode() doesn't take a bytearray and 613 # you can't get a bytes slice in one step from a bytearray. 614 # I guess I could do this instead? 615 # filename = (filename_bytes[:index] if index >= 0 else filename_bytes).decode(sys.getfilesystemencoding(), errors='surrogateescape') 616 filename = fsdecode(bytes(filename_bytes[:index] if index >= 0 else filename_bytes)) 617 else: 618 filename = None 619 620 watch_path = wd_to_path.get(wd) 621 622 if mask & IN_IGNORED and watch_path is not None: 623 wd_to_path.pop(wd, None) 624 self._path_to_wd.pop(watch_path, None) 625 626 offset = filename_end_offset 627 628 if watch_path is None: 629 _logger.debug('Got inotify event for unknown watch handle: %d, mask: %d, cookie: %d', wd, mask, cookie) 630 continue 631 632 return InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename) 633 634 def read_events(self, terminal_events: int = IN_Q_OVERFLOW | IN_UNMOUNT) -> list[InotifyEvent]: 635 """ 636 Read available events. Might return an empty list if there are none 637 available. 638 639 `terminal_events` is per default: `IN_Q_OVERFLOW` | `IN_UNMOUNT` 640 641 **NOTE:** Don't use this in blocking mode! It will never return. 642 643 Raises `TerminalEventException` if any of the flags in `terminal_events` 644 are set in an event `mask`. 645 """ 646 events: list[InotifyEvent] = [] 647 648 while event := self.read_event(): 649 if event.mask & terminal_events: 650 raise TerminalEventException(event.wd, event.mask, event.watch_path, event.filename) 651 652 events.append(event) 653 654 return events 655 656 def __iter__(self) -> Self: 657 """ 658 `Inotify` can act as an iterator over the available events. 659 """ 660 return self 661 662 def __next__(self) -> InotifyEvent: 663 """ 664 `Inotify` can act as an iterator over the available events. 665 """ 666 event = self.read_event() 667 if event is None: 668 raise StopIteration 669 return event 670 671class PollInotify(Inotify): 672 """ 673 Listen for inotify events. 674 675 In addition to the functionality of `Inotify` this class adds a `wait()` 676 method that waits for events using `epoll`. If you use `Inotify` you 677 need/can to do that yourself. 678 679 Supports the context manager and iterator protocols. 680 """ 681 __slots__ = ( 682 '_epoll', 683 '_stopfd', 684 ) 685 686 _epoll: select.epoll 687 _stopfd: Optional[int] 688 689 def __init__(self, stopfd: Optional[int] = None, buffer_size: int = 4096) -> None: 690 """ 691 If not `None` then `stopfd` is a file descriptor that will 692 be added to the `poll()` call in `PollInotify.wait()`. 693 694 `buffer_size` is the size of the used input buffer and needs to be at 695 least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`). 696 697 This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 698 and thus might raise an `OSError` with one of these `errno` values: 699 - `EINVAL` (shouldn't happen) 700 - `EMFILE` 701 - `ENOMEM` 702 - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 703 """ 704 super().__init__(IN_NONBLOCK | IN_CLOEXEC, buffer_size) 705 self._stopfd = stopfd 706 self._epoll = select.epoll(1 if stopfd is None else 2) 707 self._epoll.register(self._inotify_fd, select.POLLIN) 708 709 if stopfd is not None: 710 self._epoll.register(stopfd, select.POLLIN) 711 712 @property 713 def stopfd(self) -> Optional[int]: 714 """ 715 The `stopfd` parameter of `__init__()`, used in `wait()`. 716 """ 717 return self._stopfd 718 719 def close(self) -> None: 720 """ 721 Close the inotify and epoll handles. 722 723 Can safely be called multiple times, but you 724 can't call any other methods once closed. 725 """ 726 try: 727 super().close() 728 finally: 729 epoll = self._epoll 730 if not epoll.closed: 731 epoll.close() 732 733 def wait(self, timeout: Optional[float] = None) -> bool: 734 """ 735 Wait for inotify events or for any `POLLIN` on `stopfd`, 736 if that is not `None`. If `stopfd` signals this function will 737 return `False`, otherwise `True`. 738 739 Raises `TimeoutError` if `timeout` is not `None` and 740 the operation has expired. 741 742 This method uses using `select.epoll.poll()`, see there for 743 additional possible exceptions. 744 """ 745 events = self._epoll.poll(timeout) 746 747 if not events and timeout is not None and timeout >= 0.0: 748 raise TimeoutError 749 750 stopfd = self._stopfd 751 if stopfd is not None: 752 for fd, mask in events: 753 if fd == stopfd: 754 return False 755 756 return True
323class InotifyEvent(NamedTuple): 324 """ 325 Inotify event as read from the inotify file handle. 326 """ 327 wd: int; "Inotify watch descriptor." 328 mask: int; """ 329 Bit set of the events that occured and other information. 330 331 The flags can be: 332 - `IN_ACCESS` - File was accessed. 333 - `IN_MODIFY` - File was modified. 334 - `IN_ATTRIB` - Metadata changed. 335 - `IN_CLOSE_WRITE` - Writtable file was closed. 336 - `IN_CLOSE_NOWRITE` - Unwrittable file closed. 337 - `IN_OPEN` - File was opened. 338 - `IN_MOVED_FROM` - File was moved from X. 339 - `IN_MOVED_TO` - File was moved to Y. 340 - `IN_CREATE` - Subfile was created. 341 - `IN_DELETE` - Subfile was deleted. 342 - `IN_DELETE_SELF` - Self was deleted. 343 - `IN_MOVE_SELF` - Self was moved. 344 - `IN_UNMOUNT` - Backing file system was unmounted. 345 - `IN_Q_OVERFLOW` - Event queued overflowed. 346 - `IN_IGNORED` - File was ignored. 347 - `IN_ISDIR` - Event occurred against directory. 348 """ 349 cookie: int; """\ 350 Unique cookie associating related events (for 351 [rename(2)](https://linux.die.net/man/2/rename)). 352 """ 353 filename_len: int; "Original byte-size of the filename field." 354 watch_path: str; """\ 355 Path of the watched file as it was registered. 356 357 **NOTE:** If the watched file or directory itself is moved/renamed 358 `watch_path` for any further events will *still* be the orignial path 359 that was registered. This is because it is not possible to determine 360 the new file name in a `IN_MOVE_SELF` event and thus this cannot be 361 updated. 362 """ 363 filename: Optional[str]; """\ 364 If the event is about the child of a watched directory, this is the name 365 of that file, otherwise `None`. 366 """ 367 368 def full_path(self) -> str: 369 """ 370 Join `watch_path` and `filename`, or only `watch_path` if `filename` is `None`. 371 """ 372 filename = self.filename 373 return join_path(self.watch_path, filename) if filename is not None else self.watch_path
Inotify event as read from the inotify file handle.
Create new instance of InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename)
Bit set of the events that occured and other information.
The flags can be:
IN_ACCESS- File was accessed.IN_MODIFY- File was modified.IN_ATTRIB- Metadata changed.IN_CLOSE_WRITE- Writtable file was closed.IN_CLOSE_NOWRITE- Unwrittable file closed.IN_OPEN- File was opened.IN_MOVED_FROM- File was moved from X.IN_MOVED_TO- File was moved to Y.IN_CREATE- Subfile was created.IN_DELETE- Subfile was deleted.IN_DELETE_SELF- Self was deleted.IN_MOVE_SELF- Self was moved.IN_UNMOUNT- Backing file system was unmounted.IN_Q_OVERFLOW- Event queued overflowed.IN_IGNORED- File was ignored.IN_ISDIR- Event occurred against directory.
Path of the watched file as it was registered.
NOTE: If the watched file or directory itself is moved/renamed
watch_path for any further events will still be the orignial path
that was registered. This is because it is not possible to determine
the new file name in a IN_MOVE_SELF event and thus this cannot be
updated.
If the event is about the child of a watched directory, this is the name
of that file, otherwise None.
368 def full_path(self) -> str: 369 """ 370 Join `watch_path` and `filename`, or only `watch_path` if `filename` is `None`. 371 """ 372 filename = self.filename 373 return join_path(self.watch_path, filename) if filename is not None else self.watch_path
Join watch_path and filename, or only watch_path if filename is None.
375class Inotify: 376 """ 377 Listen for inotify events. 378 379 Supports the context manager and iterator protocols. 380 """ 381 __slots__ = ( 382 '_inotify_fd', 383 '_buffer', 384 '_offset', 385 '_size', 386 '_path_to_wd', 387 '_wd_to_path', 388 ) 389 390 _inotify_fd: int 391 _buffer: bytearray 392 _offset: int 393 _size: int 394 _path_to_wd: dict[str, int] 395 _wd_to_path: dict[int, str] 396 397 def __init__(self, flags: int = IN_CLOEXEC, buffer_size: int = 4096) -> None: 398 """ 399 `flags` is a bit set of these values: 400 - `IN_CLOEXEC` - Close inotify file descriptor on exec. 401 - `IN_NONBLOCK` - Open inotify file descriptor as non-blocking. 402 403 It's recommended to pass the `IN_CLOEXEC` flag (default) to close the 404 file descriptor on [exec*(2)](https://linux.die.net/man/3/execl). 405 406 `buffer_size` is the size of the used input buffer and needs to be at 407 least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`). 408 409 This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 410 and thus might raise an `OSError` with one of these `errno` values: 411 - `EINVAL` 412 - `EMFILE` 413 - `ENOMEM` 414 - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 415 """ 416 self._inotify_fd = -1 417 self._offset = 0 418 self._size = 0 419 self._path_to_wd = {} 420 self._wd_to_path = {} 421 422 if buffer_size < _EVENT_SIZE_MAX: 423 raise ValueError(f'buffer_size too small: {buffer_size} < {_EVENT_SIZE_MAX}') 424 425 self._buffer = bytearray(buffer_size) 426 self._inotify_fd = inotify_init1(flags) 427 428 def fileno(self) -> int: 429 """ 430 The inotify file descriptor. 431 432 You can use this to call `poll()` or similar yourself 433 instead of using `PollInotify.wait()`. 434 """ 435 return self._inotify_fd 436 437 @property 438 def closed(self) -> bool: 439 """ 440 `True` if the inotify file descriptor was closed. 441 """ 442 return self._inotify_fd == -1 443 444 def close(self) -> None: 445 """ 446 Close the inotify handle. 447 448 Can safely be called multiple times, but you 449 can't call any other methods once closed. 450 """ 451 try: 452 if self._inotify_fd != -1: 453 # A crash during inotify_init1() in __init__() means 454 # self._inotify_stream is not assigned, but self._inotify_fd 455 # is initialized with -1. 456 # Since this method is called in __del__() this state needs 457 # be handled. 458 os.close(self._inotify_fd) 459 finally: 460 self._inotify_fd = -1 461 462 def __enter__(self) -> Self: 463 return self 464 465 def __exit__(self, exc_type, exc_value, traceback) -> None: 466 self.close() 467 468 def __del__(self) -> None: 469 self.close() 470 471 def add_watch(self, path: str, mask: int = IN_ALL_EVENTS) -> int: 472 """ 473 Add a watch path. 474 475 `mask` is a bit set of these event flags: 476 - `IN_ACCESS` - File was accessed. 477 - `IN_MODIFY` - File was modified. 478 - `IN_ATTRIB` - Metadata changed. 479 - `IN_CLOSE_WRITE` - Writtable file was closed. 480 - `IN_CLOSE_NOWRITE` - Unwrittable file closed. 481 - `IN_OPEN` - File was opened. 482 - `IN_MOVED_FROM` - File was moved from X. 483 - `IN_MOVED_TO` - File was moved to Y. 484 - `IN_CREATE` - Subfile was created. 485 - `IN_DELETE` - Subfile was deleted. 486 - `IN_DELETE_SELF` - Self was deleted. 487 - `IN_MOVE_SELF` - Self was moved. 488 489 And these additional flags: 490 - `IN_ONLYDIR` - Only watch the path if it is a directory. 491 - `IN_DONT_FOLLOW` - Don't follow symbolic links. 492 - `IN_EXCL_UNLINK` - Exclude events on unlinked objects. 493 - `IN_MASK_CREATE` - Only create watches. 494 - `IN_MASK_ADD` - Add to the mask of an already existing watch. 495 - `IN_ONESHOT` - Only send event once. 496 497 This calls [inotify_add_watch(2)](https://linux.die.net/man/2/inotify_add_watch) 498 and thus might raise one of these exceptions: 499 - `PermissionError` (`EACCES`) 500 - `FileExistsError` (`EEXISTS`) 501 - `FileNotFoundError` (`ENOENT`) 502 - `NotADirectoryError` (`ENOTDIR`) 503 - `OSError` (`WBADF`, `EFAULT`, `EINVAL`, `ENAMETOOLONG`, 504 `ENOMEM`, `ENOSPC`, `ENOSYS` if your libc doesn't support 505 `inotify_rm_watch()`) 506 """ 507 path_bytes = fsencode(path) 508 509 wd = inotify_add_watch(self._inotify_fd, path_bytes, mask) 510 _check_return(wd, path) 511 512 self._path_to_wd[path] = wd 513 self._wd_to_path[wd] = path 514 515 return wd 516 517 def remove_watch(self, path: str) -> None: 518 """ 519 Remove watch by path. 520 521 Does nothing if the path is not watched. 522 523 This calls [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch) 524 and this might raise an `OSError` with one of these `errno` values: 525 - `EBADF` 526 - `EINVAL` 527 - `ENOSYS` if your libc doesn't support [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch) 528 """ 529 wd = self._path_to_wd.get(path) 530 if wd is None: 531 _logger.debug('%s: Path is not watched', path) 532 return 533 534 try: 535 res = inotify_rm_watch(self._inotify_fd, wd) 536 _check_return(res, path) 537 finally: 538 del self._path_to_wd[path] 539 del self._wd_to_path[wd] 540 541 def remove_watch_with_id(self, wd: int) -> None: 542 """ 543 Remove watch by handle. 544 545 Does nothing if the handle is invalid. 546 547 This calls `inotify_rm_watch()` and this might raise an 548 `OSError` with one of these `errno` values: 549 - `EBADF` 550 - `EINVAL` 551 - `ENOSYS` if your libc doesn't support `inotify_rm_watch()` 552 """ 553 path = self._wd_to_path.get(wd) 554 if path is None: 555 _logger.debug('%d: Invalid handle', wd) 556 return 557 558 try: 559 res = inotify_rm_watch(self._inotify_fd, wd) 560 _check_return(res, path) 561 finally: 562 del self._wd_to_path[wd] 563 del self._path_to_wd[path] 564 565 def watch_paths(self) -> set[str]: 566 """ 567 Get the set of the watched paths. 568 """ 569 return set(self._path_to_wd) 570 571 def get_watch_id(self, path: str) -> Optional[int]: 572 """ 573 Get the watch id to a path, if the path is watched. 574 """ 575 return self._path_to_wd.get(path) 576 577 def get_watch_path(self, wd: int) -> Optional[str]: 578 """ 579 Get the path to a watch id, if the watch id is valid. 580 """ 581 return self._wd_to_path.get(wd) 582 583 def read_event(self) -> Optional[InotifyEvent]: 584 """ 585 Read a single event. Might return `None` if there is none avaialbe. 586 """ 587 buffer = self._buffer 588 offset = self._offset 589 wd_to_path = self._wd_to_path 590 591 while True: 592 if offset >= self._size: 593 # Only whole records are ever returned by a read() on an inotify file descriptor. 594 # If the file descriptor is blocking a read() only blocks until the first event 595 # is available. 596 self._offset = offset = 0 597 try: 598 self._size = readinto(self._inotify_fd, buffer) 599 except BlockingIOError: 600 self._size = 0 601 return None 602 603 if self._size == 0: 604 return None 605 606 wd, mask, cookie, filename_len = _HEADER_STRUCT_unpack_from(buffer, offset) 607 offset += _HEADER_STRUCT_size 608 self._offset = filename_end_offset = offset + filename_len 609 610 if filename_len: 611 filename_bytes = buffer[offset:filename_end_offset] 612 index = filename_bytes.find(b'\0') 613 # I don't like the need for bytes() here, fsdecode() doesn't take a bytearray and 614 # you can't get a bytes slice in one step from a bytearray. 615 # I guess I could do this instead? 616 # filename = (filename_bytes[:index] if index >= 0 else filename_bytes).decode(sys.getfilesystemencoding(), errors='surrogateescape') 617 filename = fsdecode(bytes(filename_bytes[:index] if index >= 0 else filename_bytes)) 618 else: 619 filename = None 620 621 watch_path = wd_to_path.get(wd) 622 623 if mask & IN_IGNORED and watch_path is not None: 624 wd_to_path.pop(wd, None) 625 self._path_to_wd.pop(watch_path, None) 626 627 offset = filename_end_offset 628 629 if watch_path is None: 630 _logger.debug('Got inotify event for unknown watch handle: %d, mask: %d, cookie: %d', wd, mask, cookie) 631 continue 632 633 return InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename) 634 635 def read_events(self, terminal_events: int = IN_Q_OVERFLOW | IN_UNMOUNT) -> list[InotifyEvent]: 636 """ 637 Read available events. Might return an empty list if there are none 638 available. 639 640 `terminal_events` is per default: `IN_Q_OVERFLOW` | `IN_UNMOUNT` 641 642 **NOTE:** Don't use this in blocking mode! It will never return. 643 644 Raises `TerminalEventException` if any of the flags in `terminal_events` 645 are set in an event `mask`. 646 """ 647 events: list[InotifyEvent] = [] 648 649 while event := self.read_event(): 650 if event.mask & terminal_events: 651 raise TerminalEventException(event.wd, event.mask, event.watch_path, event.filename) 652 653 events.append(event) 654 655 return events 656 657 def __iter__(self) -> Self: 658 """ 659 `Inotify` can act as an iterator over the available events. 660 """ 661 return self 662 663 def __next__(self) -> InotifyEvent: 664 """ 665 `Inotify` can act as an iterator over the available events. 666 """ 667 event = self.read_event() 668 if event is None: 669 raise StopIteration 670 return event
Listen for inotify events.
Supports the context manager and iterator protocols.
397 def __init__(self, flags: int = IN_CLOEXEC, buffer_size: int = 4096) -> None: 398 """ 399 `flags` is a bit set of these values: 400 - `IN_CLOEXEC` - Close inotify file descriptor on exec. 401 - `IN_NONBLOCK` - Open inotify file descriptor as non-blocking. 402 403 It's recommended to pass the `IN_CLOEXEC` flag (default) to close the 404 file descriptor on [exec*(2)](https://linux.die.net/man/3/execl). 405 406 `buffer_size` is the size of the used input buffer and needs to be at 407 least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`). 408 409 This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 410 and thus might raise an `OSError` with one of these `errno` values: 411 - `EINVAL` 412 - `EMFILE` 413 - `ENOMEM` 414 - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 415 """ 416 self._inotify_fd = -1 417 self._offset = 0 418 self._size = 0 419 self._path_to_wd = {} 420 self._wd_to_path = {} 421 422 if buffer_size < _EVENT_SIZE_MAX: 423 raise ValueError(f'buffer_size too small: {buffer_size} < {_EVENT_SIZE_MAX}') 424 425 self._buffer = bytearray(buffer_size) 426 self._inotify_fd = inotify_init1(flags)
flags is a bit set of these values:
IN_CLOEXEC- Close inotify file descriptor on exec.IN_NONBLOCK- Open inotify file descriptor as non-blocking.
It's recommended to pass the IN_CLOEXEC flag (default) to close the
file descriptor on exec*(2).
buffer_size is the size of the used input buffer and needs to be at
least 272 (sizeof(struct inotify_event) + NAME_MAX + 1).
This calls inotify_init1(2)
and thus might raise an OSError with one of these errno values:
EINVALEMFILEENOMEMENOSYSif your libc doesn't support inotify_init1(2)
428 def fileno(self) -> int: 429 """ 430 The inotify file descriptor. 431 432 You can use this to call `poll()` or similar yourself 433 instead of using `PollInotify.wait()`. 434 """ 435 return self._inotify_fd
The inotify file descriptor.
You can use this to call poll() or similar yourself
instead of using PollInotify.wait().
437 @property 438 def closed(self) -> bool: 439 """ 440 `True` if the inotify file descriptor was closed. 441 """ 442 return self._inotify_fd == -1
True if the inotify file descriptor was closed.
444 def close(self) -> None: 445 """ 446 Close the inotify handle. 447 448 Can safely be called multiple times, but you 449 can't call any other methods once closed. 450 """ 451 try: 452 if self._inotify_fd != -1: 453 # A crash during inotify_init1() in __init__() means 454 # self._inotify_stream is not assigned, but self._inotify_fd 455 # is initialized with -1. 456 # Since this method is called in __del__() this state needs 457 # be handled. 458 os.close(self._inotify_fd) 459 finally: 460 self._inotify_fd = -1
Close the inotify handle.
Can safely be called multiple times, but you can't call any other methods once closed.
471 def add_watch(self, path: str, mask: int = IN_ALL_EVENTS) -> int: 472 """ 473 Add a watch path. 474 475 `mask` is a bit set of these event flags: 476 - `IN_ACCESS` - File was accessed. 477 - `IN_MODIFY` - File was modified. 478 - `IN_ATTRIB` - Metadata changed. 479 - `IN_CLOSE_WRITE` - Writtable file was closed. 480 - `IN_CLOSE_NOWRITE` - Unwrittable file closed. 481 - `IN_OPEN` - File was opened. 482 - `IN_MOVED_FROM` - File was moved from X. 483 - `IN_MOVED_TO` - File was moved to Y. 484 - `IN_CREATE` - Subfile was created. 485 - `IN_DELETE` - Subfile was deleted. 486 - `IN_DELETE_SELF` - Self was deleted. 487 - `IN_MOVE_SELF` - Self was moved. 488 489 And these additional flags: 490 - `IN_ONLYDIR` - Only watch the path if it is a directory. 491 - `IN_DONT_FOLLOW` - Don't follow symbolic links. 492 - `IN_EXCL_UNLINK` - Exclude events on unlinked objects. 493 - `IN_MASK_CREATE` - Only create watches. 494 - `IN_MASK_ADD` - Add to the mask of an already existing watch. 495 - `IN_ONESHOT` - Only send event once. 496 497 This calls [inotify_add_watch(2)](https://linux.die.net/man/2/inotify_add_watch) 498 and thus might raise one of these exceptions: 499 - `PermissionError` (`EACCES`) 500 - `FileExistsError` (`EEXISTS`) 501 - `FileNotFoundError` (`ENOENT`) 502 - `NotADirectoryError` (`ENOTDIR`) 503 - `OSError` (`WBADF`, `EFAULT`, `EINVAL`, `ENAMETOOLONG`, 504 `ENOMEM`, `ENOSPC`, `ENOSYS` if your libc doesn't support 505 `inotify_rm_watch()`) 506 """ 507 path_bytes = fsencode(path) 508 509 wd = inotify_add_watch(self._inotify_fd, path_bytes, mask) 510 _check_return(wd, path) 511 512 self._path_to_wd[path] = wd 513 self._wd_to_path[wd] = path 514 515 return wd
Add a watch path.
mask is a bit set of these event flags:
IN_ACCESS- File was accessed.IN_MODIFY- File was modified.IN_ATTRIB- Metadata changed.IN_CLOSE_WRITE- Writtable file was closed.IN_CLOSE_NOWRITE- Unwrittable file closed.IN_OPEN- File was opened.IN_MOVED_FROM- File was moved from X.IN_MOVED_TO- File was moved to Y.IN_CREATE- Subfile was created.IN_DELETE- Subfile was deleted.IN_DELETE_SELF- Self was deleted.IN_MOVE_SELF- Self was moved.
And these additional flags:
IN_ONLYDIR- Only watch the path if it is a directory.IN_DONT_FOLLOW- Don't follow symbolic links.IN_EXCL_UNLINK- Exclude events on unlinked objects.IN_MASK_CREATE- Only create watches.IN_MASK_ADD- Add to the mask of an already existing watch.IN_ONESHOT- Only send event once.
This calls inotify_add_watch(2) and thus might raise one of these exceptions:
PermissionError(EACCES)FileExistsError(EEXISTS)FileNotFoundError(ENOENT)NotADirectoryError(ENOTDIR)OSError(WBADF,EFAULT,EINVAL,ENAMETOOLONG,ENOMEM,ENOSPC,ENOSYSif your libc doesn't supportinotify_rm_watch())
517 def remove_watch(self, path: str) -> None: 518 """ 519 Remove watch by path. 520 521 Does nothing if the path is not watched. 522 523 This calls [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch) 524 and this might raise an `OSError` with one of these `errno` values: 525 - `EBADF` 526 - `EINVAL` 527 - `ENOSYS` if your libc doesn't support [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch) 528 """ 529 wd = self._path_to_wd.get(path) 530 if wd is None: 531 _logger.debug('%s: Path is not watched', path) 532 return 533 534 try: 535 res = inotify_rm_watch(self._inotify_fd, wd) 536 _check_return(res, path) 537 finally: 538 del self._path_to_wd[path] 539 del self._wd_to_path[wd]
Remove watch by path.
Does nothing if the path is not watched.
This calls inotify_rm_watch(2)
and this might raise an OSError with one of these errno values:
EBADFEINVALENOSYSif your libc doesn't support inotify_rm_watch(2)
541 def remove_watch_with_id(self, wd: int) -> None: 542 """ 543 Remove watch by handle. 544 545 Does nothing if the handle is invalid. 546 547 This calls `inotify_rm_watch()` and this might raise an 548 `OSError` with one of these `errno` values: 549 - `EBADF` 550 - `EINVAL` 551 - `ENOSYS` if your libc doesn't support `inotify_rm_watch()` 552 """ 553 path = self._wd_to_path.get(wd) 554 if path is None: 555 _logger.debug('%d: Invalid handle', wd) 556 return 557 558 try: 559 res = inotify_rm_watch(self._inotify_fd, wd) 560 _check_return(res, path) 561 finally: 562 del self._wd_to_path[wd] 563 del self._path_to_wd[path]
Remove watch by handle.
Does nothing if the handle is invalid.
This calls inotify_rm_watch() and this might raise an
OSError with one of these errno values:
EBADFEINVALENOSYSif your libc doesn't supportinotify_rm_watch()
565 def watch_paths(self) -> set[str]: 566 """ 567 Get the set of the watched paths. 568 """ 569 return set(self._path_to_wd)
Get the set of the watched paths.
571 def get_watch_id(self, path: str) -> Optional[int]: 572 """ 573 Get the watch id to a path, if the path is watched. 574 """ 575 return self._path_to_wd.get(path)
Get the watch id to a path, if the path is watched.
577 def get_watch_path(self, wd: int) -> Optional[str]: 578 """ 579 Get the path to a watch id, if the watch id is valid. 580 """ 581 return self._wd_to_path.get(wd)
Get the path to a watch id, if the watch id is valid.
583 def read_event(self) -> Optional[InotifyEvent]: 584 """ 585 Read a single event. Might return `None` if there is none avaialbe. 586 """ 587 buffer = self._buffer 588 offset = self._offset 589 wd_to_path = self._wd_to_path 590 591 while True: 592 if offset >= self._size: 593 # Only whole records are ever returned by a read() on an inotify file descriptor. 594 # If the file descriptor is blocking a read() only blocks until the first event 595 # is available. 596 self._offset = offset = 0 597 try: 598 self._size = readinto(self._inotify_fd, buffer) 599 except BlockingIOError: 600 self._size = 0 601 return None 602 603 if self._size == 0: 604 return None 605 606 wd, mask, cookie, filename_len = _HEADER_STRUCT_unpack_from(buffer, offset) 607 offset += _HEADER_STRUCT_size 608 self._offset = filename_end_offset = offset + filename_len 609 610 if filename_len: 611 filename_bytes = buffer[offset:filename_end_offset] 612 index = filename_bytes.find(b'\0') 613 # I don't like the need for bytes() here, fsdecode() doesn't take a bytearray and 614 # you can't get a bytes slice in one step from a bytearray. 615 # I guess I could do this instead? 616 # filename = (filename_bytes[:index] if index >= 0 else filename_bytes).decode(sys.getfilesystemencoding(), errors='surrogateescape') 617 filename = fsdecode(bytes(filename_bytes[:index] if index >= 0 else filename_bytes)) 618 else: 619 filename = None 620 621 watch_path = wd_to_path.get(wd) 622 623 if mask & IN_IGNORED and watch_path is not None: 624 wd_to_path.pop(wd, None) 625 self._path_to_wd.pop(watch_path, None) 626 627 offset = filename_end_offset 628 629 if watch_path is None: 630 _logger.debug('Got inotify event for unknown watch handle: %d, mask: %d, cookie: %d', wd, mask, cookie) 631 continue 632 633 return InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename)
Read a single event. Might return None if there is none avaialbe.
635 def read_events(self, terminal_events: int = IN_Q_OVERFLOW | IN_UNMOUNT) -> list[InotifyEvent]: 636 """ 637 Read available events. Might return an empty list if there are none 638 available. 639 640 `terminal_events` is per default: `IN_Q_OVERFLOW` | `IN_UNMOUNT` 641 642 **NOTE:** Don't use this in blocking mode! It will never return. 643 644 Raises `TerminalEventException` if any of the flags in `terminal_events` 645 are set in an event `mask`. 646 """ 647 events: list[InotifyEvent] = [] 648 649 while event := self.read_event(): 650 if event.mask & terminal_events: 651 raise TerminalEventException(event.wd, event.mask, event.watch_path, event.filename) 652 653 events.append(event) 654 655 return events
Read available events. Might return an empty list if there are none available.
terminal_events is per default: IN_Q_OVERFLOW | IN_UNMOUNT
NOTE: Don't use this in blocking mode! It will never return.
Raises TerminalEventException if any of the flags in terminal_events
are set in an event mask.
672class PollInotify(Inotify): 673 """ 674 Listen for inotify events. 675 676 In addition to the functionality of `Inotify` this class adds a `wait()` 677 method that waits for events using `epoll`. If you use `Inotify` you 678 need/can to do that yourself. 679 680 Supports the context manager and iterator protocols. 681 """ 682 __slots__ = ( 683 '_epoll', 684 '_stopfd', 685 ) 686 687 _epoll: select.epoll 688 _stopfd: Optional[int] 689 690 def __init__(self, stopfd: Optional[int] = None, buffer_size: int = 4096) -> None: 691 """ 692 If not `None` then `stopfd` is a file descriptor that will 693 be added to the `poll()` call in `PollInotify.wait()`. 694 695 `buffer_size` is the size of the used input buffer and needs to be at 696 least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`). 697 698 This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 699 and thus might raise an `OSError` with one of these `errno` values: 700 - `EINVAL` (shouldn't happen) 701 - `EMFILE` 702 - `ENOMEM` 703 - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 704 """ 705 super().__init__(IN_NONBLOCK | IN_CLOEXEC, buffer_size) 706 self._stopfd = stopfd 707 self._epoll = select.epoll(1 if stopfd is None else 2) 708 self._epoll.register(self._inotify_fd, select.POLLIN) 709 710 if stopfd is not None: 711 self._epoll.register(stopfd, select.POLLIN) 712 713 @property 714 def stopfd(self) -> Optional[int]: 715 """ 716 The `stopfd` parameter of `__init__()`, used in `wait()`. 717 """ 718 return self._stopfd 719 720 def close(self) -> None: 721 """ 722 Close the inotify and epoll handles. 723 724 Can safely be called multiple times, but you 725 can't call any other methods once closed. 726 """ 727 try: 728 super().close() 729 finally: 730 epoll = self._epoll 731 if not epoll.closed: 732 epoll.close() 733 734 def wait(self, timeout: Optional[float] = None) -> bool: 735 """ 736 Wait for inotify events or for any `POLLIN` on `stopfd`, 737 if that is not `None`. If `stopfd` signals this function will 738 return `False`, otherwise `True`. 739 740 Raises `TimeoutError` if `timeout` is not `None` and 741 the operation has expired. 742 743 This method uses using `select.epoll.poll()`, see there for 744 additional possible exceptions. 745 """ 746 events = self._epoll.poll(timeout) 747 748 if not events and timeout is not None and timeout >= 0.0: 749 raise TimeoutError 750 751 stopfd = self._stopfd 752 if stopfd is not None: 753 for fd, mask in events: 754 if fd == stopfd: 755 return False 756 757 return True
Listen for inotify events.
In addition to the functionality of Inotify this class adds a wait()
method that waits for events using epoll. If you use Inotify you
need/can to do that yourself.
Supports the context manager and iterator protocols.
690 def __init__(self, stopfd: Optional[int] = None, buffer_size: int = 4096) -> None: 691 """ 692 If not `None` then `stopfd` is a file descriptor that will 693 be added to the `poll()` call in `PollInotify.wait()`. 694 695 `buffer_size` is the size of the used input buffer and needs to be at 696 least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`). 697 698 This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 699 and thus might raise an `OSError` with one of these `errno` values: 700 - `EINVAL` (shouldn't happen) 701 - `EMFILE` 702 - `ENOMEM` 703 - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1) 704 """ 705 super().__init__(IN_NONBLOCK | IN_CLOEXEC, buffer_size) 706 self._stopfd = stopfd 707 self._epoll = select.epoll(1 if stopfd is None else 2) 708 self._epoll.register(self._inotify_fd, select.POLLIN) 709 710 if stopfd is not None: 711 self._epoll.register(stopfd, select.POLLIN)
If not None then stopfd is a file descriptor that will
be added to the poll() call in PollInotify.wait().
buffer_size is the size of the used input buffer and needs to be at
least 272 (sizeof(struct inotify_event) + NAME_MAX + 1).
This calls inotify_init1(2)
and thus might raise an OSError with one of these errno values:
EINVAL(shouldn't happen)EMFILEENOMEMENOSYSif your libc doesn't support inotify_init1(2)
713 @property 714 def stopfd(self) -> Optional[int]: 715 """ 716 The `stopfd` parameter of `__init__()`, used in `wait()`. 717 """ 718 return self._stopfd
The stopfd parameter of __init__(), used in wait().
720 def close(self) -> None: 721 """ 722 Close the inotify and epoll handles. 723 724 Can safely be called multiple times, but you 725 can't call any other methods once closed. 726 """ 727 try: 728 super().close() 729 finally: 730 epoll = self._epoll 731 if not epoll.closed: 732 epoll.close()
Close the inotify and epoll handles.
Can safely be called multiple times, but you can't call any other methods once closed.
734 def wait(self, timeout: Optional[float] = None) -> bool: 735 """ 736 Wait for inotify events or for any `POLLIN` on `stopfd`, 737 if that is not `None`. If `stopfd` signals this function will 738 return `False`, otherwise `True`. 739 740 Raises `TimeoutError` if `timeout` is not `None` and 741 the operation has expired. 742 743 This method uses using `select.epoll.poll()`, see there for 744 additional possible exceptions. 745 """ 746 events = self._epoll.poll(timeout) 747 748 if not events and timeout is not None and timeout >= 0.0: 749 raise TimeoutError 750 751 stopfd = self._stopfd 752 if stopfd is not None: 753 for fd, mask in events: 754 if fd == stopfd: 755 return False 756 757 return True
Wait for inotify events or for any POLLIN on stopfd,
if that is not None. If stopfd signals this function will
return False, otherwise True.
Raises TimeoutError if timeout is not None and
the operation has expired.
This method uses using select.epoll.poll(), see there for
additional possible exceptions.
298class TerminalEventException(Exception): 299 """ 300 Exception raised by `Inotify.read_events()` when an event mask contains one 301 of the specified `terminal_events`. 302 """ 303 304 __slots__ = ( 305 'wd', 306 'mask', 307 'watch_path', 308 'filename', 309 ) 310 311 wd: int; "Inotify watch descriptor." 312 mask: int; "Bitset of the events that occured." 313 watch_path: Optional[str]; "Path of the watched file, `None` if the Python code doesn't know about the watch." 314 filename: Optional[str]; "If the event is about the child of a watched directory, this is the name of that file, otherwise `None`." 315 316 def __init__(self, wd: int, mask: int, watch_path: Optional[str], filename: Optional[str]) -> None: 317 super().__init__(wd, mask, watch_path, filename) 318 self.wd = wd 319 self.mask = mask 320 self.watch_path = watch_path 321 self.filename = filename
Exception raised by Inotify.read_events() when an event mask contains one
of the specified terminal_events.
196def get_inotify_event_names(mask: int) -> list[str]: 197 """ 198 Get a list of event names from an event mask as returned by inotify. 199 200 If there is a flag set that isn't a know name the hexadecimal representation 201 of that flag is also returned. 202 """ 203 names: list[str] = [] 204 for code, name in INOTIFY_MASK_CODES.items(): 205 if mask & code: 206 names.append(name) 207 mask &= ~code 208 if not mask: 209 break 210 211 if mask: 212 for bit in range(0, 32): 213 if bit & mask: 214 names.append('0x%x' % bit) 215 mask &= ~bit 216 if not mask: 217 break 218 219 return names
Get a list of event names from an event mask as returned by inotify.
If there is a flag set that isn't a know name the hexadecimal representation of that flag is also returned.
True if your libc exports inotify_init1, inotify_add_watch, and inotify_rm_watch, otherwise False.
Close inotify file descriptor on exec.
See Also: Inotify.__init__()
Open inotify file descriptor as non-blocking.
See Also: Inotify.__init__()
File was accessed.
See Also: Inotify.add_watch(), InotifyEvent.mask
File was modified.
See Also: Inotify.add_watch(), InotifyEvent.mask
Metadata changed.
See Also: Inotify.add_watch(), InotifyEvent.mask
Writtable file was closed.
See Also: Inotify.add_watch(), InotifyEvent.mask
Unwrittable file closed.
See Also: Inotify.add_watch(), InotifyEvent.mask
File was opened.
See Also: Inotify.add_watch(), InotifyEvent.mask
File was moved from X.
See Also: Inotify.add_watch(), InotifyEvent.mask
File was moved to Y.
See Also: Inotify.add_watch(), InotifyEvent.mask
Subfile was created.
See Also: Inotify.add_watch(), InotifyEvent.mask
Subfile was deleted.
See Also: Inotify.add_watch(), InotifyEvent.mask
Self was deleted.
See Also: Inotify.add_watch(), InotifyEvent.mask
Self was moved.
See Also: Inotify.add_watch(), InotifyEvent.mask
Backing file system was unmounted.
See Also: InotifyEvent.mask
Event queued overflowed.
See Also: InotifyEvent.mask
File was ignored.
See Also: InotifyEvent.mask
All close events.
See Also: Inotify.add_watch(), InotifyEvent.mask
All move events.
See Also: Inotify.add_watch(), InotifyEvent.mask
Only watch the path if it is a directory.
See Also: Inotify.add_watch()
Don't follow symbolic links.
See Also: Inotify.add_watch()
Exclude events on unlinked objects.
See Also: Inotify.add_watch()
Only create watches.
See Also: Inotify.add_watch()
Add to the mask of an already existing watch.
See Also: Inotify.add_watch()
Event occurred against directory.
See Also: InotifyEvent.mask
Only send event once.
See Also: Inotify.add_watch()
All of the events.
See Also: Inotify.add_watch(), InotifyEvent.mask
Mapping from inotify event mask flag to it's name.
See Also: InotifyEvent.mask