diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py index bb2b1aa67..9853ca2cb 100644 --- a/src/watchdog/observers/inotify_c.py +++ b/src/watchdog/observers/inotify_c.py @@ -1,5 +1,6 @@ from __future__ import annotations +import time import contextlib import ctypes import ctypes.util @@ -167,7 +168,12 @@ def __init__(self, path: bytes, *, recursive: bool = False, event_mask: int | No self._add_dir_watch(path, event_mask, recursive=recursive) else: self._add_watch(path, event_mask) - self._moved_from_events: dict[int, InotifyEvent] = {} + + self._moved_from_events: dict[int, EventPathAndTime] = {} + self.stop_event = threading.Event() + self.cleaner_thread = threading.Thread(target=self.clear_move_records_older_than, args=(5,self.stop_event)) + self.cleaner_thread.start() + @property def event_mask(self) -> int: @@ -189,9 +195,22 @@ def fd(self) -> int: """The file descriptor associated with the inotify instance.""" return self._inotify_fd - def clear_move_records(self) -> None: - """Clear cached records of MOVED_FROM events""" - self._moved_from_events = {} + def clear_move_records_older_than(self, delay_sec: int, stop_event: threading.Event) -> None: + """Clears cached records of MOVED_FROM events older than delay_sec.""" + sleep_max = 10 + while not stop_event.is_set(): + if len(self._moved_from_events) != 0: + item =next(iter(self._moved_from_events)) + if self._moved_from_events[item].time < (time.time() - delay_sec): + del self._moved_from_events[item] + else: + time.sleep(delay_sec) + else: + time.sleep(sleep_max) + + def stop_cleaner(self): + self.stop_event.set() + self.cleaner_thread.join() def source_for_move(self, destination_event: InotifyEvent) -> bytes | None: """The source path corresponding to the given MOVED_TO event. @@ -208,8 +227,9 @@ def remember_move_from_event(self, event: InotifyEvent) -> None: """Save this event as the source event for future MOVED_TO events to reference. """ - self._moved_from_events[event.cookie] = event - + path_and_time = EventPathAndTime(event.src_path) + self._moved_from_events[event.cookie] = path_and_time + def add_watch(self, path: bytes) -> None: """Adds a watch for the given path. @@ -236,6 +256,7 @@ def close(self) -> None: with self._lock: if not self._closed: self._closed = True + self.stop_cleaner() if self._path in self._wd_for_path: wd = self._wd_for_path[self._path] @@ -586,3 +607,15 @@ def __repr__(self) -> str: f" mask={self._get_mask_string(self.mask)}, cookie={self.cookie}," f" name={os.fsdecode(self.name)!r}>" ) + +class EventPathAndTime: + def __init__(self, src_path: bytes) -> None: + self._time = time.time() + self._src_path = src_path + + @property + def src_path(self) -> bytes: + return self._src_path + @property + def time(self) -> float: + return self._time \ No newline at end of file diff --git a/tests/test_inotify_c.py b/tests/test_inotify_c.py index 8d4b59d40..2bc29de48 100644 --- a/tests/test_inotify_c.py +++ b/tests/test_inotify_c.py @@ -157,6 +157,18 @@ def test_watch_file(p: P, event_queue: TestEventQueue, start_watching: StartWatc event, _ = event_queue.get(timeout=5) assert repr(event) +def test_watch_file_move(p: P, event_queue: TestEventQueue, start_watching: StartWatching) -> None: + folder = p() + path = p("this_is_a_file") + path_moved = p("this_is_a_file2") + with open(path, "a"): + pass + start_watching(path=folder) + os.rename(path, path_moved) + event, _ = event_queue.get(timeout=5) + assert event.src_path == path + assert event.dest_path == path_moved + assert repr(event) def test_event_equality(p: P) -> None: wd_parent_dir = 42