1""":module: watchdog.observers.inotify 2:synopsis: ``inotify(7)`` based emitter implementation. 3:author: Sebastien Martini <[email protected]> 4:author: Luke McCarthy <[email protected]> 5:author: [email protected] (Yesudeep Mangalapilly) 6:author: Tim Cuthbertson <[email protected]> 7:author: [email protected] (Mickaël Schoentgen) 8:platforms: Linux 2.6.13+. 9 10.. ADMONITION:: About system requirements 11 12 Recommended minimum kernel version: 2.6.25. 13 14 Quote from the inotify(7) man page: 15 16 "Inotify was merged into the 2.6.13 Linux kernel. The required library 17 interfaces were added to glibc in version 2.4. (IN_DONT_FOLLOW, 18 IN_MASK_ADD, and IN_ONLYDIR were only added in version 2.5.)" 19 20 Therefore, you must ensure the system is running at least these versions 21 appropriate libraries and the kernel. 22 23.. ADMONITION:: About recursiveness, event order, and event coalescing 24 25 Quote from the inotify(7) man page: 26 27 If successive output inotify events produced on the inotify file 28 descriptor are identical (same wd, mask, cookie, and name) then they 29 are coalesced into a single event if the older event has not yet been 30 read (but see BUGS). 31 32 The events returned by reading from an inotify file descriptor form 33 an ordered queue. Thus, for example, it is guaranteed that when 34 renaming from one directory to another, events will be produced in 35 the correct order on the inotify file descriptor. 36 37 ... 38 39 Inotify monitoring of directories is not recursive: to monitor 40 subdirectories under a directory, additional watches must be created. 41 42 This emitter implementation therefore automatically adds watches for 43 sub-directories if running in recursive mode. 44 45Some extremely useful articles and documentation: 46 47.. _inotify FAQ: http://inotify.aiken.cz/?section=inotify&page=faq&lang=en 48.. _intro to inotify: http://www.linuxjournal.com/article/8478 49 50""" 51 52from __future__ import annotations 53 54import logging 55import os 56import threading 57from typing import TYPE_CHECKING 58 59from watchdog.events import ( 60 DirCreatedEvent, 61 DirDeletedEvent, 62 DirModifiedEvent, 63 DirMovedEvent, 64 FileClosedEvent, 65 FileClosedNoWriteEvent, 66 FileCreatedEvent, 67 FileDeletedEvent, 68 FileModifiedEvent, 69 FileMovedEvent, 70 FileOpenedEvent, 71 FileSystemEvent, 72 generate_sub_created_events, 73 generate_sub_moved_events, 74) 75from watchdog.observers.api import DEFAULT_EMITTER_TIMEOUT, DEFAULT_OBSERVER_TIMEOUT, BaseObserver, EventEmitter 76from watchdog.observers.inotify_buffer import InotifyBuffer 77from watchdog.observers.inotify_c import InotifyConstants 78 79if TYPE_CHECKING: 80 from watchdog.observers.api import EventQueue, ObservedWatch 81 82logger = logging.getLogger(__name__) 83 84 85class InotifyEmitter(EventEmitter): 86 """inotify(7)-based event emitter. 87 88 :param event_queue: 89 The event queue to fill with events. 90 :param watch: 91 A watch object representing the directory to monitor. 92 :type watch: 93 :class:`watchdog.observers.api.ObservedWatch` 94 :param timeout: 95 Read events blocking timeout (in seconds). 96 :type timeout: 97 ``float`` 98 :param event_filter: 99 Collection of event types to emit, or None for no filtering (default). 100 :type event_filter: 101 Iterable[:class:`watchdog.events.FileSystemEvent`] | None 102 """ 103 104 def __init__( 105 self, 106 event_queue: EventQueue, 107 watch: ObservedWatch, 108 *, 109 timeout: float = DEFAULT_EMITTER_TIMEOUT, 110 event_filter: list[type[FileSystemEvent]] | None = None, 111 ) -> None: 112 super().__init__(event_queue, watch, timeout=timeout, event_filter=event_filter) 113 self._lock = threading.Lock() 114 self._inotify: InotifyBuffer | None = None 115 116 def on_thread_start(self) -> None: 117 path = os.fsencode(self.watch.path) 118 event_mask = self.get_event_mask_from_filter() 119 self._inotify = InotifyBuffer(path, recursive=self.watch.is_recursive, event_mask=event_mask) 120 121 def on_thread_stop(self) -> None: 122 if self._inotify: 123 self._inotify.close() 124 self._inotify = None 125 126 def queue_events(self, timeout: float, *, full_events: bool = False) -> None: 127 # If "full_events" is true, then the method will report unmatched move events as separate events 128 # This behavior is by default only called by a InotifyFullEmitter 129 if self._inotify is None: 130 logger.error("InotifyEmitter.queue_events() called when the thread is inactive") 131 return 132 with self._lock: 133 if self._inotify is None: 134 logger.error("InotifyEmitter.queue_events() called when the thread is inactive") 135 return 136 event = self._inotify.read_event() 137 if event is None: 138 return 139 140 cls: type[FileSystemEvent] 141 if isinstance(event, tuple): 142 move_from, move_to = event 143 src_path = self._decode_path(move_from.src_path) 144 dest_path = self._decode_path(move_to.src_path) 145 cls = DirMovedEvent if move_from.is_directory else FileMovedEvent 146 self.queue_event(cls(src_path, dest_path)) 147 self.queue_event(DirModifiedEvent(os.path.dirname(src_path))) 148 self.queue_event(DirModifiedEvent(os.path.dirname(dest_path))) 149 if move_from.is_directory and self.watch.is_recursive: 150 for sub_moved_event in generate_sub_moved_events(src_path, dest_path): 151 self.queue_event(sub_moved_event) 152 return 153 154 src_path = self._decode_path(event.src_path) 155 if event.is_moved_to: 156 if full_events: 157 cls = DirMovedEvent if event.is_directory else FileMovedEvent 158 self.queue_event(cls("", src_path)) 159 else: 160 cls = DirCreatedEvent if event.is_directory else FileCreatedEvent 161 self.queue_event(cls(src_path)) 162 self.queue_event(DirModifiedEvent(os.path.dirname(src_path))) 163 if event.is_directory and self.watch.is_recursive: 164 for sub_created_event in generate_sub_created_events(src_path): 165 self.queue_event(sub_created_event) 166 elif event.is_attrib or event.is_modify: 167 cls = DirModifiedEvent if event.is_directory else FileModifiedEvent 168 self.queue_event(cls(src_path)) 169 elif event.is_delete or (event.is_moved_from and not full_events): 170 cls = DirDeletedEvent if event.is_directory else FileDeletedEvent 171 self.queue_event(cls(src_path)) 172 self.queue_event(DirModifiedEvent(os.path.dirname(src_path))) 173 elif event.is_moved_from and full_events: 174 cls = DirMovedEvent if event.is_directory else FileMovedEvent 175 self.queue_event(cls(src_path, "")) 176 self.queue_event(DirModifiedEvent(os.path.dirname(src_path))) 177 elif event.is_create: 178 cls = DirCreatedEvent if event.is_directory else FileCreatedEvent 179 self.queue_event(cls(src_path)) 180 self.queue_event(DirModifiedEvent(os.path.dirname(src_path))) 181 elif event.is_delete_self and src_path == self.watch.path: 182 cls = DirDeletedEvent if event.is_directory else FileDeletedEvent 183 self.queue_event(cls(src_path)) 184 self.stop() 185 elif not event.is_directory: 186 if event.is_open: 187 cls = FileOpenedEvent 188 self.queue_event(cls(src_path)) 189 elif event.is_close_write: 190 cls = FileClosedEvent 191 self.queue_event(cls(src_path)) 192 self.queue_event(DirModifiedEvent(os.path.dirname(src_path))) 193 elif event.is_close_nowrite: 194 cls = FileClosedNoWriteEvent 195 self.queue_event(cls(src_path)) 196 197 def _decode_path(self, path: bytes | str) -> bytes | str: 198 """Decode path only if unicode string was passed to this emitter.""" 199 return path if isinstance(self.watch.path, bytes) else os.fsdecode(path) 200 201 def get_event_mask_from_filter(self) -> int | None: 202 """Optimization: Only include events we are filtering in inotify call.""" 203 if self._event_filter is None: 204 return None 205 206 # Always listen to delete self 207 event_mask = InotifyConstants.IN_DELETE_SELF 208 209 for cls in self._event_filter: 210 if cls in {DirMovedEvent, FileMovedEvent}: 211 event_mask |= InotifyConstants.IN_MOVE 212 elif cls in {DirCreatedEvent, FileCreatedEvent}: 213 event_mask |= InotifyConstants.IN_MOVE | InotifyConstants.IN_CREATE 214 elif cls is DirModifiedEvent: 215 event_mask |= ( 216 InotifyConstants.IN_MOVE 217 | InotifyConstants.IN_ATTRIB 218 | InotifyConstants.IN_MODIFY 219 | InotifyConstants.IN_CREATE 220 | InotifyConstants.IN_CLOSE_WRITE 221 ) 222 elif cls is FileModifiedEvent: 223 event_mask |= InotifyConstants.IN_ATTRIB | InotifyConstants.IN_MODIFY 224 elif cls in {DirDeletedEvent, FileDeletedEvent}: 225 event_mask |= InotifyConstants.IN_DELETE 226 elif cls is FileClosedEvent: 227 event_mask |= InotifyConstants.IN_CLOSE_WRITE 228 elif cls is FileClosedNoWriteEvent: 229 event_mask |= InotifyConstants.IN_CLOSE_NOWRITE 230 elif cls is FileOpenedEvent: 231 event_mask |= InotifyConstants.IN_OPEN 232 233 return event_mask 234 235 236class InotifyFullEmitter(InotifyEmitter): 237 """inotify(7)-based event emitter. By default this class produces move events even if they are not matched 238 Such move events will have a ``None`` value for the unmatched part. 239 """ 240 241 def queue_events(self, timeout: float, *, events: bool = True) -> None: # type: ignore[override] 242 super().queue_events(timeout, full_events=events) 243 244 245class InotifyObserver(BaseObserver): 246 """Observer thread that schedules watching directories and dispatches 247 calls to event handlers. 248 """ 249 250 def __init__(self, *, timeout: float = DEFAULT_OBSERVER_TIMEOUT, generate_full_events: bool = False) -> None: 251 cls = InotifyFullEmitter if generate_full_events else InotifyEmitter 252 super().__init__(cls, timeout=timeout) 253