bug#81531: 30.2; filenotify: moving file out of watched directory delays the "deleted" event until file is later moved back in
Michael Albinus via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]> Tue, 04 Aug 2026 16:14:07 +0200
| Newsgroups | gmane.emacs.bugs |
|---|---|
| Message-ID | <[email protected]> |
i_ix_iv <[email protected]> writes: Hi, > Using a basic filenotify watcher on a directory and testing several > file movement commands doesn't result in expected `delete` actions being > caught by the filenotify watcher. Thanks for the report. However, this isn't a bug. Before we're going to analysis, just some few words about filenotify.el. It is an umbrella for different file notification backends, like inotify, kqueue, gio, w32notify, and several file name handlers for remote files/directories. All of these backends raise different events, depending on the underlying OS. filenotify.el transforms them into the generic events documented in file-notify-add-watch. In your case, the inotify backend is in use. > Starting from `emacs -Q`: > - Run the below ELisp: > > ```elisp > (progn > (require 'filenotify) > (setq my-watch > (file-notify-add-watch > default-directory > '(change) > (lambda (event) > (message "%S" event))))) > ``` We must investigate both events coming from the respective backend, and the generic events you'll see in your handler. For tests, I use the following call: --8<---------------cut here---------------start------------->8--- # ~/src/emacs-30/src/emacs -Q -l filenotify --eval "(setq file-notify-debug t)" --eval "(file-notify-add-watch default-directory '(change) #'ignore)" --8<---------------cut here---------------end--------------->8--- Setting `file-notify-debug' to t enables all needed traces, so we can use the `ignore' handler. > 1 -> `touch foo` --8<---------------cut here---------------start------------->8--- file-notify-handle-event (file-notify ((1 . 0) (create) "foo" 0) file-notify--callback-inotify) file-notify-callback (1 . 0) created "/home/albinus/.emacs.d/foo" nil #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d" --8<---------------cut here---------------end--------------->8--- inotify sends the 'create "foo"' event. This is transformed into 'created "/home/albinus/.emacs.d/foo"', which is shown to the handler. Good. > 2 -> `mv foo bar` --8<---------------cut here---------------start------------->8--- file-notify-handle-event (file-notify ((1 . 0) (moved-from) "foo" 357539) file-notify--callback-inotify) file-notify-handle-event (file-notify ((1 . 0) (moved-to) "bar" 357539) file-notify--callback-inotify) file-notify-callback (1 . 0) renamed "/home/albinus/.emacs.d/foo" "/home/albinus/.emacs.d/bar" #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d" --8<---------------cut here---------------end--------------->8--- We get two events from inotify: 'moved-from "foo"', and 'moved-to "bar"'. Since both events carry the same cookie (357539), they belong together. Therefore, they are transformed into the generic event 'renamed "/home/albinus/.emacs.d/foo" "/home/albinus/.emacs.d/bar"'. Still good. > 3 -> `mv bar ..` --8<---------------cut here---------------start------------->8--- file-notify-handle-event (file-notify ((1 . 0) (moved-from) "bar" 370367) file-notify--callback-inotify) --8<---------------cut here---------------end--------------->8--- This is the problematic action. We get from inotify only a 'moved-from "bar"' event. The corresponding 'moved-to' event is not returned, because the file path is ouside of the watched directory. filenotify.el is instructed, to wait for the next backend event after 'moved-from'. This is needed to decide what to do. Since no second backend event arrives, nothing is sent to the handler. This is what you have observed. > 4 -> `mv ../bar .` --8<---------------cut here---------------start------------->8--- file-notify-handle-event (file-notify ((1 . 0) (moved-to) "bar" 389895) file-notify--callback-inotify) file-notify-callback (1 . 0) created "/home/albinus/.emacs.d/bar" nil #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d" --8<---------------cut here---------------end--------------->8--- inotify raises two events 'moved-from' and 'moved-to', again. But we see the latter only, because the file path of the 'moved-from' event is not seen. Now, inotify.el has two events 'moved-from' "bar"' (from step 3) and 'moved-to "bar"' (from step 4) in order. However, the cookies are different (370367 and 389895), so these events do *not* belong to each other. Therefore, filenotify.el decides, that the first event has to be understood as 'deleted' (not shown in the trace, I'll fix it), and the second event has to be understood as 'created'. Both generic events are forwarded to the handler. > 5 -> `rm bar` --8<---------------cut here---------------start------------->8--- file-notify-handle-event (file-notify ((1 . 0) (delete) "bar" 0) file-notify--callback-inotify) file-notify-callback (1 . 0) deleted "/home/albinus/.emacs.d/bar" nil #s(file-notify--watch "/home/albinus/.emacs.d" nil ignore) "/home/albinus/.emacs.d" "/home/albinus/.emacs.d" --8<---------------cut here---------------end--------------->8--- inotify event 'delete "bar"', which is forwarded to the handler as 'deleted "/home/albinus/.emacs.d/bar"'. Good. I hope this clarifies what happened.