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