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