From 2b231b02200e132ee10fd7263873120f2e5ff6f5 Mon Sep 17 00:00:00 2001 From: Mark Proctor Date: Thu, 12 Nov 2020 22:45:46 +0000 Subject: [PATCH] Bug -Carbon returns real paths, this will not map added paths which are symbolic. To address this it turns the real path back into the user provided path. -Fixed some tests, by making them await longer Improvements -When underlying root paths were deleted, it would not detect and close the Carbon deamon thread or clean up the paths or invalidate the WatchKey. This is now addressed -Can now check if DirectoryWatcher is open or closed. -Added pom.xml so maven sort of works, but it's a quick hack and can be improved.- Feature -Allow a context value to be associated with a given subtree (i.e. from root path and all child paths), and provided as part of the Event. This avoids having to search for matching roots, to lookup a context value. --- core/pom.xml | 67 ++++++++ .../methvin/watcher/DirectoryChangeEvent.java | 12 +- .../io/methvin/watcher/DirectoryWatcher.java | 96 ++++++++--- .../watchservice/AbstractWatchKey.java | 11 ++ .../MacOSXListeningWatchService.java | 56 ++++++- .../methvin/watchservice/MacOSXWatchKey.java | 19 +-- .../methvin/watchservice/jna/CarbonAPI.java | 2 +- .../DirectoryWatcherOnDiskTest.java | 150 +++++++++++++++++- .../watchservice/DirectoryWatcherTest.java | 2 +- pom.xml | 39 +++++ 10 files changed, 403 insertions(+), 51 deletions(-) create mode 100644 core/pom.xml create mode 100644 pom.xml diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..bb9363d --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + + io.methvin + directory-watcher-parent + 1.0-SNAPSHOT + + + directory-watcher-core + 1.0-SNAPSHOT + jar + + + + junit + junit + 4.13 + test + + + org.codehaus.plexus + plexus-utils + 3.3.0 + + + com.google.guava + guava + 30.0-jre + + + net.java.dev.jna + jna + 4.5.0 + + + org.awaitility + awaitility + 4.0.3 + test + + + commons-io + commons-io + 2.6 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + + \ No newline at end of file diff --git a/core/src/main/java/io/methvin/watcher/DirectoryChangeEvent.java b/core/src/main/java/io/methvin/watcher/DirectoryChangeEvent.java index 4b16f25..4df9b44 100644 --- a/core/src/main/java/io/methvin/watcher/DirectoryChangeEvent.java +++ b/core/src/main/java/io/methvin/watcher/DirectoryChangeEvent.java @@ -18,7 +18,7 @@ import java.nio.file.WatchEvent; import java.util.Objects; -public final class DirectoryChangeEvent { +public final class DirectoryChangeEvent { public enum EventType { /* A new file was created */ @@ -47,11 +47,13 @@ public WatchEvent.Kind getWatchEventKind() { private final EventType eventType; private final Path path; private final int count; + private final C context; - public DirectoryChangeEvent(EventType eventType, Path path, int count) { + public DirectoryChangeEvent(EventType eventType, Path path, int count, C context) { this.eventType = eventType; this.path = path; this.count = count; + this.context = context; } public EventType eventType() { @@ -66,6 +68,10 @@ public int count() { return count; } + public C context() { + return context; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -88,6 +94,8 @@ public String toString() { + path + ", count=" + count + + ", context=" + + context + '}'; } } diff --git a/core/src/main/java/io/methvin/watcher/DirectoryWatcher.java b/core/src/main/java/io/methvin/watcher/DirectoryWatcher.java index 905db21..6fea5fc 100644 --- a/core/src/main/java/io/methvin/watcher/DirectoryWatcher.java +++ b/core/src/main/java/io/methvin/watcher/DirectoryWatcher.java @@ -24,8 +24,11 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.*; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -35,14 +38,15 @@ import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import static java.nio.file.StandardWatchEventKinds.*; -public class DirectoryWatcher { +public class DirectoryWatcher { /** * A builder for a {@link DirectoryWatcher}. Use {@code DirectoryWatcher.builder()} to get a new * instance. */ - public static final class Builder { + public static final class Builder { private List paths = Collections.emptyList(); + private Map contexts = new HashMap<>(); private DirectoryChangeListener listener = (event -> {}); private Logger logger = null; private FileHasher fileHasher = FileHasher.DEFAULT_FILE_HASHER; @@ -56,6 +60,14 @@ public Builder paths(List paths) { return this; } + + /** Set multiple paths to watch with a context per path. */ + public Builder paths(Map contexts) { + paths(new ArrayList<>(contexts.keySet())); + this.contexts = contexts; + return this; + } + /** Set a single path to watch. */ public Builder path(Path path) { return paths(Collections.singletonList(path)); @@ -114,7 +126,7 @@ public DirectoryWatcher build() throws IOException { if (logger == null) { staticLogger(); } - return new DirectoryWatcher(paths, listener, watchService, fileHasher, logger); + return new DirectoryWatcher(paths, contexts, listener, watchService, fileHasher, logger); } private Builder osDefaultWatchService() throws IOException { @@ -146,7 +158,8 @@ public static Builder builder() { private final Logger logger; private final WatchService watchService; - private final List paths; + private Map contexts; + private Map registeredContexts; private final boolean isMac; private final DirectoryChangeListener listener; private final Map pathHashes; @@ -156,14 +169,19 @@ public static Builder builder() { private Boolean fileTreeSupported = null; private FileHasher fileHasher; + private volatile boolean closed; + public DirectoryWatcher( List paths, + Map contexts, DirectoryChangeListener listener, WatchService watchService, FileHasher fileHasher, Logger logger) throws IOException { - this.paths = paths; + this.closed = false; + this.contexts = contexts; + this.registeredContexts = new HashMap<>(contexts); this.listener = listener; this.watchService = watchService; this.isMac = watchService instanceof MacOSXListeningWatchService; @@ -173,7 +191,7 @@ public DirectoryWatcher( this.logger = logger; for (Path path : paths) { - registerAll(path); + registerAll(path, contexts.get(path)); } } @@ -225,19 +243,21 @@ public void watch() { throw new IllegalStateException( "WatchService returned key [" + key + "] but it was not found in keyRoots!"); } + Path registeredPath = keyRoots.get(key); + C context = registeredContexts.get(registeredPath); Path childPath = eventPath == null ? null : keyRoots.get(key).resolve(eventPath); logger.debug("{} [{}]", kind, childPath); /* * If a directory is created, and we're watching recursively, then register it and its sub-directories. */ if (kind == OVERFLOW) { - listener.onEvent(new DirectoryChangeEvent(EventType.OVERFLOW, childPath, count)); + onEvent(count, childPath, EventType.OVERFLOW, context); } else if (eventPath == null) { throw new IllegalStateException("WatchService returned a null path for " + kind.name()); } else if (kind == ENTRY_CREATE) { if (Files.isDirectory(childPath, NOFOLLOW_LINKS)) { if (!Boolean.TRUE.equals(fileTreeSupported)) { - registerAll(childPath); + registerAll(childPath, context); } /* * Our custom Mac service sends subdirectory changes but the Windows/Linux do not. @@ -246,11 +266,11 @@ public void watch() { if (!isMac) { PathUtils.recursiveVisitFiles( childPath, - dir -> notifyCreateEvent(dir, count), - file -> notifyCreateEvent(file, count)); + dir -> notifyCreateEvent(dir, count, context), + file -> notifyCreateEvent(file, count, context)); } } - notifyCreateEvent(childPath, count); + notifyCreateEvent(childPath, count, context); } else if (kind == ENTRY_MODIFY) { if (fileHasher != null || Files.isDirectory(childPath)) { /* @@ -267,19 +287,19 @@ public void watch() { if (newHash != null && !newHash.equals(existingHash)) { pathHashes.put(childPath, newHash); - listener.onEvent(new DirectoryChangeEvent(EventType.MODIFY, childPath, count)); + onEvent(count, childPath, EventType.MODIFY, context); } else if (newHash == null) { logger.debug( "Failed to hash modified file [{}]. It may have been deleted.", childPath); } } else { - listener.onEvent(new DirectoryChangeEvent(EventType.MODIFY, childPath, count)); + onEvent(count, childPath, EventType.MODIFY, context); } } else if (kind == ENTRY_DELETE) { // we cannot tell if the deletion was on file or folder because path points nowhere // (file/folder was deleted) pathHashes.entrySet().removeIf(e -> e.getKey().startsWith(childPath)); - listener.onEvent(new DirectoryChangeEvent(EventType.DELETE, childPath, count)); + onEvent(count, childPath, EventType.DELETE, context); } } catch (Exception e) { logger.debug("DirectoryWatcher got an exception while watching!", e); @@ -290,7 +310,12 @@ public void watch() { if (!valid) { logger.debug("WatchKey for [{}] no longer valid; removing.", key.watchable()); // remove the key from the keyRoots - keyRoots.remove(key); + Path registeredPath = keyRoots.remove(key); + + // Also remove from the context maps + registeredContexts.remove(registeredPath); + contexts.remove(registeredPath); // it may not be in this one, if it's a nested path. But quicker to remove, than check and remove. + // if there are no more keys left to watch, we can break out if (keyRoots.isEmpty()) { logger.debug("No more directories left to watch; terminating watcher."); @@ -298,21 +323,43 @@ public void watch() { } } } + try { + close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void onEvent(int count, Path childPath, EventType eventType, C context) throws IOException { + listener.onEvent(new DirectoryChangeEvent(eventType, childPath, count, context)); } public DirectoryChangeListener getListener() { return listener; } + public Map getContexts() { + return contexts; + } + + public Map getRegisteredContexts() { + return registeredContexts; + } + public void close() throws IOException { watchService.close(); + closed = true; + } + + public boolean isClosed() { + return closed; } - private void registerAll(final Path start) throws IOException { + private void registerAll(final Path start, final C context) throws IOException { if (!Boolean.FALSE.equals(fileTreeSupported)) { // Try using FILE_TREE modifier since we aren't certain that it's unsupported try { - register(start, true); + register(start, true, context); // We didn't get an UnsupportedOperationException so assume FILE_TREE is supported fileTreeSupported = true; } catch (UnsupportedOperationException e) { @@ -320,16 +367,16 @@ private void registerAll(final Path start) throws IOException { logger.debug("Assuming ExtendedWatchEventModifier.FILE_TREE is not supported", e); fileTreeSupported = false; // If we failed to use the FILE_TREE modifier, try again without - registerAll(start); + registerAll(start, context); } } else { // Since FILE_TREE is unsupported, register root directory and sub-directories - PathUtils.recursiveVisitFiles(start, dir -> register(dir, false), file -> {}); + PathUtils.recursiveVisitFiles(start, dir -> register(dir, false, context), file -> {}); } } // Internal method to be used by registerAll - private void register(Path directory, boolean useFileTreeModifier) throws IOException { + private void register(Path directory, boolean useFileTreeModifier, C context) throws IOException { logger.debug("Registering [{}].", directory); Watchable watchable = isMac ? new WatchablePath(directory) : directory; WatchEvent.Modifier[] modifiers = @@ -340,9 +387,10 @@ private void register(Path directory, boolean useFileTreeModifier) throws IOExce new WatchEvent.Kind[] {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}; WatchKey watchKey = watchable.register(watchService, kinds, modifiers); keyRoots.put(watchKey, directory); + registeredContexts.put(directory, context); } - private void notifyCreateEvent(Path path, int count) throws IOException { + private void notifyCreateEvent(Path path, int count, C context) throws IOException { if (fileHasher != null || Files.isDirectory(path)) { HashCode newHash = PathUtils.hash(fileHasher, path); if (newHash == null) { @@ -353,19 +401,19 @@ private void notifyCreateEvent(Path path, int count) throws IOException { } else { logger.debug("Failed to hash created file [{}]. It may be locked.", path); logger.debug("{} [{}]", EventType.CREATE, path); - listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count)); + onEvent(count, path, EventType.CREATE, context); } } else { // Notify for the file create if not already notified if (!pathHashes.containsKey(path)) { logger.debug("{} [{}]", EventType.CREATE, path); - listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count)); + onEvent(count, path, EventType.CREATE, context); pathHashes.put(path, newHash); } } } else { logger.debug("{} [{}]", EventType.CREATE, path); - listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count)); + onEvent(count, path, EventType.CREATE, context); } } } diff --git a/core/src/main/java/io/methvin/watchservice/AbstractWatchKey.java b/core/src/main/java/io/methvin/watchservice/AbstractWatchKey.java index 152f29b..e1b5fd2 100644 --- a/core/src/main/java/io/methvin/watchservice/AbstractWatchKey.java +++ b/core/src/main/java/io/methvin/watchservice/AbstractWatchKey.java @@ -70,6 +70,10 @@ AbstractWatchKey.State state() { return state.get(); } + AbstractWatchService watchService() { + return this.watcher; + } + /** Gets whether or not this key is subscribed to the given type of event. */ public boolean subscribesTo(WatchEvent.Kind eventType) { return subscribedTypes.contains(eventType); @@ -149,4 +153,11 @@ final void signalEvent(WatchEvent.Kind kind, Path context) { post(new Event<>(kind, 1, context)); signal(); } + + @Override public String toString() { + return "AbstractWatchKey{" + + "watchable=" + watchable + + ", valid=" + valid.get() + + '}'; + } } diff --git a/core/src/main/java/io/methvin/watchservice/MacOSXListeningWatchService.java b/core/src/main/java/io/methvin/watchservice/MacOSXListeningWatchService.java index 66f5cb5..8d44ce8 100644 --- a/core/src/main/java/io/methvin/watchservice/MacOSXListeningWatchService.java +++ b/core/src/main/java/io/methvin/watchservice/MacOSXListeningWatchService.java @@ -23,6 +23,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.WatchEvent; import java.util.*; import java.util.concurrent.atomic.AtomicLong; @@ -114,21 +115,22 @@ public MacOSXListeningWatchService() { private final int kFSEventStreamCreateFlagFileEvents = 0x00000010; @Override - public AbstractWatchKey register( + public synchronized AbstractWatchKey register( WatchablePath watchable, Iterable> events) throws IOException { checkOpen(); - final MacOSXWatchKey watchKey = new MacOSXWatchKey(this, events, queueSize); + final MacOSXWatchKey watchKey = new MacOSXWatchKey(this, watchable, events, queueSize); final Path file = watchable.getFile().toAbsolutePath(); // if we are already watching a parent of this directory, do nothing. for (Path watchedPath : pathsWatching) { if (file.startsWith(watchedPath)) return watchKey; } + final Map hashCodeMap = PathUtils.createHashCodeMap(file, fileHasher); final Pointer[] values = {CFStringRef.toCFString(file.toString()).getPointer()}; final CFArrayRef pathsToWatch = CarbonAPI.INSTANCE.CFArrayCreate(null, values, CFIndex.valueOf(1), null); - final CarbonAPI.FSEventStreamCallback callback = - new MacOSXListeningCallback(watchKey, fileHasher, hashCodeMap); + final MacOSXListeningCallback callback = + new MacOSXListeningCallback(watchKey, fileHasher, hashCodeMap, file.toString(), file.toRealPath().toString()); callbackList.add(callback); int flags = kFSEventStreamCreateFlagNoDefer; if (fileLevelEvents) { @@ -145,6 +147,8 @@ public AbstractWatchKey register( flags); final CFRunLoopThread thread = new CFRunLoopThread(streamRef, file.toFile()); + callback.setRunLoopThread(thread); + thread.setDaemon(true); thread.start(); threadList.add(thread); @@ -189,7 +193,7 @@ public void close() { } @Override - public void close() { + public synchronized void close() { super.close(); threadList.forEach(CFRunLoopThread::close); threadList.clear(); @@ -197,16 +201,43 @@ public void close() { pathsWatching.clear(); } + public synchronized void close(CFRunLoopThread runLoopThread, CarbonAPI.FSEventStreamCallback callback, Path path) { + threadList.remove(runLoopThread); + callbackList.remove(callback); + pathsWatching.remove(path); + + new Thread(() -> { + // I put this in it's own thread, as I don't fully understand the sync interactions between components in this class + // and I wanted to be sure to avoid a deadlock. + runLoopThread.close(); + + }).start(); + } + private static class MacOSXListeningCallback implements CarbonAPI.FSEventStreamCallback { private final MacOSXWatchKey watchKey; private final Map hashCodeMap; private final FileHasher fileHasher; + private final String realPath; + private final String absPath; + + private CFRunLoopThread runLoopThread; private MacOSXListeningCallback( - MacOSXWatchKey watchKey, FileHasher fileHasher, Map hashCodeMap) { + MacOSXWatchKey watchKey, FileHasher fileHasher, Map hashCodeMap, String absPath, String realPath) { this.watchKey = watchKey; this.hashCodeMap = hashCodeMap; this.fileHasher = fileHasher; + this.realPath = realPath; + this.absPath = absPath; + } + + public CFRunLoopThread getRunLoopThread() { + return runLoopThread; + } + + public void setRunLoopThread(CFRunLoopThread runLoopThread) { + this.runLoopThread = runLoopThread; } @Override @@ -220,6 +251,7 @@ public void invoke( final int length = numEvents.intValue(); for (String fileName : eventPaths.getStringArray(0, length)) { + fileName = absPath + fileName.substring(realPath.length()); /* * Note: If file-level events are enabled, fileName will be an individual file so we usually won't recurse. */ @@ -254,11 +286,21 @@ public void invoke( } } - for (Path file : findDeletedFiles(fileName, filesOnDisk)) { + List deletedPaths = findDeletedFiles(fileName, filesOnDisk); + + if (hashCodeMap.isEmpty()) { + // all underlying paths are gone, so stop this service and cancel the key + watchKey.watchService().close(runLoopThread, this, Paths.get(absPath)); + watchKey.cancel(); + } + + for (Path file : deletedPaths) { if (watchKey.isReportDeleteEvents()) { watchKey.signalEvent(ENTRY_DELETE, file); } } + + } } diff --git a/core/src/main/java/io/methvin/watchservice/MacOSXWatchKey.java b/core/src/main/java/io/methvin/watchservice/MacOSXWatchKey.java index fd4ca02..9a25944 100644 --- a/core/src/main/java/io/methvin/watchservice/MacOSXWatchKey.java +++ b/core/src/main/java/io/methvin/watchservice/MacOSXWatchKey.java @@ -19,16 +19,15 @@ import java.util.concurrent.atomic.AtomicBoolean; class MacOSXWatchKey extends AbstractWatchKey { - private final AtomicBoolean cancelled = new AtomicBoolean(false); private final boolean reportCreateEvents; private final boolean reportModifyEvents; private final boolean reportDeleteEvents; public MacOSXWatchKey( - AbstractWatchService macOSXWatchService, - Iterable> events, - int queueSize) { - super(macOSXWatchService, null, events, queueSize); + AbstractWatchService macOSXWatchService, + WatchablePath watchable, Iterable> events, + int queueSize) { + super(macOSXWatchService, watchable, events, queueSize); boolean reportCreateEvents = false; boolean reportModifyEvents = false; boolean reportDeleteEvents = false; @@ -47,9 +46,8 @@ public MacOSXWatchKey( this.reportModifyEvents = reportModifyEvents; } - @Override - public void cancel() { - cancelled.set(true); + MacOSXListeningWatchService watchService() { + return (MacOSXListeningWatchService) super.watchService(); } public boolean isReportCreateEvents() { @@ -63,9 +61,4 @@ public boolean isReportModifyEvents() { public boolean isReportDeleteEvents() { return reportDeleteEvents; } - - @Override - public Watchable watchable() { - return null; - } } diff --git a/core/src/main/java/io/methvin/watchservice/jna/CarbonAPI.java b/core/src/main/java/io/methvin/watchservice/jna/CarbonAPI.java index f4c855e..ed6d62c 100644 --- a/core/src/main/java/io/methvin/watchservice/jna/CarbonAPI.java +++ b/core/src/main/java/io/methvin/watchservice/jna/CarbonAPI.java @@ -17,7 +17,7 @@ public interface CarbonAPI extends Library { // sbt uses JNA 4.5.0, so use JNA 4.x's API - CarbonAPI INSTANCE = Native.loadLibrary("Carbon", CarbonAPI.class); + CarbonAPI INSTANCE = (CarbonAPI) Native.loadLibrary("Carbon", CarbonAPI.class); CFArrayRef CFArrayCreate( CFAllocatorRef allocator, // always set to Pointer.NULL diff --git a/core/src/test/java/io/methvin/watchservice/DirectoryWatcherOnDiskTest.java b/core/src/test/java/io/methvin/watchservice/DirectoryWatcherOnDiskTest.java index 1238acb..3463407 100644 --- a/core/src/test/java/io/methvin/watchservice/DirectoryWatcherOnDiskTest.java +++ b/core/src/test/java/io/methvin/watchservice/DirectoryWatcherOnDiskTest.java @@ -1,10 +1,12 @@ package io.methvin.watchservice; +import com.google.common.util.concurrent.UncheckedExecutionException; import io.methvin.watcher.DirectoryChangeEvent; import io.methvin.watcher.DirectoryChangeListener; import io.methvin.watcher.DirectoryWatcher; import org.apache.commons.io.FileUtils; import org.junit.After; +import org.junit.Assert; import org.junit.Assume; import org.junit.Before; import org.junit.Test; @@ -12,13 +14,19 @@ import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import java.io.UncheckedIOException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; +import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -29,6 +37,7 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; import static org.awaitility.Awaitility.await; +import static org.junit.Assert.*; public class DirectoryWatcherOnDiskTest { @@ -74,7 +83,7 @@ public void copySubDirectoryFromOutsideWithHashing() } @Test - public void copySubDirectoryFromOutsideTwiceHashing() throws IOException { + public void copySubDirectoryFromOutsideTwiceHashing() throws IOException, InterruptedException { this.watcher = DirectoryWatcher.builder() .path(this.tmpDir) @@ -86,6 +95,8 @@ public void copySubDirectoryFromOutsideTwiceHashing() throws IOException { copyAndVerifyEvents(structure); await().atMost(5, TimeUnit.SECONDS).until(() -> tmpDir.toFile().listFiles().length == 0); + await().atMost(5, TimeUnit.SECONDS).until(() -> this.recorder.events.size() == 6); + // reset recorder this.recorder.events.clear(); copyAndVerifyEvents(structure); @@ -101,10 +112,17 @@ public void copySubDirectoryFromOutsideTwiceNoHashing() throws IOException { .fileHashing(false) .build(); this.watcher.watchAsync(); + + ensureStill(); + List structure = createFolderStructure(); copyAndVerifyEvents(structure); await().atMost(5, TimeUnit.SECONDS).until(() -> tmpDir.toFile().listFiles().length == 0); + + // it sometimes returns 8 (when run on it's own), sometimes 10 (when run with other tests). + await().atMost(5, TimeUnit.SECONDS).until(() -> this.recorder.events.size() == 8 || this.recorder.events.size() == 10); + // reset recorder this.recorder.events.clear(); copyAndVerifyEvents(structure); @@ -121,8 +139,7 @@ private void copyAndVerifyEvents(List structure) throws IOException { assertTrue( "Create event for the parent directory was notified", existsMatch( - e -> - e.eventType() == DirectoryChangeEvent.EventType.CREATE + e -> e.eventType() == DirectoryChangeEvent.EventType.CREATE && e.path() .getFileName() .equals(structure.get(0).getFileName())))); @@ -370,6 +387,133 @@ public void emitCreateEventWhenFileLockedWithHashing() } } + @Test + public void pathsWithContexts() throws IOException, InterruptedException { + Path p1 = this.tmpDir.resolve("parent1"); + Path p2 = this.tmpDir.resolve("parent2"); + Path p3 = this.tmpDir.resolve("parent3"); + + Map contexts = new HashMap<>(); + contexts.put(p1, "p1"); + contexts.put(p2, "p2"); + contexts.put(p3, "p3"); + + Files.createDirectory(p1); + Files.createDirectory(p2); + Files.createDirectory(p3); + + this.watcher = + DirectoryWatcher.builder() + .paths(contexts) + .listener(this.recorder) + .fileHashing(false) + .build(); + this.watcher.watchAsync(); + assertFalse( this.watcher.isClosed() ); + + ensureStill(); + + assertEquals(3, this.watcher.getContexts().size()); + assertEquals(3, this.watcher.getRegisteredContexts().size()); + + //List paths = new ArrayList<>(); + List paths1 = createStructure2(p1); + List paths2 = createStructure2(p2); + List paths3 = createStructure2(p3); + + await().atMost(3, TimeUnit.SECONDS).until(() -> recorder.events.size() == 33); + checkEventsMatchContext(p1, p2, p3); + this.recorder.events.clear(); + + updatePaths(paths1); + await().atMost(3, TimeUnit.SECONDS).until(() -> recorder.events.size() == 3); + checkEventsMatchContext(p1, p2, p3); + this.recorder.events.clear(); + + updatePaths(paths2, paths3); + await().atMost(3, TimeUnit.SECONDS).until(() -> recorder.events.size() == 6); + checkEventsMatchContext(p1, p2, p3); + this.recorder.events.clear(); + + + FileUtils.deleteDirectory(paths1.get(2).toFile()); + await().atMost(3, TimeUnit.SECONDS).until(() -> recorder.events.size() == 4); + checkEventsMatchContext(p1, p2, p3); + this.recorder.events.clear(); + ; + FileUtils.deleteDirectory(paths1.get(0).toFile()); // deletes the p1 root + await().atMost(3, TimeUnit.SECONDS).until(() -> recorder.events.size() == 2); + checkEventsMatchContext(p1, p2, p3); + assertEquals(2, this.watcher.getContexts().size()); + assertEquals(2, this.watcher.getRegisteredContexts().size()); + this.recorder.events.clear(); + + FileUtils.deleteDirectory(paths2.get(0).toFile()); // deletes the p1 root + await().atMost(3, TimeUnit.SECONDS).until(() -> recorder.events.size() == 6); + checkEventsMatchContext(p1, p2, p3); + assertEquals(1, this.watcher.getContexts().size()); + assertEquals(1, this.watcher.getRegisteredContexts().size()); + this.recorder.events.clear(); + + assertFalse( this.watcher.isClosed() ); + FileUtils.deleteDirectory(paths3.get(0).toFile()); // deletes the p1 root + await().atMost(3, TimeUnit.SECONDS).until(() -> recorder.events.size() == 6); + assertEquals(0, this.watcher.getContexts().size()); + assertEquals(0, this.watcher.getRegisteredContexts().size()); + assertTrue( this.watcher.isClosed() ); + } + + private void ensureStill() { + // I found some tests unstable with regards to counts, unless this was here + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(1)); + while ( recorder.events.size() != 0 ) { + recorder.events.clear(); + Thread.sleep(TimeUnit.SECONDS.toMillis(1)); + } + } catch (InterruptedException e) { + throw new UncheckedExecutionException(e); + } + + } + + private byte counter = 1; + private void updatePaths(List... paths) throws IOException { + Arrays.stream( paths).forEach(path -> { + try { + Files.write(path.get(1), new byte[] {counter++}); + Files.write(path.get(3), new byte[] {counter++}); + Files.write(path.get(5), new byte[] {counter++}); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + private void checkEventsMatchContext(Path p1, Path p2, Path p3) { + this.recorder.events.stream().forEach( e -> { + if ( e.path().startsWith(p1) ) { + assertEquals("p1", e.context()); + } else if ( e.path().startsWith(p2) ) { + assertEquals("p2", e.context()); + } else if ( e.path().startsWith(p3) ) { + assertEquals("p3", e.context()); + } else { + Assert.fail("Path must match one of the p1, p2 or p3 Path subsets"); + } + }); + } + + private List createStructure2(Path parent) throws IOException { + final Path parentFile1 = Files.createTempFile(parent, "parent1-", ".dat"); + final Path childFolder1 = Files.createTempDirectory(parent, "child1-"); + final Path childFile1 = Files.createTempFile(childFolder1, "child1-", ".dat"); + final Path childFolder2 = Files.createTempDirectory(childFolder1, "child2-"); + final Path childFile2 = Files.createTempFile(childFolder2, "child2-", ".dat"); + + return Arrays.asList(parent, parentFile1, childFolder1, childFile1, childFolder2, childFile2); + } + @Test public void testCrash() throws IOException { DirectoryWatcher directoryWatcher1 = DirectoryWatcher.builder().path(this.tmpDir).build(); diff --git a/core/src/test/java/io/methvin/watchservice/DirectoryWatcherTest.java b/core/src/test/java/io/methvin/watchservice/DirectoryWatcherTest.java index a5523ec..e8ee338 100644 --- a/core/src/test/java/io/methvin/watchservice/DirectoryWatcherTest.java +++ b/core/src/test/java/io/methvin/watchservice/DirectoryWatcherTest.java @@ -34,7 +34,7 @@ public void validateOsxWatchKeyOverflow() throws Exception { directory.mkdirs(); MacOSXListeningWatchService service = new MacOSXListeningWatchService(); MacOSXWatchKey key = - new MacOSXWatchKey(service, ImmutableList.of(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY), 16); + new MacOSXWatchKey(service, null, ImmutableList.of(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY), 16); int totalEvents = 0; for (int i = 0; i < 10; i++) { Path toSignal = Paths.get(directory.toPath().toAbsolutePath().toString() + "/" + i); diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0230f7c --- /dev/null +++ b/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + pom + + io.methvin + directory-watcher-parent + 1.0-SNAPSHOT + + + + + org.slf4j + slf4j-api + 1.7.30 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + + + core + + + \ No newline at end of file