Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for path contexts + misc improvements and fixes #58

Merged
merged 10 commits into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"?>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a maven build?

I'm happy to discuss alternative build systems, but I think it makes more sense to discuss this in a separate issue before we make any changes. Ideally we should choose a single build system and stick with it for everything, and not try to build the project with multiple build systems.

Copy link
Contributor Author

@mdproctor mdproctor Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't, you can exclude it. It's just easier for me, as it was what I was familiar with, and works well in my IDE. I put it there, as it may make things easier for other people too, who don't have all the SBT/Scala stuff.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I see your point. It just hard to make sure things are consistent between the maven and sbt builds, so it ultimately ends up creating more issues for us to fix.

I am eventually thinking about splitting out the better-files extension into a completely separate library (with separate release cycle) so maybe that's the right time to consider the maven stuff.

<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>
27 changes: 22 additions & 5 deletions core/src/main/java/io/methvin/watcher/DirectoryChangeEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ public WatchEvent.Kind<?> getWatchEventKind() {
private final EventType eventType;
private final Path path;
private final int count;
private final Path rootPath;

public DirectoryChangeEvent(EventType eventType, Path path, int count) {
public DirectoryChangeEvent(EventType eventType, Path path, int count, Path rootPath) {
this.eventType = eventType;
this.path = path;
this.count = count;
this.rootPath = rootPath;
}

public EventType eventType() {
Expand All @@ -66,17 +68,30 @@ public int count() {
return count;
}

public Path rootPath() {
return rootPath;
}

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

DirectoryChangeEvent that = (DirectoryChangeEvent) o;
return count == that.count && eventType == that.eventType && Objects.equals(path, that.path);

return count == that.count
&& eventType == that.eventType
&& Objects.equals(path, that.path)
&& Objects.equals(rootPath, that.rootPath);
}

@Override
public int hashCode() {
return Objects.hash(eventType, path, count);
return Objects.hash(eventType, path, count, rootPath);
}

@Override
Expand All @@ -88,6 +103,8 @@ public String toString() {
+ path
+ ", count="
+ count
+ ", rootPath="
+ rootPath
+ '}';
}
}
71 changes: 49 additions & 22 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,10 @@
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -144,9 +146,8 @@ public static Builder builder() {
}

private final Logger logger;

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

private volatile boolean closed;

public DirectoryWatcher(
List<Path> paths,
DirectoryChangeListener listener,
WatchService watchService,
FileHasher fileHasher,
Logger logger)
throws IOException {
this.paths = paths;
this.closed = false;
this.registeredPathToRootPath = new HashMap<>();
this.listener = listener;
this.watchService = watchService;
this.isMac = watchService instanceof MacOSXListeningWatchService;
Expand All @@ -173,7 +177,7 @@ public DirectoryWatcher(
this.logger = logger;

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

Expand Down Expand Up @@ -225,19 +229,21 @@ public void watch() {
throw new IllegalStateException(
"WatchService returned key [" + key + "] but it was not found in keyRoots!");
}
Path registeredPath = keyRoots.get(key);
Path rootPath = registeredPathToRootPath.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(EventType.OVERFLOW, childPath, count, rootPath);
} 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, rootPath);
}
/*
* Our custom Mac service sends subdirectory changes but the Windows/Linux do not.
Expand All @@ -246,11 +252,11 @@ public void watch() {
if (!isMac) {
PathUtils.recursiveVisitFiles(
childPath,
dir -> notifyCreateEvent(dir, count),
file -> notifyCreateEvent(file, count));
dir -> notifyCreateEvent(dir, count, rootPath),
file -> notifyCreateEvent(file, count, rootPath));
}
}
notifyCreateEvent(childPath, count);
notifyCreateEvent(childPath, count, rootPath);
} else if (kind == ENTRY_MODIFY) {
if (fileHasher != null || Files.isDirectory(childPath)) {
/*
Expand All @@ -267,19 +273,19 @@ public void watch() {

if (newHash != null && !newHash.equals(existingHash)) {
pathHashes.put(childPath, newHash);
listener.onEvent(new DirectoryChangeEvent(EventType.MODIFY, childPath, count));
onEvent(EventType.MODIFY, childPath, count, rootPath);
} 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(EventType.MODIFY, childPath, count, rootPath);
}
} 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(EventType.DELETE, childPath, count, rootPath);
}
} catch (Exception e) {
logger.debug("DirectoryWatcher got an exception while watching!", e);
Expand All @@ -290,14 +296,28 @@ 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 registeredPathToRootPath maps
registeredPathToRootPath.remove(registeredPath);

// 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(EventType eventType, Path childPath, int count, Path rootPath)
throws IOException {
listener.onEvent(new DirectoryChangeEvent(eventType, childPath, count, rootPath));
}

public DirectoryChangeListener getListener() {
Expand All @@ -306,30 +326,36 @@ public DirectoryChangeListener getListener() {

public void close() throws IOException {
watchService.close();
closed = true;
}

private void registerAll(final Path start) throws IOException {
public boolean isClosed() {
return closed;
}

private void registerAll(final Path start, final Path 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, Path context)
throws IOException {
logger.debug("Registering [{}].", directory);
Watchable watchable = isMac ? new WatchablePath(directory) : directory;
WatchEvent.Modifier[] modifiers =
Expand All @@ -340,9 +366,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);
registeredPathToRootPath.put(directory, context);
}

private void notifyCreateEvent(Path path, int count) throws IOException {
private void notifyCreateEvent(Path path, int count, Path context) throws IOException {
if (fileHasher != null || Files.isDirectory(path)) {
HashCode newHash = PathUtils.hash(fileHasher, path);
if (newHash == null) {
Expand All @@ -353,19 +380,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(EventType.CREATE, path, count, 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(EventType.CREATE, path, count, context);
pathHashes.put(path, newHash);
}
}
} else {
logger.debug("{} [{}]", EventType.CREATE, path);
listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count));
onEvent(EventType.CREATE, path, count, context);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -149,4 +153,9 @@ final void signalEvent(WatchEvent.Kind<Path> kind, Path context) {
post(new Event<>(kind, 1, context));
signal();
}

@Override
public String toString() {
return "AbstractWatchKey{" + "watchable=" + watchable + ", valid=" + valid.get() + '}';
}
}
Loading