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

Created + Modified event is notified simultaneously #190

Closed
ritalin opened this issue Nov 18, 2024 · 6 comments
Closed

Created + Modified event is notified simultaneously #190

ritalin opened this issue Nov 18, 2024 · 6 comments
Assignees

Comments

@ritalin
Copy link

ritalin commented Nov 18, 2024

I'm writing unit tests using efsw for my program.
When I've operated from a file-create over a file-delete, Created + Modified event is notified simultaneously.

Actually, following event is notified:

  • Created
  • Created + Modified
  • Created + Modified + Deleted

As a result, following callbacks is called:

  • Created
  • Created
  • Modified
  • Created
  • Modified
  • Deleted

Did you have same experiences?

This is also reported the other project.

howeyc/fsnotify#62
howeyc/fsnotify#54 (comment)

I've tried latency and kFSEventStreamCreateFlagNoDefer but it is no effect.
I've carried out a file operation manually but this situation is not occurred.

@ritalin
Copy link
Author

ritalin commented Nov 18, 2024

I's effective somewhat calling callbacks in order of Deleted, Modified and Created .
But this is not perfect resolution.

WatcherFSEvents.cpp:

void WatcherFSEvents::handleAddModDel( const Uint32& flags, const std::string& path,
									   std::string& dirPath, std::string& filePath ) {
	if ( flags & efswFSEventStreamEventFlagItemRemoved ) {
		// Since i don't know the order, at least i try to keep the data consistent with the real
		// state
		if ( !FileInfo::exists( path ) ) {
			sendFileAction( ID, dirPath, filePath, Actions::Delete );
			return;
		}
	}

	if ( flags & efswFSEventStreamEventFlagItemModified ) { 
		sendFileAction( ID, dirPath, filePath, Actions::Modified );
			return;
	}

	if ( flags & efswFSEventStreamEventFlagItemCreated ) {
		if ( FileInfo::exists( path ) ) {
			sendFileAction( ID, dirPath, filePath, Actions::Add );
			return;
		}
	}
}

If following events is notified, It will be broken:

  • Created
  • Modified + Deleted

@SpartanJ SpartanJ self-assigned this Nov 19, 2024
@SpartanJ
Copy link
Owner

Hi!
Yes, this is basically how FSEvents works, it coalesces events into a single message and you end up with these weird situations. Although latency should work in your particular case, I can add an option to set the latency and disable NoDefer, but I don't know why you said it didn't work for you (I'll probably just add it even if it's not useful for you particular case).
I have never seen your particular case happening (repeated Create event seems really odd) so I'm not sure how are you achieving this, could you share a minimal example on how to reproduce it?

Regarding your suggestion it will not work properly for some cases, for example, you could get a Create + Remove event and in your case you'll simply send a Remove of an unreported file. Skipping events is a bad idea given how FSEvents behaves and because it asumes that the user requires some information more than other, it's better to let the consumer decide what to do with the information, this will depend on each particular case, and also can potentially break other current users implementation (it would break mine at the very least).

@ritalin
Copy link
Author

ritalin commented Nov 19, 2024

could you share a minimal example on how to reproduce it?

The following is reproducing code.

#include <filesystem>
#include <fstream>
#include <iostream>
#include <barrier>
#include <format>

#include "efsw/efsw.h"
#include "efsw/WatcherFSEvents.hpp"

namespace fs = std::filesystem;

struct Context {
    std::barrier<> sync{2};
    efsw_action expect_state = EFSW_ADD;
};

static auto notify(efsw_watcher watcher, efsw_watchid watchid, const char *dir, const char *filename, efsw_action action, const char *old_filename, void *param) -> void {
    auto ctx = reinterpret_cast<Context *>(param);
    std::cout << std::format("called: {}", (int)action) << std::endl;

    if (ctx->expect_state == action) {
        ctx->sync.arrive();
    }
}

auto main() -> int {
	efsw_watcher watcher = efsw_create(false);
    efsw_watcher_option opts[] = {
        { .option = EFSW_OPT_MAC_MODIFIED_FILTER, .value = efsw::efswFSEventStreamEventFlagItemModified },
    };

    struct Context ctx;

    const efsw_watchid id = efsw_addwatch_withoptions(watcher, "./.build", notify, false, opts, 1, &ctx);
    efsw_watch(watcher);
    
    fs::path path("./.build/a.txt");

    std::cout << "<<create stage>>" << std::endl;
    std::ofstream ofs(path);

    ctx.sync.arrive_and_wait();

    std::cout << "<<edit stage>>" << std::endl;
    ctx.expect_state = EFSW_MODIFIED;
    ofs << "Hello World" << std::endl;
    ofs.close();

    ctx.sync.arrive_and_wait();

    std::cout << "<<remove stage>>" << std::endl;
    ctx.expect_state = EFSW_DELETE;
    fs::remove(path);

    ctx.sync.arrive_and_wait();

    return 0;
}

Result:

<<create stage>>
notify:action/created: true, edited: false, removed: false
called: 1
<<edit stage>>
notify:action/created: true, edited: true, removed: false
called: 1
called: 3
<<remove stage>>
notify:action/created: true, edited: true, removed: true
called: 3
called: 2

EDIT: I wrote debug message to WatcherFSEvents::handleAddModDel.

void WatcherFSEvents::handleAddModDel( const Uint32& flags, const std::string& path,
									   std::string& dirPath, std::string& filePath ) {
			
	bool created = flags & efswFSEventStreamEventFlagItemCreated;
	bool edited = flags & ModifiedFlags;
	bool removed = flags & efswFSEventStreamEventFlagItemRemoved;

	std::cout << std::format("notify:action/created: {}, edited: {}, removed: {}", created, edited, removed) << std::endl;

       ....

Environment:

  • MacOS ventura 13.6.9 (Intel i7)

@SpartanJ
Copy link
Owner

SpartanJ commented Nov 22, 2024

I've been investigating and it seems to be expected behaviour from FSEvent which sucks honestly. I've tried to work around the issue modifying FSStream parameters but it's not possible to avoid. So there's only one only possible partial solution: keep track of the Add events (which is the most problematic reported event). Given that this will effectively change the reported events behaviour on macOS events I added a new option to "sanitize" the reported events, the option is Option::MacSanitizeEvents (or EFSW_OPT_MAC_SANITIZE_EVENTS for the C version). For your example will skip sending already sent Add events for an already inode add reported event. So in your example you'll get:

  • Created ( + Modified depending the case )
  • Modified
  • Modified + Deleted

Which is not perfect (IDK why still sends another modified) but it's good enough.

The whole problem comes from how FSEvents is implemented to be fast and cheap and not exactly precise (this is why they coalesce the events).

@ritalin
Copy link
Author

ritalin commented Nov 22, 2024

As the library package, it will be difficult to resolve this issue.
A work around may be only managing state into the callback to avoid the event duplication.

@SpartanJ
Copy link
Owner

It's fixable by just not using FSEvents, but that's just not the idea, this is good enough for basically any use case, since if you need more precision you can control it on the consumer side. I don't want to add more complexity to the current implementation, the idea is to have a general abstraction for each file system monitor OS API that there is, but not to build one on top of it. Although I must add that if you want it, you can actually use the Kqueue implementation for macOS, has its own limitations but it's more precise, in order to compile the macOS build to use Kqueue you must disable FSEvents by adding the compile flag EFSW_FSEVENTS_NOT_SUPPORTED. Maybe this is better for you use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants