Skip to content

Commit

Permalink
Bug
Browse files Browse the repository at this point in the history
-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.
  • Loading branch information
Mark Proctor committed Nov 13, 2020
1 parent f2df265 commit 2b231b0
Show file tree
Hide file tree
Showing 10 changed files with 403 additions and 51 deletions.
67 changes: 67 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.methvin</groupId>
<artifactId>directory-watcher-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>directory-watcher-core</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

</project>
12 changes: 10 additions & 2 deletions core/src/main/java/io/methvin/watcher/DirectoryChangeEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import java.nio.file.WatchEvent;
import java.util.Objects;

public final class DirectoryChangeEvent {
public final class DirectoryChangeEvent<C> {
public enum EventType {

/* A new file was created */
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
Expand All @@ -88,6 +94,8 @@ public String toString() {
+ path
+ ", count="
+ count
+ ", context="
+ context
+ '}';
}
}
96 changes: 72 additions & 24 deletions core/src/main/java/io/methvin/watcher/DirectoryWatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,14 +38,15 @@
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;

public class DirectoryWatcher {
public class DirectoryWatcher<C> {

/**
* 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<C> {
private List<Path> paths = Collections.emptyList();
private Map<Path, C> contexts = new HashMap<>();
private DirectoryChangeListener listener = (event -> {});
private Logger logger = null;
private FileHasher fileHasher = FileHasher.DEFAULT_FILE_HASHER;
Expand All @@ -56,6 +60,14 @@ public Builder paths(List<Path> paths) {
return this;
}


/** Set multiple paths to watch with a context per path. */
public Builder paths(Map<Path, C> 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));
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -146,7 +158,8 @@ public static Builder builder() {
private final Logger logger;

private final WatchService watchService;
private final List<Path> paths;
private Map<Path, C> contexts;
private Map<Path, C> registeredContexts;
private final boolean isMac;
private final DirectoryChangeListener listener;
private final Map<Path, HashCode> pathHashes;
Expand All @@ -156,14 +169,19 @@ public static Builder builder() {
private Boolean fileTreeSupported = null;
private FileHasher fileHasher;

private volatile boolean closed;

public DirectoryWatcher(
List<Path> paths,
Map<Path, C> 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;
Expand All @@ -173,7 +191,7 @@ public DirectoryWatcher(
this.logger = logger;

for (Path path : paths) {
registerAll(path);
registerAll(path, contexts.get(path));
}
}

Expand Down Expand Up @@ -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.
Expand All @@ -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)) {
/*
Expand All @@ -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);
Expand All @@ -290,46 +310,73 @@ 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.");
break;
}
}
}
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<Path, C> getContexts() {
return contexts;
}

public Map<Path, C> 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) {
// UnsupportedOperationException should only happen if FILE_TREE is unsupported
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 =
Expand All @@ -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) {
Expand All @@ -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);
}
}
}
Loading

0 comments on commit 2b231b0

Please sign in to comment.