Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes missing notification for deletion of watched dir on macOS #727

Merged
merged 12 commits into from Dec 16, 2020

Conversation

samschott
Copy link
Contributor

This PR fixes an issue which would prevent us from being notified when the watched directory is deleted. This is achieved by passing the kFSEventStreamCreateFlagWatchRoot to the watch.

@CCP-Aporia
Copy link
Contributor

CCP-Aporia commented Dec 10, 2020

First of all thanks for this PR! However, there appear to be a few gotchas with kFSEventStreamCreateFlagWatchRoot as far as I can see:

According to Apple's file system events documentation then RootChanged indicates more than just a deletion.
Also note that it resets the event ID to 0 in case of a move, so there might be more work required in the FSEventsEmitter to not break.

I'll take a look tomorrow to whip up a few test cases to see what happens (on Big Sur at least, no access to older versions here).

@samschott
Copy link
Contributor Author

samschott commented Dec 10, 2020

Indeed, RootChanged will be emitted if the watched folder is moved and not deleted. A proper deletion is emitted when the watched folder is deleted (only with kFSEventStreamCreateFlagWatchRoot enabled, though). Keeping with the watchdog API, which does not allow for a watch to be dynamically moved to a new folder, I thought it best to treat RootChanged as a deleted event (like any other rename to a destination outside of watch.path).

Also, the event ID is not "reset". Only the RootChanged event itself will have an ID of zero, all other file system events will continue to have global and continuously increasing event IDs.

@BoboTiG
Copy link
Collaborator

BoboTiG commented Dec 11, 2020

Thank a lot @samschott !

I will wait for @CCP-Aporia approval. In the meantime, could you rebase your PR 🙏 ?

@CCP-Aporia
Copy link
Contributor

If the public API should simply be that a changed root always translates into a DirDeleted event, then this is certainly fine and just misses a few tests (I'm currently working on some). 🙂

@samschott
Copy link
Contributor Author

Rebased. @BoboTiG, could you confirm what the expected behaviour is for a moved root? I was merely extrapolating from the behaviour when child paths are moved out of root.

@samschott
Copy link
Contributor Author

@CCP-Aporia, there is already a test for the root being deleted (which is failing without this PR) but no test currently for the root being renamed.

@BoboTiG
Copy link
Collaborator

BoboTiG commented Dec 11, 2020

I think we need a unified behavior.
On Windows:

elif winapi_event.is_removed_self:
self.stop()

On Linux:
try:
event_buffer = os.read(self._inotify_fd, event_buffer_size)
except OSError as e:
if e.errno == errno.EINTR:
continue
and
def is_delete_self(self):
is never used.

So on other OSes, we just skip the event and even stop the observer on Windows.

Maybe this is not ideal 🤔

Should we just fire a DirDeletedEvent and stop the observer?

@BoboTiG
Copy link
Collaborator

BoboTiG commented Dec 11, 2020

For a moved root, I would say this is the same behavior. It should just fire the DirMovedEventevent and stop the observer. But I really need inputs because this may not be the better idea.

@samschott
Copy link
Contributor Author

I think it's definitely good to fire a DirDeletedEvent before stopping the observer when the root is deleted. Not so sure about moving, a DirMovedEvent may be more appropriate because it's more informative.

@samschott
Copy link
Contributor Author

Always firing a DirDeletedEvent will be easier to implement because finding out where the directory moved to requires some more work, at least on macOS. From their docs here:

If you need to figure out where the directory moved, you should open the root directory with open, then pass F_GETPATH to fcntl to find its current path. See the manual page for fcntl for more information.

@CCP-Aporia
Copy link
Contributor

CCP-Aporia commented Dec 11, 2020

Agreeing with @samschott on the separate events for the separate scenarios. And I'd even go as far as updating the watched path in the observer in case of a DirMovedEvent assuming that is feasible on all platforms that expose this behaviour.

@BoboTiG
Copy link
Collaborator

BoboTiG commented Dec 11, 2020

We can start light with only targeting the deleted root in that PR. Then, as the rename/move root is more work, it would be good to tackle it in a separate branch.

@samschott can you ensure the behavior will be the same on all implementation?

@samschott
Copy link
Contributor Author

samschott commented Dec 11, 2020

Will do. I have expanded the tests to check for a DirDeletedEvent on all platforms now and to check that the emitter thread is stopped. I have also adapted the Windows emitter to actually emit a DirDeletedEvent.

From looking at the code, it seems to me that the inotify backend already emits a DirDeletedEvent:

self._queue.put(inotify_event, delay)
if not isinstance(inotify_event, tuple) and inotify_event.is_delete_self and \
inotify_event.src_path == self._inotify.path:
# Deleted the watched directory, stop watching for events
deleted_self = True

Edit: The polling backend is also already in line, though catching all OSErrors here may be a bit broad:

try:
new_snapshot = self._take_snapshot()
except OSError:
self.queue_event(DirDeletedEvent(self.watch.path))
self.stop()
return

Lets see what the tests give...

@samschott
Copy link
Contributor Author

samschott commented Dec 13, 2020

There was a remaining issue with propagating the deleted event in inotify. Apart from this, test_delete_self should be passing on all platforms.

However, there remain issues with moving the root folder which will be difficult to solve. This is why I have removed test_move_self for now and would propose to tackle this in a separate PR. In particular:

  1. On Windows, the watch is created with a handle to the directory and not the path. When deleting the underlying directory, this raises an error which is caught in winapi.py and used to generate the deleted event:

    except OSError as e:
    if e.winerror == ERROR_OPERATION_ABORTED:
    return [], 0
    # Handle the case when the root path is deleted
    if _is_observed_path_deleted(handle, path):
    return _generate_observed_path_deleted_event()

    However, the watch seemingly happily continues when the root directory is moved without emitting any event. This follows the documented behaviour here. I do not have access to a Windows machine so cannot test if this is also the case when moving the root to trash.

  2. On inotify, we also don't receive a native event when the root is moved. This is problematic because it does inlcude moving the root to the trash (which is how end users typically delete a folder...). Fortunately, we can change this easily by requesting to be notified for IN_MOVE_SELF events.

  3. On macOS there is the known "problem" of being notified that the root changed but not knowing if it was deleted (os.path.exists should do the trick here) or moved and where it was moved to. As previously quoted, Apple docs recommend opening the root with open and passing F_GETPATH to fcntl to find its new path. This should be easy enough apart from Python not exposing F_GETPATH (see here). It's probably best to do this in the extension module instead.

To summarise, there are clear paths forward on FSEvents and Inotify and possibly a way on Windows. However, it involves moderate amounts of work and a choice of API: Do we want to emit a DirDeletedEvent or a DirMovedEvent? In case of the latter, do we want do move the watch or cancel it? To stay consistent with the API when an item is moved outside the root path, I would probably favour just emitted a DirDeletedEvent and leaving it at that. But of course it's not my choice!

In any case, dealing with a moved root is probably the topic of a separate PR.

@samschott
Copy link
Contributor Author

I just had a look at the test failures and none of them seem related to the changes in this PR. @BoboTiG, if you agree on postponing the handling of a moved root to a future PR (after making a decision on a uniform API), could you have a look at the changes here?

One last note on handling a moved root: In appears that the inotify backend already exposes a InotifyEvent.is_move_self attribute, it's just not used by the emitter. So it's really only the fsevents and winapi backends that need more work.

@CCP-Aporia
Copy link
Contributor

Let's get this merged. :-)

@BoboTiG
Copy link
Collaborator

BoboTiG commented Dec 16, 2020

Hm those Linux tests are now always failing:

FAILED tests/test_emitter.py::test_fast_subdirectory_creation_deletion
FAILED tests/test_inotify_c.py::test_late_double_deletion
FAILED tests/test_inotify_c.py::test_watch_file

Could you have a look?

@samschott
Copy link
Contributor Author

@BoboTiG, thanks for flagging that. There was actually a missing check that is_delete_self applied to the root path and not any of the children.

@BoboTiG BoboTiG merged commit 2758815 into gorakhargosh:master Dec 16, 2020
@BoboTiG
Copy link
Collaborator

BoboTiG commented Dec 16, 2020

Great job, thanks 💯

@samschott samschott deleted the fix-deletion-of-wachted-dir branch December 16, 2020 22:09
BoboTiG added a commit that referenced this pull request Feb 5, 2021
* Fix installation of Python 3.7.9 on Windows (#705)

* [windows] winapi.BUFFER_SIZE now defaults to 64000 (instead of 2048)

To handle cases with lot of changes, this seems the highest safest value we can use.

Note: it will fail with `ERROR_INVALID_PARAMETER` when it is greater than 64 KB and
      the application is monitoring a directory over the network.
      This is due to a packet size limitation with the underlying file sharing protocols.
      https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks

Also introduced `winapi.PATH_BUFFER_SIZE` (defaults to `2048`)
to keep the old behavior with path-realted functions.

* [mac] Regression fixes for native fsevents (#717)

* [mac] Fix missing event_id attribute in fsevents (#722)

* [mac] Return byte paths if a byte path was given in fsevents (#726)

* Pin py.test to a version that supports 2.7

* Disable pypy2 tests on Windows

They hang and timeout after 6 hours for some unknown reason

* [mac] Add compatibility with old macOS versions (#733)

* Ensure version macros are available

* Uniformize event for deletion of watched dir (#727)

* [inotify] Add support for the IN_CLOSE_WRITE event (#747)

* [mac] Support coalesced fsevents (#734)

* Add `is_coalesced` property to `NativeEvent`

So that we can effectively decide if we need to perform additional
system calls to figure out what really happened.

* Replace `NativeEvent._event_type` with `repr()` support

It's more pythonic, and the `_event_type` implementation wasn't quite
usable anyway.

NB: the representation is not truly copy/paste python code if there is
a double quote inside event.path, but that should be a rare case so we
don't add the expensive special case handling there.

* Allow running tests with debugger attached

Some Python debuggers create additional threads, so we shouldn't assume that there is only one.

* Request notifications for watched root

* Expect events on macOS instead of using `time.sleep()`

It might be even better to check for the emitter class, as opposed to platform

* Add exception handling to FSEventsEmitter

Reduce the amount of 'silent breakage'

* Use sentinel event when setting up tests on macOS

So that we can avoid a race between test setup and fseventsd

* Improve handling of coalesced events

* Revert accidental platform check change

* Fix renaming_top_level_directory test on macOS

* Generate sub events for move operations

* Remove `filesystem_view` again

While the `filesystem_view` helps with filtering out additional
`FileCreatedEvent`+`DirModifiedEvent` pairs then it also introduces
a huge amount of edge cases for synthetic events caused by move and
rename operations. On top of that, in order to properly resolve
those edge cases we'd have to go back to a solution very similar to
the old directory snapshots, with all the performance penalties they
suffered from...

As such I think it's better to acknowledge the behaviour for coalesced
events instead, and thus remove the `filesystem_view` again.

* [mac] Improve handling of rename events (#750)

* drop support for macOS 10.12 and lower

Co-authored-by: SamSchott <ss2151@cam.ac.uk>
Co-authored-by: Boris Staletic <boris.staletic@gmail.com>
Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr>
Co-authored-by: Dustin Ingram <di@users.noreply.github.com>
Co-authored-by: ysard <ysard@users.noreply.github.com>
Co-authored-by: Lukas Šupienis <l.supienis@ncs.lt>
Co-authored-by: Lukas Šupienis <lukas.supienis@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants