Java 7 WatchService - Ignoring multiple occurrences of the same event
Asked Answered
P

15

45

The javadoc for StandardWatchEventKinds.ENTRY_MODIFY says:

Directory entry modified. When a directory is registered for this event then the WatchKey is queued when it is observed that an entry in the directory has been modified. The event count for this event is 1 or greater.

When you edit the content of a file through an editor, it'll modify both date (or other metadata) and content. You therefore get two ENTRY_MODIFY events, but each will have a count of 1 (at least that's what I'm seeing).

I'm trying to monitor a configuration file (servers.cfg previously registered with the WatchService) that is manually updated (ie. through command line vi) with the following code:

while(true) {
    watchKey = watchService.take(); // blocks

    for (WatchEvent<?> event : watchKey.pollEvents()) {
        WatchEvent<Path> watchEvent = (WatchEvent<Path>) event;
        WatchEvent.Kind<Path> kind = watchEvent.kind();

        System.out.println(watchEvent.context() + ", count: "+ watchEvent.count() + ", event: "+ watchEvent.kind());
        // prints (loop on the while twice)
        // servers.cfg, count: 1, event: ENTRY_MODIFY
        // servers.cfg, count: 1, event: ENTRY_MODIFY

        switch(kind.name()) {
            case "ENTRY_MODIFY":
                handleModify(watchEvent.context()); // reload configuration class
                break;
            case "ENTRY_DELETE":
                handleDelete(watchEvent.context()); // do something else
                break;              
        }
    }   

    watchKey.reset();       
}

Since you get two ENTRY_MODIFY events, the above would reload the configuration twice when only once is needed. Is there any way to ignore all but one of these, assuming there could be more than one such event?

If the WatchService API has such a utility so much the better. (I kind of don't want to check times between each event. All the handler methods in my code are synchronous.

The same thing occurs if you create (copy/paste) a file from one directory to the watched directory. How can you combine both of those into one event?

Pestilential answered 27/5, 2013 at 17:23 Comment(0)
T
17

I had a similar issue - I am using the WatchService API to keep directories in sync, but observed that in many cases, updates were being performed twice. I seem to have resolved the issue by checking the timestamp on the files - this seems to screen out the second copy operation. (At least in windows 7 - I can't be sure if it will work correctly in other operation systems)

Maybe you could use something similar? Store the timestamp from the file and reload only when the timestamp is updated?

Tojo answered 4/6, 2013 at 11:3 Comment(1)
What if the file was modified two or more times in that group of events? Hypothetically you'd have 4, 6, 8... etc. modify events, but the value of .lastModified() would only represent the latest event.Surrebutter
R
58

WatcherServices reports events twice because the underlying file is updated twice. Once for the content and once for the file modified time. These events happen within a short time span. To solve this, sleep between the poll() or take() calls and the key.pollEvents() call. For example:

@Override
@SuppressWarnings( "SleepWhileInLoop" )
public void run() {
  setListening( true );

  while( isListening() ) {
    try {
      final WatchKey key = getWatchService().take();
      final Path path = get( key );

      // Prevent receiving two separate ENTRY_MODIFY events: file modified
      // and timestamp updated. Instead, receive one ENTRY_MODIFY event
      // with two counts.
      Thread.sleep( 50 );

      for( final WatchEvent<?> event : key.pollEvents() ) {
        final Path changed = path.resolve( (Path)event.context() );

        if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
          System.out.println( "Changed: " + changed );
        }
      }

      if( !key.reset() ) {
        ignore( path );
      }
    } catch( IOException | InterruptedException ex ) {
      // Stop eavesdropping.
      setListening( false );
    }
  }
}

Calling sleep() helps eliminate the double calls. The delay might have to be as high as three seconds.

Rasure answered 27/5, 2013 at 17:23 Comment(6)
Doesn't the WatchService key these events in the background? What would delaying retrieving them do?Pestilential
Doing so, you will receive an event with count value 2 instead of two separate events with count value 1. So, yo don't need to eliminate duplicate events manually. I walk this wa. Happy :)Entopic
This is the best answer, thank you for the description of why there are two events.Logic
Not working. Do I need to write it in separate thread? Currently I am writing in main method just for testing.Gravelblind
You do need to write this in a separate thread. Works perfectly. I tested this in windows and had 3 modify entry events when overwriting a file. Added Thread sleep and problem was solved. Thank you for the answer.Marks
can you also paste definition for isListening?Ardith
T
17

I had a similar issue - I am using the WatchService API to keep directories in sync, but observed that in many cases, updates were being performed twice. I seem to have resolved the issue by checking the timestamp on the files - this seems to screen out the second copy operation. (At least in windows 7 - I can't be sure if it will work correctly in other operation systems)

Maybe you could use something similar? Store the timestamp from the file and reload only when the timestamp is updated?

Tojo answered 4/6, 2013 at 11:3 Comment(1)
What if the file was modified two or more times in that group of events? Hypothetically you'd have 4, 6, 8... etc. modify events, but the value of .lastModified() would only represent the latest event.Surrebutter
S
6

One of my goto solutions for problems like this is to simply queue up the unique event resources and delay processing for an acceptable amount of time. In this case I maintain a Set<String> that contains every file name derived from each event that arrives. Using a Set<> ensures that duplicates don't get added and, therefore, will only be processed once (per delay period).

Each time an interesting event arrives I add the file name to the Set<> and restart my delay timer. When things settle down and the delay period elapses, I proceed to processing the files.

The addFileToProcess() and processFiles() methods are 'synchronized' to ensure that no ConcurrentModificationExceptions are thrown.

This simplified/standalone example is a derivative of Oracle's WatchDir.java:

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

public class DirectoryWatcherService implements Runnable {
    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>)event;
    }

    /*
     * Wait this long after an event before processing the files.
     */
    private final int DELAY = 500;

    /*
     * Use a SET to prevent duplicates from being added when multiple events on the 
     * same file arrive in quick succession.
     */
    HashSet<String> filesToReload = new HashSet<String>();

    /*
     * Keep a map that will be used to resolve WatchKeys to the parent directory
     * so that we can resolve the full path to an event file. 
     */
    private final Map<WatchKey,Path> keys;

    Timer processDelayTimer = null;

    private volatile Thread server;

    private boolean trace = false;

    private WatchService watcher = null;

    public DirectoryWatcherService(Path dir, boolean recursive) 
        throws IOException {
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<WatchKey,Path>();

        if (recursive) {
            registerAll(dir);
        } else {
            register(dir);
        }

        // enable trace after initial registration
        this.trace = true;
    }

    private synchronized void addFileToProcess(String filename) {
        boolean alreadyAdded = filesToReload.add(filename) == false;
        System.out.println("Queuing file for processing: " 
            + filename + (alreadyAdded?"(already queued)":""));
        if (processDelayTimer != null) {
            processDelayTimer.cancel();
        }
        processDelayTimer = new Timer();
        processDelayTimer.schedule(new TimerTask() {

            @Override
            public void run() {
                processFiles();
            }
        }, DELAY);
    }

    private synchronized void processFiles() {
        /*
         * Iterate over the set of file to be processed
         */
        for (Iterator<String> it = filesToReload.iterator(); it.hasNext();) {
            String filename = it.next();

            /*
             * Sometimes you just have to do what you have to do...
             */
            System.out.println("Processing file: " + filename);

            /*
             * Remove this file from the set.
             */
            it.remove();
        }
    }

    /**
     * Register the given directory with the WatchService
     */
    private void register(Path dir) throws IOException {
        WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        if (trace) {
            Path prev = keys.get(key);
            if (prev == null) {
                System.out.format("register: %s\n", dir);
            } else {
                if (!dir.equals(prev)) {
                    System.out.format("update: %s -> %s\n", prev, dir);
                }
            }
        }
        keys.put(key, dir);
    }

    /**
     * Register the given directory, and all its sub-directories, with the
     * WatchService.
     */
    private void registerAll(final Path start) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                throws IOException
            {
                if (dir.getFileName().toString().startsWith(".")) {
                    return FileVisitResult.SKIP_SUBTREE;
                }

                register(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Thread thisThread = Thread.currentThread();

        while (server == thisThread) {
            try {
                // wait for key to be signaled
                WatchKey key;
                try {
                    key = watcher.take();
                } catch (InterruptedException x) {
                    return;
                }

                Path dir = keys.get(key);
                if (dir == null) {
                    continue;
                }

                for (WatchEvent<?> event: key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();

                    if (kind == OVERFLOW) {
                        continue;
                    }

                    if (kind == ENTRY_MODIFY) {

                        WatchEvent<Path> ev = (WatchEvent<Path>)event;
                        Path name = ev.context();
                        Path child = dir.resolve(name);

                        String filename = child.toAbsolutePath().toString();

                        addFileToProcess(filename);
                    }
                }

                key.reset();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void start() {
        server = new Thread(this);
        server.setName("Directory Watcher Service");
        server.start();
    }


    public void stop() {
        Thread moribund = server;
        server = null;
        if (moribund != null) {
            moribund.interrupt();
        }
    }

    public static void main(String[] args) {
        if (args==null || args.length == 0) {
            System.err.println("You need to provide a path to watch!");
            System.exit(-1);
        }

        Path p = Paths.get(args[0]);
        if (!Files.isDirectory(p)) {
            System.err.println(p + " is not a directory!");
            System.exit(-1);
        }

        DirectoryWatcherService watcherService;
        try {
            watcherService = new DirectoryWatcherService(p, true);
            watcherService.start();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

}
Sandhurst answered 15/1, 2016 at 19:41 Comment(0)
A
4

I modified WatchDir.java to receive only human-made modifications. Comparing .lastModified() of a file.

long lastModi=0; //above for loop
if(kind==ENTRY_CREATE){
    System.out.format("%s: %s\n", event.kind().name(), child);
}else if(kind==ENTRY_MODIFY){
    if(child.toFile().lastModified() - lastModi > 1000){
        System.out.format("%s: %s\n", event.kind().name(), child);
    }
}else if(kind==ENTRY_DELETE){
    System.out.format("%s: %s\n", event.kind().name(), child);
}
    lastModi=child.toFile().lastModified();
Arise answered 4/4, 2015 at 4:32 Comment(0)
S
3

Here is a full implementation using timestamps to avoid firing multiple events:

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;

public abstract class DirectoryWatcher
{
    private WatchService watcher;
    private Map<WatchKey, Path> keys;
    private Map<Path, Long> fileTimeStamps;
    private boolean recursive;
    private boolean trace = true;

    @SuppressWarnings("unchecked")
    private static <T> WatchEvent<T> cast(WatchEvent<?> event)
    {
        return (WatchEvent<T>) event;
    }

    /**
     * Register the given directory with the WatchService
     */
    private void register(Path directory) throws IOException
    {
        WatchKey watchKey = directory.register(watcher, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);

        addFileTimeStamps(directory);

        if (trace)
        {
            Path existingFilePath = keys.get(watchKey);
            if (existingFilePath == null)
            {
                System.out.format("register: %s\n", directory);
            } else
            {
                if (!directory.equals(existingFilePath))
                {
                    System.out.format("update: %s -> %s\n", existingFilePath, directory);
                }
            }
        }

        keys.put(watchKey, directory);
    }

    private void addFileTimeStamps(Path directory)
    {
        File[] files = directory.toFile().listFiles();
        if (files != null)
        {
            for (File file : files)
            {
                if (file.isFile())
                {
                    fileTimeStamps.put(file.toPath(), file.lastModified());
                }
            }
        }
    }

    /**
     * Register the given directory, and all its sub-directories, with the
     * WatchService.
     */
    private void registerAll(Path directory) throws IOException
    {
        Files.walkFileTree(directory, new SimpleFileVisitor<Path>()
        {
            @Override
            public FileVisitResult preVisitDirectory(Path currentDirectory, BasicFileAttributes attrs)
                    throws IOException
            {
                register(currentDirectory);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * Creates a WatchService and registers the given directory
     */
    DirectoryWatcher(Path directory, boolean recursive) throws IOException
    {
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<>();
        fileTimeStamps = new HashMap<>();
        this.recursive = recursive;

        if (recursive)
        {
            System.out.format("Scanning %s ...\n", directory);
            registerAll(directory);
            System.out.println("Done.");
        } else
        {
            register(directory);
        }

        // enable trace after initial registration
        this.trace = true;
    }

    /**
     * Process all events for keys queued to the watcher
     */
    void processEvents() throws InterruptedException, IOException
    {
        while (true)
        {
            WatchKey key = watcher.take();

            Path dir = keys.get(key);
            if (dir == null)
            {
                System.err.println("WatchKey not recognized!!");
                continue;
            }

            for (WatchEvent<?> event : key.pollEvents())
            {
                WatchEvent.Kind watchEventKind = event.kind();

                // TBD - provide example of how OVERFLOW event is handled
                if (watchEventKind == OVERFLOW)
                {
                    continue;
                }

                // Context for directory entry event is the file name of entry
                WatchEvent<Path> watchEvent = cast(event);
                Path fileName = watchEvent.context();
                Path filePath = dir.resolve(fileName);

                long oldFileModifiedTimeStamp = fileTimeStamps.get(filePath);
                long newFileModifiedTimeStamp = filePath.toFile().lastModified();
                if (newFileModifiedTimeStamp > oldFileModifiedTimeStamp)
                {
                    fileTimeStamps.remove(filePath);
                    onEventOccurred();
                    fileTimeStamps.put(filePath, filePath.toFile().lastModified());
                }

                if (recursive && watchEventKind == ENTRY_CREATE)
                {
                    if (Files.isDirectory(filePath, NOFOLLOW_LINKS))
                    {
                        registerAll(filePath);
                    }
                }

                break;
            }

            boolean valid = key.reset();

            if (!valid)
            {
                keys.remove(key);

                if (keys.isEmpty())
                {
                    break;
                }
            }
        }
    }

    public abstract void onEventOccurred();
}

Extend the class and implement the onEventOccurred() method.

Seeger answered 16/11, 2016 at 20:56 Comment(0)
U
2

Are you sure there is problem with jdk7? It gives correct result for me (jdk7u15, windows)

Code

import java.io.IOException;
import java.nio.file.*;

public class WatchTest {

    public void watchMyFiles() throws IOException, InterruptedException {
        Path path = Paths.get("c:/temp");
        WatchService watchService = path.getFileSystem().newWatchService();
        path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);

        while (true) {
            WatchKey watchKey = watchService.take(); // blocks

            for (WatchEvent<?> event : watchKey.pollEvents()) {
                WatchEvent<Path> watchEvent = (WatchEvent<Path>) event;
                WatchEvent.Kind<Path> kind = watchEvent.kind();

                System.out.println(watchEvent.context() + ", count: " +
                        watchEvent.count() + ", event: " + watchEvent.kind());
                // prints (loop on the while twice)
                // servers.cfg, count: 1, event: ENTRY_MODIFY
                // servers.cfg, count: 1, event: ENTRY_MODIFY

                switch (kind.name()) {
                    case "ENTRY_MODIFY":
                        handleModify(watchEvent.context()); // reload configuration class
                        break;
                    case "ENTRY_DELETE":
                        handleDelete(watchEvent.context()); // do something else
                        break;
                    default:
                        System.out.println("Event not expected " + event.kind().name());
                }
            }

            watchKey.reset();
        }
    }

    private void handleDelete(Path context) {
        System.out.println("handleDelete  " + context.getFileName());
    }

    private void handleModify(Path context) {
        System.out.println("handleModify " + context.getFileName());
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        new WatchTest().watchMyFiles();
    }
}

Output is like below- when file is copied over or edited using notepad.

config.xml, count: 1, event: ENTRY_MODIFY
handleModify config.xml

Vi uses many additional files, and seems to update file attribute multiple times. notepad++ does exactly two times.

Udelle answered 11/6, 2013 at 5:47 Comment(4)
WatchService is a Java 7 feature, so yes, in that sense. If you copied over a file, you should see both an ENTRY_CREATED and ENTRY_MODIFY.Pestilential
@ Sotirios Delimanolis: You mean you get create notification when watching for StandardWatchEventKinds.ENTRY_MODIFY? Solution from tofarr seems to be a good workaround.Udelle
Yea, nvm, you aren't watching for it. I'm surprised you only get one ENTRY_MODIFY though. Tofarr's solution is what I've done for now, but I'm still giving it some time for other possible solutions.Pestilential
I had the same issue, then I read Jayan's comment regarding the notepad++ updating the file attributes twice. The problem is not the watcher, it was the file editor, in my case.Phenacite
M
2

If you use RxJava you can use the operator throttleLast. In the example below only the last event in 1000 milliseconds is emitted for each file in the watched directory.

public class FileUtils {
    private static final long EVENT_DELAY = 1000L;

    public static Observable<FileWatchEvent> watch(Path directory, String glob) {
        return Observable.<FileWatchEvent>create(subscriber -> {
            final PathMatcher matcher = directory.getFileSystem().getPathMatcher("glob:" + glob);

            WatchService watcher = FileSystems.getDefault().newWatchService();
            subscriber.setCancellable(watcher::close);

            try {
                directory.register(watcher,
                        ENTRY_CREATE,
                        ENTRY_DELETE,
                        ENTRY_MODIFY);
            } catch (IOException e) {
                subscriber.onError(e);
                return;
            }

            while (!subscriber.isDisposed()) {
                WatchKey key;
                try {
                    key = watcher.take();
                } catch (InterruptedException e) {
                    if (subscriber.isDisposed())
                        subscriber.onComplete();
                    else
                        subscriber.onError(e);
                    return;
                }

                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();

                    if (kind != OVERFLOW) {
                        WatchEvent<Path> ev = (WatchEvent<Path>) event;
                        Path child = directory.resolve(ev.context());

                        if (matcher.matches(child.getFileName()))
                            subscriber.onNext(new FileWatchEvent(kindToType(kind), child));
                    }
                }

                if (!key.reset()) {
                    subscriber.onError(new IOException("Invalid key"));
                    return;
                }
            }
        }).groupBy(FileWatchEvent::getPath).flatMap(o -> o.throttleLast(EVENT_DELAY, TimeUnit.MILLISECONDS));
    }

    private static FileWatchEvent.Type kindToType(WatchEvent.Kind kind) {
        if (StandardWatchEventKinds.ENTRY_CREATE.equals(kind))
            return FileWatchEvent.Type.ADDED;
        else if (StandardWatchEventKinds.ENTRY_MODIFY.equals(kind))
            return FileWatchEvent.Type.MODIFIED;
        else if (StandardWatchEventKinds.ENTRY_DELETE.equals(kind))
            return FileWatchEvent.Type.DELETED;
        throw new RuntimeException("Invalid kind: " + kind);
    }

    public static class FileWatchEvent {
        public enum Type {
            ADDED, DELETED, MODIFIED
        }

        private Type type;
        private Path path;

        public FileWatchEvent(Type type, Path path) {
            this.type = type;
            this.path = path;
        }

        public Type getType() {
            return type;
        }

        public Path getPath() {
            return path;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            FileWatchEvent that = (FileWatchEvent) o;

            if (type != that.type) return false;
            return path != null ? path.equals(that.path) : that.path == null;
        }

        @Override
        public int hashCode() {
            int result = type != null ? type.hashCode() : 0;
            result = 31 * result + (path != null ? path.hashCode() : 0);
            return result;
        }
    }
}
Michaelson answered 22/7, 2017 at 9:11 Comment(0)
F
1

I tried this and it's working perfectly :

import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static java.nio.file.StandardWatchEventKinds.*;

public class FileWatcher implements Runnable, AutoCloseable {

    private final WatchService service;
    private final Map<Path, WatchTarget> watchTargets = new HashMap<>();
    private final List<FileListener> fileListeners = new CopyOnWriteArrayList<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock r = lock.readLock();
    private final Lock w = lock.writeLock();
    private final AtomicBoolean running = new AtomicBoolean(false);

    public FileWatcher() throws IOException {
        service = FileSystems.getDefault().newWatchService();
    }

    @Override
    public void run() {
        if (running.compareAndSet(false, true)) {
            while (running.get()) {
                WatchKey key;
                try {
                    key = service.take();
                } catch (Throwable e) {
                    break;
                }
                if (key.isValid()) {
                    r.lock();
                    try {
                        key.pollEvents().stream()
                                .filter(e -> e.kind() != OVERFLOW)
                                .forEach(e -> watchTargets.values().stream()
                                        .filter(t -> t.isInterested(e))
                                        .forEach(t -> fireOnEvent(t.path, e.kind())));
                    } finally {
                        r.unlock();
                    }
                    if (!key.reset()) {
                        break;
                    }
                }
            }
            running.set(false);
        }
    }

    public boolean registerPath(Path path, boolean updateIfExists, WatchEvent.Kind... eventKinds) {
        w.lock();
        try {
            WatchTarget target = watchTargets.get(path);
            if (!updateIfExists && target != null) {
                return false;
            }
            Path parent = path.getParent();
            if (parent != null) {
                if (target == null) {
                    watchTargets.put(path, new WatchTarget(path, eventKinds));
                    parent.register(service, eventKinds);
                } else {
                    target.setEventKinds(eventKinds);
                }
                return true;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            w.unlock();
        }
        return false;
    }

    public void addFileListener(FileListener fileListener) {
        fileListeners.add(fileListener);
    }

    public void removeFileListener(FileListener fileListener) {
        fileListeners.remove(fileListener);
    }

    private void fireOnEvent(Path path, WatchEvent.Kind eventKind) {
        for (FileListener fileListener : fileListeners) {
            fileListener.onEvent(path, eventKind);
        }
    }

    public boolean isRunning() {
        return running.get();
    }

    @Override
    public void close() throws IOException {
        running.set(false);
        w.lock();
        try {
            service.close();
        } finally {
            w.unlock();
        }
    }

    private final class WatchTarget {

        private final Path path;
        private final Path fileName;
        private final Set<String> eventNames = new HashSet<>();
        private final Event lastEvent = new Event();

        private WatchTarget(Path path, WatchEvent.Kind[] eventKinds) {
            this.path = path;
            this.fileName = path.getFileName();
            setEventKinds(eventKinds);
        }

        private void setEventKinds(WatchEvent.Kind[] eventKinds) {
            eventNames.clear();
            for (WatchEvent.Kind k : eventKinds) {
                eventNames.add(k.name());
            }
        }

        private boolean isInterested(WatchEvent e) {
            long now = System.currentTimeMillis();
            String name = e.kind().name();
            if (e.context().equals(fileName) && eventNames.contains(name)) {
                if (lastEvent.name == null || !lastEvent.name.equals(name) || now - lastEvent.when > 100) {
                    lastEvent.name = name;
                    lastEvent.when = now;
                    return true;
                }
            }
            return false;
        }

        @Override
        public int hashCode() {
            return path.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            return obj == this || obj != null && obj instanceof WatchTarget && Objects.equals(path, ((WatchTarget) obj).path);
        }

    }

    private final class Event {

        private String name;
        private long when;

    }

    public static void main(String[] args) throws IOException, InterruptedException {
        FileWatcher watcher = new FileWatcher();
        if (watcher.registerPath(Paths.get("filename"), false, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE)) {
            watcher.addFileListener((path, eventKind) -> System.out.println(path + " -> " + eventKind.name()));
            new Thread(watcher).start();
            System.in.read();
        }
        watcher.close();
        System.exit(0);
    }

}

FileListener :

import java.nio.file.Path;
import java.nio.file.WatchEvent;

public interface FileListener {

    void onEvent(Path path, WatchEvent.Kind eventKind);

}
Fasto answered 31/5, 2015 at 11:49 Comment(3)
Can you explain what the code does? Does it combine both of those into one event? Does it use a solution where only one event is triggered?Pestilential
@SotiriosDelimanolis in the method isInterested we are ignoring occurrence of same events that occurred within 100 ms. but in some cases it could be unsafe. for example, if two applications are modifying the same file and application B finishes it's job 50 ms after than application A did ...Fasto
The OP: I kind of don't want to check times between each eventHildegard
H
1

I compiled Oracle's WatchDir.java and @nilesh's suggestion into an Observable class that will notify it's observers once when the watched file is changed.

I tried to make it as readable and short as possible, but still landed with more than 100 lines. Improvements welcome, of course.

Usage:

FileChangeNotifier fileReloader = new FileChangeNotifier(File file);
fileReloader.addObserver((Observable obj, Object arg) -> {
    System.out.println("File changed for the " + arg + " time.");
});

See my solution on GitHub: FileChangeNotifier.java.

Hildegard answered 31/7, 2015 at 11:59 Comment(4)
+1 for Observable. The only issue with this solution is that it's not very scalable, as you are spawning one thread per observer. That's a lot if you are going to watch several hundreds of files!Verb
@Verb you're very right, thanks! My use case was watching a single file, hadn't thought about scalability. FaNaJ's solution below probably works a lot better when watching a lot of files.Hildegard
I guess you could combine both somehow, and have the FileChangeNotifier register an Observable representation of a file change. Just have the while-loop in the solution you pointed out notifying the observers, and you have the best of both worlds ;-)Issuing a random, platform-dependant sleep seems very prone to chance.Verb
I cluttered my implementation already with the ignoreNext flag for when I myself modified the file (a bit quick and dirty...). It should be relatively easy to drop that and add a Map of Paths to a List of Observers. I rather would like to not do it myself without a use case though, and remembering what an effort testing this on different platforms was...Hildegard
L
1

I solved this problem by defining a global boolean variable named "modifySolver" which be false by default. You can handle this problem as I show bellow:

else if (eventKind.equals (ENTRY_MODIFY))
        {
            if (event.count() == 2)
            {
                getListener(getDirPath(key)).onChange (FileChangeType.MODIFY, file.toString ());
            }
            /*capture first modify event*/
            else if ((event.count() == 1) && (!modifySolver))
            {
                getListener(getDirPath(key)).onChange (FileChangeType.MODIFY, file.toString ());
                modifySolver = true;
            }
            /*discard the second modify event*/
            else if ((event.count() == 1) && (modifySolver))
            {
                modifySolver = false;
            }
        }
Lone answered 10/5, 2016 at 5:51 Comment(0)
J
0

I had similar problem. I know this is late but it might help someone. I just needed to eliminate duplicate ENTRY_MODIFY. Whenever ENTRY_MODIFY is triggered, count() returns either 2 or 1. If it is 1, then there will be another event with count() 1. So just put a global counter which keeps the count of return values and carry out the operations only when the counter becomes 2. Something like this can do:

WatchEvent event; 
int count = 0;

if(event.count() == 2)
     count = 2;

if(event.count() == 1)
     count++;

if(count == 2){
     //your operations here
     count = 0;
}
Jacksnipe answered 5/11, 2015 at 14:17 Comment(4)
This seems OK if 2 is guaranteed. In my tests, different applications behave differently. cmd and java trigger one event on write where as notepad.exe and notepad++ trigger two.Hostel
You are right. This was not that helpful in some cases so I ended up using this solution from Dave Jarvis. May be you can use both the solutions combined if that helps you.Jacksnipe
I've done some benchmarking. On my machine, saving a file with 1 line results in about 6ms between "duplicate" events. Saving a file with 10,000,000 lines results in about 500ms between "duplicate" events.Hostel
The duplicate calls actually depend on the underlying file system and the application which changes the file content. The behavior changes from application to application as well so your mileage may vary.Jacksnipe
Y
0
    /**
 * 
 * 
 * in windows os, multiple event will be fired for a file create action
 * this method will combine the event on same file
 * 
 * for example:
 * 
 * pathA -> createEvent -> createEvent
 * pathA -> createEvent + modifyEvent, .... -> modifyEvent
 * pathA -> createEvent + modifyEvent, ...., deleteEvent -> deleteEvent
 * 
 * 
 * 
 * 在windows环境下创建一个文件会产生1个创建事件+多个修改事件, 这个方法用于合并重复事件
 * 合并优先级为 删除 > 更新 > 创建
 * 
 *
 * @param events
 * @return
 */
private List<WatchEvent<?>> filterEvent(List<WatchEvent<?>> events) {


    // sorted by event create > modify > delete
    Comparator<WatchEvent<?>> eventComparator = (eventA, eventB) -> {
        HashMap<WatchEvent.Kind, Integer> map = new HashMap<>();
        map.put(StandardWatchEventKinds.ENTRY_CREATE, 0);
        map.put(StandardWatchEventKinds.ENTRY_MODIFY, 1);
        map.put(StandardWatchEventKinds.ENTRY_DELETE, 2);
        return map.get(eventA.kind()) - map.get(eventB.kind());

    };
    events.sort(eventComparator);

    HashMap<String, WatchEvent<?>> hashMap = new HashMap<>();
    for (WatchEvent<?> event : events) {
        // if this is multiple event on same path
        // the create event will added first
        // then override by modify event
        // then override by delete event
        hashMap.put(event.context().toString(), event);
    }


    return new ArrayList<>(hashMap.values());


}
Yecies answered 27/3, 2018 at 8:33 Comment(0)
P
0

If you are trying the same in Scala using better-files-akka library, I have comeup with this work around based on the solution proposed in the accepted answer.

https://github.com/pathikrit/better-files/issues/313

trait ConfWatcher {

  implicit def actorSystem: ActorSystem

  private val confPath = "/home/codingkapoor/application.conf"
  private val appConfFile = File(confPath)
  private var appConfLastModified = appConfFile.lastModifiedTime

  val watcher: ActorRef = appConfFile.newWatcher(recursive = false)

  watcher ! on(EventType.ENTRY_MODIFY) { file =>
    if (appConfLastModified.compareTo(file.lastModifiedTime) < 0) {
      // TODO
      appConfLastModified = file.lastModifiedTime
    }
  }

}
Palmapalmaceous answered 9/5, 2019 at 6:19 Comment(0)
Y
0

The set of multiple events depends on the tool used to create/modify files.

1.Create new file with Vim

MODIFIED, CREATED events are fired

2.Modify file with Vim

DELETED, MODIFIED, CREATED events are fired

3.Use Linux command 'mv' to move file from other folder to watched folder

MODIFIED event is fired

4.Use Linux command 'cp' to copy file from other folder to watched folder

MODIFIED, CREATED are fired if no file with same file name exists CREATED is fired if file with same file name exists

I used 3 maps to collect CREATED/MODIFIED/DELETED entries in the for loop iterating over WatchEvent. Then, run 3 for loops over those 3 maps to determine the correct event we need to notify (ex: if a file name appears in all three map, then we could say it is a MODIFIED event)

            File f = new File(sourceDir);
        if (!f.exists() || !f.isDirectory()) {
            LOGGER.warn("File " + sourceDir + " does not exist OR is not a directory");
            return;
        }
        WatchService watchService = FileSystems.getDefault().newWatchService();
        Path watchedDir = f.toPath();
        watchedDir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);

        while (true) {
            try {
                WatchKey watchKey = watchService.take();

                Thread.sleep(checkInterval);

                //Events fired by Java NIO file watcher depends on the tool used
                //to create/update/delete file. We need those 3 maps to collect
                //all entries fired by NIO watcher. Then, make 3 loops at the end
                //to determine the correct unique event to notify
                final Map<String, Boolean> createdEntries = new HashMap<>();
                final Map<String, Boolean> modifiedEntries = new HashMap<>();
                final Map<String, Boolean> deletedEntries = new HashMap<>();

                List<WatchEvent<?>> events = watchKey.pollEvents();
                for (WatchEvent<?> event : events) {
                    if (event.kind() == OVERFLOW) {
                        continue;
                    }

                    WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
                    WatchEvent.Kind<Path> kind = pathEvent.kind();

                    Path path = pathEvent.context();
                    String fileName = path.toString();
                    if (accept(fileName)) {
                        if (kind == ENTRY_CREATE) {
                            createdEntries.put(fileName, true);
                        } else if (kind == ENTRY_MODIFY) {
                            modifiedEntries.put(fileName, true);
                        } else if (kind == ENTRY_DELETE) {
                            deletedEntries.put(fileName, true);
                        }
                    }
                }

                long timeStamp = System.currentTimeMillis();
                final Map<String, Boolean> handledEntries = new HashMap<>();

                //3 for loops to determine correct event to notify
                for (String key : createdEntries.keySet()) {
                    if (handledEntries.get(key) == null) {
                        Boolean modified = modifiedEntries.get(key);
                        Boolean deleted = deletedEntries.get(key);
                        if (modified != null && deleted != null) {
                            //A triplet of DELETED/MODIFIED/CREATED means a MODIFIED event
                            LOGGER.debug("File " + key + " was modified");
                            notifyFileModified(key, timeStamp);
                        } else if (modified != null) {
                            LOGGER.debug("New file " + key + " was created");
                            notifyFileCreated(key, timeStamp);
                        } else {
                            LOGGER.debug("New file " + key + " was created");
                            notifyFileCreated(key, timeStamp);
                        }
                        handledEntries.put(key, true);
                    }
                }

                for (String key : modifiedEntries.keySet()) {
                    if (handledEntries.get(key) == null) {
                        //Current entry survives from loop on CREATED entries. It is certain
                        //that we have MODIFIED event
                        LOGGER.debug("File " + key + " was modified");
                        notifyFileModified(key, timeStamp);
                        handledEntries.put(key, true);
                    }

                }

                for (String key : deletedEntries.keySet()) {
                    if (handledEntries.get(key) == null) {
                        //Current entry survives from two loops on CREATED/MODIFIED entries. It is certain
                        //that we have DELETE event
                        LOGGER.debug("File " + key + " was deleted");
                        notifyFileDeleted(key, timeStamp);
                    }
                }

                boolean valid = watchKey.reset();
                if (!valid) {
                    break;
                }
            } catch (Exception ex) {
                LOGGER.warn("Error while handling file events under: " + sourceDir, ex);
            }

            Thread.sleep(checkInterval);
        }
Yokoyokohama answered 14/8, 2021 at 7:55 Comment(0)
R
-1

Untested, but perhaps this will work:

AtomicBoolean modifyEventFired = new AtomicBoolean();
modifyEventFired.set(false);

while(true) {
    watchKey = watchService.take(); // blocks

    for (WatchEvent<?> event : watchKey.pollEvents()) {
        WatchEvent<Path> watchEvent = (WatchEvent<Path>) event;
        WatchEvent.Kind<Path> kind = watchEvent.kind();

        System.out.println(watchEvent.context() + ", count: "+ watchEvent.count() + ", event: "+ watchEvent.kind());
        // prints (loop on the while twice)
        // servers.cfg, count: 1, event: ENTRY_MODIFY
        // servers.cfg, count: 1, event: ENTRY_MODIFY

        switch(kind.name()) {
            case "ENTRY_MODIFY":
                if(!modifyEventFired.get()){
                   handleModify(watchEvent.context()); // reload configuration class
                   modifyEventFired.set(true);                           
                }
                break;
            case "ENTRY_DELETE":
                handleDelete(watchEvent.context()); // do something else
                break;              
        }
    }   
    modifyEventFired.set(false);
    watchKey.reset();       
}
Repertoire answered 27/5, 2013 at 20:37 Comment(3)
The for loop actually just loops once. The while loop loops twice with a single event in watchKey.pollEvents(), so I don't think this will work.Pestilential
If you expand out your for each loop, you can get access to the size() method of the pollEvents list: List<WatchEvent<?>> events = watchKey.pollEvents(); System.out.println(events.size()); Does this also return 1, or does it accurately show 2 events? The count of watchEvent should be 2 if its a duplicate, but in this instance I think your 2 may reflect better in the list.Repertoire
pollEvents().size() is one, but take() happens twice (then blocks again, waiting). It's confusing to talk about this. There are two OS events (ex: modify content and modify metadata) but only one human event. I guess the WatchService API sees it as two WatchKeys with one WatchEvent each.Pestilential

© 2022 - 2024 — McMap. All rights reserved.