Skip to content

Commit

Permalink
[mac] Improve handling of rename events (#750)
Browse files Browse the repository at this point in the history
* drop support for macOS 10.12 and lower

* record inodes for all events

* use inode to match rename events

* add flush method

* update tests to new NativeEvent

* simplify handling of created and removed events

* added test for case-change

* clean up loop over native events

Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr>

* added changelog entries

* remove unneeded Py_INCREF calls

* Revert "simplify handling of created and removed events"

This reverts commit 75afbd2.

* allow inode to be None

this is the case for kFSEventStreamEventFlagRootChanged

* join event_dispatcher in `test_event_dispatcher`

* fix memory management for inode

* better logs for macOS tests

* show flags in log output

* fix returning inode attribute

* tweak error message for unsupported macOS

Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr>

* emit debug instead of info logs from `queue_events`

Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr>
  • Loading branch information
SamSchott and BoboTiG committed Jan 21, 2021
1 parent fc1242e commit d9a6e63
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 89 deletions.
2 changes: 2 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Changelog
- Avoid deprecated ``PyEval_InitThreads`` on Python 3.7+ (`#746 <https://github.com/gorakhargosh/watchdog/pull/746>`_)
- [inotify] Add support for ``IN_CLOSE_WRITE`` events. A ``FileCloseEvent`` event will be fired. Note that ``IN_CLOSE_NOWRITE`` events are not handled to prevent much noise. (`#184 <https://github.com/gorakhargosh/watchdog/pull/184>`_, `#245 <https://github.com/gorakhargosh/watchdog/pull/245>`_, `#280 <https://github.com/gorakhargosh/watchdog/pull/280>`_, `#313 <https://github.com/gorakhargosh/watchdog/pull/313>`_, `#690 <https://github.com/gorakhargosh/watchdog/pull/690>`_)
- [mac] Support coalesced filesystem events (`#734 <https://github.com/gorakhargosh/watchdog/pull/734>`_)
- [mac] Drop support for OSX 10.12 and earlier (`#750 <https://github.com/gorakhargosh/watchdog/pull/750>`_)
- [mac] Fix an issue when renaming an item changes only the casing (`#750 <https://github.com/gorakhargosh/watchdog/pull/750>`_)
- Thanks to our beloved contributors: @bstaletic, @lukassup, @ysard, @SamSchott, @CCP-Aporia


Expand Down
76 changes: 27 additions & 49 deletions src/watchdog/observers/fsevents.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,38 +84,39 @@ def queue_event(self, event):
EventEmitter.queue_event(self, event)

def queue_events(self, timeout, events):
i = 0
while i < len(events):
event = events[i]
logger.info(event)

if logger.getEffectiveLevel() <= logging.DEBUG:
for event in events:
flags = ", ".join(attr for attr in dir(event) if getattr(event, attr) is True)
logger.debug(f"{event}: {flags}")

while events:
event = events.pop(0)
src_path = self._encode_path(event.path)

if event.is_renamed:
# Internal moves appears to always be consecutive in the same
# buffer and have IDs differ by exactly one (while others
# don't) making it possible to pair up the two events coming
# from a singe move operation. (None of this is documented!)
# Otherwise, guess whether file was moved in or out.
# TODO: handle id wrapping
if (i + 1 < len(events) and events[i + 1].is_renamed
and events[i + 1].event_id == event.event_id + 1):
logger.info("Next event for rename is %s", events[i + 1])
dest_event = next(iter(e for e in events if e.is_renamed and e.inode == event.inode), None)
if dest_event:
# item was moved within the watched folder
events.remove(dest_event)
logger.debug("Destination event for rename is %s", dest_event)
cls = DirMovedEvent if event.is_directory else FileMovedEvent
dst_path = self._encode_path(events[i + 1].path)
dst_path = self._encode_path(dest_event.path)
self.queue_event(cls(src_path, dst_path))
self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
self.queue_event(DirModifiedEvent(os.path.dirname(dst_path)))
for sub_event in generate_sub_moved_events(src_path, dst_path):
logger.info("Generated sub event: %s", sub_event)
logger.debug("Generated sub event: %s", sub_event)
self.queue_event(sub_event)
i += 1
elif os.path.exists(event.path):
# item was moved into the watched folder
cls = DirCreatedEvent if event.is_directory else FileCreatedEvent
self.queue_event(cls(src_path))
self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
for sub_event in generate_sub_created_events(src_path):
self.queue_event(sub_event)
else:
# item was moved out of the watched folder
cls = DirDeletedEvent if event.is_directory else FileDeletedEvent
self.queue_event(cls(src_path))
self.queue_event(DirModifiedEvent(os.path.dirname(src_path)))
Expand Down Expand Up @@ -148,56 +149,33 @@ def queue_events(self, timeout, events):
if src_path == self.watch.path:
# this should not really occur, instead we expect
# is_root_changed to be set
logger.info("Stopping because root path was removed")
logger.debug("Stopping because root path was removed")
self.stop()

if event.is_root_changed:
# This will be set if root or any of its parents is renamed or
# deleted.
# TODO: find out new path and generate DirMovedEvent?
self.queue_event(DirDeletedEvent(self.watch.path))
logger.info("Stopping because root path was changed")
logger.debug("Stopping because root path was changed")
self.stop()

i += 1

def run(self):
try:
def callback(pathnames, flags, ids, emitter=self):
def callback(paths, inodes, flags, ids, emitter=self):
try:
with emitter._lock:
emitter.queue_events(emitter.timeout, [
_fsevents.NativeEvent(event_path, event_flags, event_id)
for event_path, event_flags, event_id in zip(pathnames, flags, ids)
])
events = [
_fsevents.NativeEvent(path, inode, event_flags, event_id)
for path, inode, event_flags, event_id in zip(paths, inodes, flags, ids)
]
emitter.queue_events(emitter.timeout, events)
except Exception:
logger.exception("Unhandled exception in fsevents callback")

# for pathname, flag in zip(pathnames, flags):
# if emitter.watch.is_recursive: # and pathname != emitter.watch.path:
# new_sub_snapshot = DirectorySnapshot(pathname, True)
# old_sub_snapshot = self.snapshot.copy(pathname)
# diff = new_sub_snapshot - old_sub_snapshot
# self.snapshot += new_subsnapshot
# else:
# new_snapshot = DirectorySnapshot(emitter.watch.path, False)
# diff = new_snapshot - emitter.snapshot
# emitter.snapshot = new_snapshot

# INFO: FSEvents reports directory notifications recursively
# by default, so we do not need to add subdirectory paths.
# pathnames = set([self.watch.path])
# if self.watch.is_recursive:
# for root, directory_names, _ in os.walk(self.watch.path):
# for directory_name in directory_names:
# full_path = absolute_path(
# os.path.join(root, directory_name))
# pathnames.add(full_path)
self.pathnames = [self.watch.path]
_fsevents.add_watch(self,
self.watch,
callback,
self.pathnames)

_fsevents.add_watch(self, self.watch, callback, self.pathnames)
_fsevents.read_events(self)
except Exception:
logger.exception("Unhandled exception in FSEventsEmitter")
Expand Down
Loading

0 comments on commit d9a6e63

Please sign in to comment.