Search code examples
cmacosbsdkqueue

kqueue on directory not trigger when file within is modified


I have used kquque to monitor desktop with:

  • flags - EV_ADD | EV_CLEAR
  • fflags - NOTE_DELETE | NOTE_WRITE | NOTE_EXTEND | NOTE_ATTRIB | NOTE_LINK | NOTE_RENAME | NOTE_REVOKE
  • filter - EVFILT_VNODE

However when I edit a .js file on desktop with sublime2 software, it doesnt trigger a notification :(

Please advise

Here is my js-ctypes code:

var rez_fd = ostypes.API('kqueue')();
console.info('rez_fd:', rez_fd.toString(), uneval(rez_fd));
if (ctypes.errno != 0) {
    throw new Error('Failed rez_fd, errno: ' + ctypes.errno);
}

this.kq = rez_fd;
this.path = OS.Constants.Path.desktopDir;

// Open a file descriptor for the file/directory that you want to monitor.
var event_fd = ostypes.API('open')(this.path, OS.Constants.libc.O_EVTONLY);
console.info('event_fd:', event_fd.toString(), uneval(event_fd));
if (ctypes.errno != 0) {
    throw new Error('Failed event_fd, errno: ' + ctypes.errno);
}

// The address in user_data will be copied into a field in the event.If you are monitoring multiple files,you could,for example,pass in different data structure for each file.For this example,the path string is used.
var user_data = ctypes.cast(ctypes.char.array()(this.path), ctypes.void.ptr);

// Set the timeout to wake us every half second.
var timeout = ostypes.TYPE.timespec();
var useSec = 0;
var useNsec = 500000000;
timeout.tv_sec = useSec; // 0 seconds
timeout.tv_nsec = useNsec; // 500 milliseconds

// Set up a list of events to monitor.
var fflags = vnode_events = ostypes.CONST.NOTE_DELETE | ostypes.CONST.NOTE_WRITE | ostypes.CONST.NOTE_EXTEND | ostypes.CONST.NOTE_ATTRIB | ostypes.CONST.NOTE_LINK | ostypes.CONST.NOTE_RENAME | ostypes.CONST.NOTE_REVOKE; // ostypes.TYPE.unsigned_int
var events_to_monitor = ostypes.TYPE.kevent.array(ostypes.CONST.NUM_EVENT_FDS)();
var filter = ostypes.CONST.EVFILT_VNODE;
var flags = ostypes.CONST.EV_ADD | ostypes.CONST.EV_CLEAR;
EV_SET(events_to_monitor.addressOfElement(0), event_fd, filter, flags, fflags, 0, user_data);

// Handle events
var event_data = ostypes.TYPE.kevent.array(ostypes.CONST.NUM_EVENT_SLOTS)(); // 1 slot

var num_files = 1; // ostypes.TYPE.int
var continue_loop = 40; // Monitor for twenty seconds. // ostypes.TYPE.int
while (--continue_loop) {
    var event_count = ostypes.API('kevent')(this.kq, ctypes.cast(events_to_monitor.address(), ostypes.TYPE.kevent.ptr), ostypes.CONST.NUM_EVENT_SLOTS, ctypes.cast(event_data.address(), ostypes.TYPE.kevent.ptr), num_files, timeout.address());
    console.info('event_count:', event_count.toString(), uneval(event_count));
    if (ctypes.errno != 0) {
        throw new Error('Failed event_count, errno: ' + ctypes.errno + ' and event_count: ' + cutils.jscGetDeepest(event_count));
    }
    if (cutils.jscEqual(event_data.addressOfElement(0).contents.flags, ostypes.CONST.EV_ERROR)) {
        throw new Error('Failed event_count, due to event_data.flags == EV_ERROR, errno: ' + ctypes.errno + ' and event_count: ' + cutils.jscGetDeepest(event_count));
    }

    if (!cutils.jscEqual(event_count, '0')) {
        console.log('Event ' + cutils.jscGetDeepest(event_data.addressOfElement(0).contents.ident) + ' occurred. Filter ' + cutils.jscGetDeepest(event_data.addressOfElement(0).contents.filter) + ', flags ' + cutils.jscGetDeepest(event_data.addressOfElement(0).contents.flags) + ', filter flags ' + cutils.jscGetDeepest(event_data.addressOfElement(0).contents.fflags) + ', filter data ' + cutils.jscGetDeepest(event_data.addressOfElement(0).contents.data) + ', path ' + cutils.jscGetDeepest(event_data.addressOfElement(0).contents.udata /*.contents.readString()*/ ));
    } else {
        // No event
    }

    // Reset the timeout. In case of a signal interrruption, the values may change.
    timeout.tv_sec = useSec; // 0 seconds
    timeout.tv_nsec = useNsec; // 500 milliseconds
}
ostypes.API('close')(event_fd);

Solution

  • I just realized that you're not monitoring the .js file, you're monitoring its directory. That makes everything a lot less mysterious.

    The short version is: If you open a file and write it, that doesn't change anything on the directory. If you atomically save a file, that does change the directory, but Sublime 2 doesn't atomically save by default.

    So, to watch for any changes to any file in the directory, you need to enumerate all files in the directory and add them all to the kqueue,* as well as the directory.

    Watching the directory will catch atomic saves (and new files being created); watching the files will catch overwrites. (Files being unlinked will trigger both.) If you're worried about performance… well, kqueue is designed to handle switching on 10000 file descriptors, and neither UFS nor HFS+ is a good filesystem for hundreds of thousands of directory entries in the same directory, so you're probably OK… but you may want to add some code that warns or aborts if the directory turns out to be massively huge.


    If you want to understand why this is necessary, you have to think about how the two different kinds of saves work.

    A write just writes to a file descriptor. That file descriptor could have one directory entry link on the filesystem—but it could just as easily have none (e.g., it was created in a temporary namespace, or you just unlinked the file after creating it), or many (e.g., you've created hard links to it). So it can't actually update "the directory entry for the file", because there is no such thing.

    An atomic save, on the other hand, works by creating a new temporary file, writing to that, and then rename-ing the temporary over the original file. That rename has to update the directory, replacing the entry pointing at the old file with an entry pointing at the new file. (Of course it also sends a DELETE notification for the file itself, because the file is losing a link. And you'll also usually sends an ATTRIB, because most apps want the new file to have the same extended attributes, extra forks, etc.)


    * There is an obvious race condition here: if a file is moved or deleted between the readdir and adding it to the kqueue, you'll get an error. You may want to handle that error by generating an immediate notification, or maybe you just want to ignore it—after all, from the user perspective, it's not much different from the case where someone deletes a file between the time your program starts and the time you do the readdir.