WatchService

Wolfgang Baltes wolfgang.baltes at laposte.net
Thu Dec 16 12:43:26 PST 2010


On 2010-12-16 10:17, Wolfgang Baltes wrote:
>
>
> On 2010-12-16 06:48, Alan Bateman wrote:
>>
>> wolfgang.baltes wrote:
>>>
>>> Hi,
>>>
>>> I have started to use the WatchService and would like to get 
>>> confirmation for some of my observations. At least one of them is 
>>> not in agreement with the documentation.
>>>
>>> My system is Windows 7 Ultimate 64bit with JDK7-b121 32bit and using 
>>> Eclipse 3.61 32bit as the development platform. I am writing a piece 
>>> of code that watches one or several directories recursively or not 
>>> for file additions and deletions.
>>>
>>> My observations are for a directory tree where all 
>>> directories/subdirectories are being watched. I use Windows Explorer 
>>> to copy files and directory trees into this directory tree and to 
>>> delete them.
>>>
>>> 1 - If I delete a file at any level in the tree, then I get the 
>>> ENTRY_DELETE event for the file and an ENTRY_MODIFIED event for its 
>>> parent directory. - This should be normal.
>>>
>>> 2 - If I delete a subdirectory that contains files but no further 
>>> subdirectories, then I receive an ENTRY_DELETE event for the 
>>> subdirectory and an ENTRY_MODIFIED event for its parent directory. 
>>> In addition - and only sometimes - I also receieve ENTRY_DELETE 
>>> events for the files within the subdirectory. - I assume that this 
>>> is normal, although I don't understand the random behavior of 
>>> getting or not getting events regarding the files.
>>>
>>> 3 - If I try to delete a subdirectory tree - that is a subdirectory 
>>> with nested directories - then Windows Explorer does not let me 
>>> delete the tree. The WatchService seems to block this. I can 
>>> however, delete this tree by starting at the bottom (leaf) 
>>> directories and delete them one at a time. This corresponds 
>>> essentially to the scenario described in 2. - This may be normal, 
>>> but is certainly not convenient. Is this a Windows feature?
>>>
>>> 4 - Contrary to the documentation (tutorial on WatchService), the 
>>> WatchKey remains valid (isValid()) after deletion of the associated 
>>> directory. That is, the expression (dir.notExists() && 
>>> key.isValid()), where key is the WatchKey associated with dir is 
>>> true. The first part of the expression confirms that dir is deleted, 
>>> and the second part should be false if the key is automatically 
>>> cancelled. - I don' think this is normal at all!
>>>
>>> Thanks for any comments you may have,
>>>
>> For item 3, yes, unfortunately if you are watching a directory then 
>> Windows prevents you deleting or moving the directory's parent. It's 
>> not a problem deleting the directory that is being watched, it's the 
>> parent that is the issue.  We have a bug tracking it:
>>    http://bugs.sun.com/view_bug.do?bug_id=6972833
>>
>> but I don't think there is anything we can do because it seems to be 
>> a Windows limitation.
>>
>> I'm interested if you can create a test case to demonstrate item 4. 
>> When a watched directory is deleted then we get a notification from 
>> Windows (via a completion port) and that causes the key to be 
>> invalidated. We have various test cases for this and I'm not aware of 
>> any issues.
>>
>> -Alan
>>
>>
> Thanks, Alan for the quick reply.
>
> I am happy to see that my observations were correct, at least 1 to 3, 
> with 4 needing to be confirmed. I will spend some time on it today to 
> make a small program that displays the behavior. My application is an 
> OSGi bundle which requires quite some heavy lifting for such a small 
> thing.
>
> Wolfgang.
>
>
>
Hi Alan,

I took the WatchDir sample code and modified it as follows:
- The directory parsing and event loop runs in a separate thread, which 
allows me to delete files and directories programmatically.
- Added a few extra console outputs to better catch what is going on.
- In the event loop, added a section to catch the particular condition 
in which we are interested.

The bottom line of what I found is that the behavior changes depending 
on whether the deleted subdirectory is empty or not. To reproduce:

1: Change the code to create a parent directory to your liking. This is 
in line 241: final Path dir = Paths.get("c:\\watchtest");

2: Run the code without other modification. It will create the before 
mentioned directory, plus a subdirectory, plus two files in that 
subdirectory, then waits for something to happen.

3: Manually delete "subdir". The output is:
       Scanning c:\watchtest ...
       register: c:\watchtest
       register: c:\watchtest\subdir
       Done with registering directories.
       ENTRY_DELETE: c:\watchtest\subdir
       c:\watchtest\subdir does not exist... ... but key is valid.
       ENTRY_MODIFY: c:\watchtest\subdir\file1.txt
       ENTRY_MODIFY: c:\watchtest\subdir\file2.txt

4: Kill the thread and start over.

5: Manually delete first the two files in subdir, and then subdir. The 
output is:
       Scanning c:\watchtest ...
       register: c:\watchtest
       register: c:\watchtest\subdir
       Done with registering directories.
       ENTRY_DELETE: c:\watchtest\subdir\file1.txt
       ENTRY_MODIFY: c:\watchtest\subdir
       ENTRY_DELETE: c:\watchtest\subdir\file2.txt
       ENTRY_MODIFY: c:\watchtest\subdir
       Removing watch key for c:\watchtest\subdir.
       ENTRY_DELETE: c:\watchtest\subdir

6: You can kill the watch thread by deleting directory "watchtest".

7: You can remove the comments from lines 265 to 273 to automate step 5.

For me it is important to make step 3 above work correctly. This is what 
a user would do when a directory needs to be removed manually.

I hope this helps. Please let me know how I can be of further help.
Wolfgang.

package test;

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

import java.io.IOException;

import java.nio.file.FileRef;
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.StandardWatchEventKind;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.Attributes;
import java.nio.file.attribute.BasicFileAttributes;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

/**
  * Example to watch a directory (or tree) for changes to files.
  */

public class
WatchDir {
     /**
      * Thread to process watch events.
      */
     private static class
     WatchThread
     extends Thread {
         private final Path    dir;
         private final boolean recursive;

         public
         WatchThread(final Path dir, final boolean recursive) {
             this.dir       = dir;
             this.recursive = recursive;
         }

         /** @see  java.lang.Thread#run() */
         @Override
         public void
         run() {
             try {
                 new WatchDir(this.dir, this.recursive).processEvents();
             }
             catch (final IOException ignore) {
             }
         }
     }

     private final WatchService        watcher;
     private final Map<WatchKey, Path> keys;
     private final boolean             recursive;
     private final Path                dir;
     private boolean                   trace = true;

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

     /**
      * Register the given directory with the WatchService
      */
     void
     register(final Path dir)
     throws IOException {
         final WatchKey key =
            dir.register(this.watcher,
                         StandardWatchEventKind.ENTRY_CREATE,
                         StandardWatchEventKind.ENTRY_DELETE,
                         StandardWatchEventKind.ENTRY_MODIFY);
         if (this.trace) {
             final FileRef prev = this.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);
                 }
             }
         }
         this.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(final 
Path                dir,
                                                    final 
BasicFileAttributes attrs)
                                  throws IOException {
                                     WatchDir.this.register(dir);
                                     return FileVisitResult.CONTINUE;
                                      }
                                  });
     }

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

         if (recursive) {
             System.out.format("Scanning %s ...\n", dir);
             this.registerAll(dir);
             System.out.println("Done with registering directories.");
         }
         else {
             this.register(dir);
         }

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

     /**
      * Process all events for keys queued to the watcher
      */
     void
     processEvents() {
         Path dir = null;
         for (;;) {
             // wait for key to be signalled
             final WatchKey key;
             try {
                 key = this.watcher.take();
             }
             catch (final InterruptedException x) {
                 System.out.println("Thread interrupted.");
                 return;
             }

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

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

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

                 // Context for directory entry event is the file name 
of entry
                 final WatchEvent<Path> ev    = WatchDir.cast(event);
                 final Path             name  = ev.context();
                 final Path             child = dir.resolve(name);

                 // print out event
                 System.out.format("%s: %s\n",
                                   event.kind().name(),
                                   child);

                 // if directory is created, and watching recursively, then
                 // register it and its sub-directories
                 if (this.recursive && (kind == 
StandardWatchEventKind.ENTRY_CREATE)) {
                     try {
                         if (Attributes.readBasicFileAttributes(child, 
NOFOLLOW_LINKS)
                             .isDirectory()) {
                             this.registerAll(child);
                         }
                     }
                     catch (final IOException x) {
                         // ignore to keep sample readbale
                     }
                 }

                 // if event is DELETE ...
                 if (kind == StandardWatchEventKind.ENTRY_DELETE) {
                     WatchKey ckey = null;
                     for (final Entry<WatchKey, Path> e : 
this.keys.entrySet()) {
                         if (e.getValue().equals(child)) {
                             ckey = e.getKey();
                             break;
                         }
                     }
                     // ... and child is a directory and it is 
effectively deleted.
                     if ((ckey != null) && child.notExists()) {
                         System.out.format("%s does not exist... ", child);
                         if (ckey.isValid()) {
                             System.out.println("... but key is valid.");
                         }
                         else {
                             System.out.println("... and key is 
cancelled.");
                         }
                     }
                 }
             }

             // reset key and remove from set if directory no longer 
accessible
             final boolean valid = key.reset();
             if (!valid) {
                 System.out.format("Removing watch key for %s.\n", dir);
                 this.keys.remove(key);

                 // all directories are inaccessible
                 if (this.keys.isEmpty()) {
                     break;
                 }
             }
         }
     }

     /**
      * TODO method description
      *
      * @param   args  TODO parameter description
      *
      * @throws  IOException  TODO exception description
      */
     public static void
     main(final String[] args)
     throws IOException {
         // parse arguments
         final boolean recursive = true;
         final Path    dir       = Paths.get("c:\\watchtest");
         try {
             dir.createDirectory();
         }
         catch (final Exception ignore) {
         }
         //
         final WatchThread t = new WatchThread(dir, recursive);
         t.start();
         //
         final Path subdir = dir.resolve(Paths.get("subdir"));
         subdir.createDirectory();
         final Path file1 = subdir.resolve(Paths.get("file1.txt"));
         file1.createFile();
         final Path file2 = subdir.resolve(Paths.get("file2.txt"));
         file2.createFile();
         //
         try {
             Thread.sleep(200);
         }
         catch (final InterruptedException e) {
         }
         //
         // Comment out this section vvvv
//        file1.delete();
//        file2.delete();
//        subdir.delete();
//        try {
//            Thread.sleep(500);
//        }
//        catch (final InterruptedException e) {
//        }
//        t.interrupt();
         // Comment out this section ^^^^

     }

}




More information about the nio-dev mailing list