From d860b15e30fbfb9fe3ebb1d31b2db31ea2ccfa34 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 31 Jan 2025 09:40:44 +0100 Subject: [PATCH 01/17] Rename `JDK...Watcher` to `JDK...Watch` (to align with existing `ActiveWatch` and distinguish from existing `Watcher`) --- src/main/java/engineering/swat/watch/Watcher.java | 14 +++++++------- ...irectoryWatcher.java => JDKDirectoryWatch.java} | 6 +++--- .../jdk/{JDKFileWatcher.java => JDKFileWatch.java} | 8 ++++---- ...atcher.java => JDKRecursiveDirectoryWatch.java} | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) rename src/main/java/engineering/swat/watch/impl/jdk/{JDKDirectoryWatcher.java => JDKDirectoryWatch.java} (94%) rename src/main/java/engineering/swat/watch/impl/jdk/{JDKFileWatcher.java => JDKFileWatch.java} (94%) rename src/main/java/engineering/swat/watch/impl/jdk/{JDKRecursiveDirectoryWatcher.java => JDKRecursiveDirectoryWatch.java} (97%) diff --git a/src/main/java/engineering/swat/watch/Watcher.java b/src/main/java/engineering/swat/watch/Watcher.java index 303bfefc..b3bf3728 100644 --- a/src/main/java/engineering/swat/watch/Watcher.java +++ b/src/main/java/engineering/swat/watch/Watcher.java @@ -37,9 +37,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import engineering.swat.watch.impl.jdk.JDKDirectoryWatcher; -import engineering.swat.watch.impl.jdk.JDKFileWatcher; -import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatcher; +import engineering.swat.watch.impl.jdk.JDKDirectoryWatch; +import engineering.swat.watch.impl.jdk.JDKFileWatch; +import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch; /** *

Watch a path for changes.

@@ -158,26 +158,26 @@ public ActiveWatch start() throws IOException { } switch (scope) { case PATH_AND_CHILDREN: { - var result = new JDKDirectoryWatcher(path, executor, this.eventHandler, false); + var result = new JDKDirectoryWatch(path, executor, this.eventHandler, false); result.start(); return result; } case PATH_AND_ALL_DESCENDANTS: { try { - var result = new JDKDirectoryWatcher(path, executor, this.eventHandler, true); + var result = new JDKDirectoryWatch(path, executor, this.eventHandler, true); result.start(); return result; } catch (Throwable ex) { // no native support, use the simulation logger.debug("Not possible to register the native watcher, using fallback for {}", path); logger.trace(ex); - var result = new JDKRecursiveDirectoryWatcher(path, executor, this.eventHandler); + var result = new JDKRecursiveDirectoryWatch(path, executor, this.eventHandler); result.start(); return result; } } case PATH_ONLY: { - var result = new JDKFileWatcher(path, executor, this.eventHandler); + var result = new JDKFileWatch(path, executor, this.eventHandler); result.start(); return result; } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatcher.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java similarity index 94% rename from src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatcher.java rename to src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java index ba00891d..96e89f9c 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatcher.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java @@ -44,7 +44,7 @@ import engineering.swat.watch.impl.util.BundledSubscription; import engineering.swat.watch.impl.util.SubscriptionKey; -public class JDKDirectoryWatcher implements ActiveWatch { +public class JDKDirectoryWatch implements ActiveWatch { private final Logger logger = LogManager.getLogger(); private final Path directory; private final Executor exec; @@ -55,11 +55,11 @@ public class JDKDirectoryWatcher implements ActiveWatch { private static final BundledSubscription>> BUNDLED_JDK_WATCHERS = new BundledSubscription<>(JDKPoller::register); - public JDKDirectoryWatcher(Path directory, Executor exec, Consumer eventHandler) { + public JDKDirectoryWatch(Path directory, Executor exec, Consumer eventHandler) { this(directory, exec, eventHandler, false); } - public JDKDirectoryWatcher(Path directory, Executor exec, Consumer eventHandler, boolean nativeRecursive) { + public JDKDirectoryWatch(Path directory, Executor exec, Consumer eventHandler, boolean nativeRecursive) { this.directory = directory; this.exec = exec; this.eventHandler = eventHandler; diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatcher.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java similarity index 94% rename from src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatcher.java rename to src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index dcf99317..e0b170d9 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatcher.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -44,7 +44,7 @@ * * Note that you should take care to call start only once. */ -public class JDKFileWatcher implements ActiveWatch { +public class JDKFileWatch implements ActiveWatch { private final Logger logger = LogManager.getLogger(); private final Path file; private final Path fileName; @@ -52,7 +52,7 @@ public class JDKFileWatcher implements ActiveWatch { private final Consumer eventHandler; private volatile @MonotonicNonNull Closeable activeWatch; - public JDKFileWatcher(Path file, Executor exec, Consumer eventHandler) { + public JDKFileWatch(Path file, Executor exec, Consumer eventHandler) { this.file = file; Path filename= file.getFileName(); if (filename == null) { @@ -75,12 +75,12 @@ public void start() throws IOException { } assert !dir.equals(file); - JDKDirectoryWatcher parentWatch; + JDKDirectoryWatch parentWatch; synchronized(this) { if (activeWatch != null) { throw new IOException("Cannot start an already started watch"); } - activeWatch = parentWatch = new JDKDirectoryWatcher(dir, exec, this::filter); + activeWatch = parentWatch = new JDKDirectoryWatch(dir, exec, this::filter); parentWatch.start(); } logger.debug("Started file watch for {} (in reality a watch on {}): {}", file, dir, parentWatch); diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatcher.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java similarity index 97% rename from src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatcher.java rename to src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index 814298c6..8d0e4fa0 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatcher.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -51,14 +51,14 @@ import engineering.swat.watch.ActiveWatch; import engineering.swat.watch.WatchEvent; -public class JDKRecursiveDirectoryWatcher implements ActiveWatch { +public class JDKRecursiveDirectoryWatch implements ActiveWatch { private final Logger logger = LogManager.getLogger(); private final Path root; private final Executor exec; private final Consumer eventHandler; - private final ConcurrentMap activeWatches = new ConcurrentHashMap<>(); + private final ConcurrentMap activeWatches = new ConcurrentHashMap<>(); - public JDKRecursiveDirectoryWatcher(Path directory, Executor exec, Consumer eventHandler) { + public JDKRecursiveDirectoryWatch(Path directory, Executor exec, Consumer eventHandler) { this.root = directory; this.exec = exec; this.eventHandler = eventHandler; @@ -167,7 +167,7 @@ public FileVisitResult postVisitDirectory(Path subdir, IOException exc) throws I } private void addNewDirectory(Path dir) throws IOException { - var watcher = activeWatches.computeIfAbsent(dir, d -> new JDKDirectoryWatcher(d, exec, relocater(dir))); + var watcher = activeWatches.computeIfAbsent(dir, d -> new JDKDirectoryWatch(d, exec, relocater(dir))); try { if (!watcher.safeStart()) { logger.debug("We lost the race on starting a nested watcher, that shouldn't be a problem, but it's a very busy, so we might have lost a few events in {}", dir); From 21890c7f10fdf6e6a44f36296dad7f1a3740d447 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 31 Jan 2025 10:05:16 +0100 Subject: [PATCH 02/17] Extract common base class from `JDK...Watch` classes --- .../swat/watch/impl/jdk/JDKBaseWatch.java | 21 ++++++++++++ .../watch/impl/jdk/JDKDirectoryWatch.java | 23 +++++-------- .../swat/watch/impl/jdk/JDKFileWatch.java | 24 +++++--------- .../impl/jdk/JDKRecursiveDirectoryWatch.java | 33 ++++++++----------- 4 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java new file mode 100644 index 00000000..5f357fe4 --- /dev/null +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java @@ -0,0 +1,21 @@ +package engineering.swat.watch.impl.jdk; + +import java.nio.file.Path; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +import engineering.swat.watch.ActiveWatch; +import engineering.swat.watch.WatchEvent; + +public abstract class JDKBaseWatch implements ActiveWatch { + + protected final Path path; + protected final Executor exec; + protected final Consumer eventHandler; + + protected JDKBaseWatch(Path path, Executor exec, Consumer eventHandler) { + this.path = path; + this.exec = exec; + this.eventHandler = eventHandler; + } +} diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java index 96e89f9c..eb592b8a 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java @@ -39,16 +39,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import engineering.swat.watch.ActiveWatch; import engineering.swat.watch.WatchEvent; import engineering.swat.watch.impl.util.BundledSubscription; import engineering.swat.watch.impl.util.SubscriptionKey; -public class JDKDirectoryWatch implements ActiveWatch { +public class JDKDirectoryWatch extends JDKBaseWatch { private final Logger logger = LogManager.getLogger(); - private final Path directory; - private final Executor exec; - private final Consumer eventHandler; private volatile @MonotonicNonNull Closeable activeWatch; private final boolean nativeRecursive; @@ -60,18 +56,15 @@ public JDKDirectoryWatch(Path directory, Executor exec, Consumer eve } public JDKDirectoryWatch(Path directory, Executor exec, Consumer eventHandler, boolean nativeRecursive) { - this.directory = directory; - this.exec = exec; - this.eventHandler = eventHandler; + super(directory, exec, eventHandler); this.nativeRecursive = nativeRecursive; } - synchronized boolean safeStart() throws IOException { if (activeWatch != null) { return false; } - activeWatch = BUNDLED_JDK_WATCHERS.subscribe(new SubscriptionKey(directory, nativeRecursive), this::handleChanges); + activeWatch = BUNDLED_JDK_WATCHERS.subscribe(new SubscriptionKey(path, nativeRecursive), this::handleChanges); return true; } @@ -80,9 +73,9 @@ public void start() throws IOException { if (!safeStart()) { throw new IllegalStateException("Cannot start a watcher twice"); } - logger.debug("Started watch for: {}", directory); + logger.debug("Started watch for: {}", path); } catch (IOException e) { - throw new IOException("Could not register directory watcher for: " + directory, e); + throw new IOException("Could not register directory watcher for: " + path, e); } } @@ -116,15 +109,15 @@ else if (ev.kind() == StandardWatchEventKinds.OVERFLOW) { else { throw new IllegalArgumentException("Unexpected watch event: " + ev); } - var path = kind == WatchEvent.Kind.OVERFLOW ? this.directory : (@Nullable Path)ev.context(); + var path = kind == WatchEvent.Kind.OVERFLOW ? this.path : (@Nullable Path)ev.context(); logger.trace("Translated: {} to {} at {}", ev, kind, path); - return new WatchEvent(kind, directory, path); + return new WatchEvent(kind, path, path); } @Override public synchronized void close() throws IOException { if (activeWatch != null) { - logger.trace("Closing watch for: {}", this.directory); + logger.trace("Closing watch for: {}", this.path); activeWatch.close(); } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index e0b170d9..88fc187f 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -36,7 +36,6 @@ import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import engineering.swat.watch.ActiveWatch; import engineering.swat.watch.WatchEvent; /** @@ -44,23 +43,18 @@ * * Note that you should take care to call start only once. */ -public class JDKFileWatch implements ActiveWatch { +public class JDKFileWatch extends JDKBaseWatch { private final Logger logger = LogManager.getLogger(); - private final Path file; private final Path fileName; - private final Executor exec; - private final Consumer eventHandler; private volatile @MonotonicNonNull Closeable activeWatch; public JDKFileWatch(Path file, Executor exec, Consumer eventHandler) { - this.file = file; - Path filename= file.getFileName(); - if (filename == null) { + super(file, exec, eventHandler); + + this.fileName = file.getFileName(); + if (this.fileName == null) { throw new IllegalArgumentException("Cannot pass in a root path"); } - this.fileName = filename; - this.exec = exec; - this.eventHandler = eventHandler; } /** @@ -69,12 +63,12 @@ public JDKFileWatch(Path file, Executor exec, Consumer eventHandler) */ public void start() throws IOException { try { - var dir = file.getParent(); + var dir = path.getParent(); if (dir == null) { throw new IllegalArgumentException("cannot watch a single entry that is on the root"); } - assert !dir.equals(file); + assert !dir.equals(path); JDKDirectoryWatch parentWatch; synchronized(this) { if (activeWatch != null) { @@ -83,10 +77,10 @@ public void start() throws IOException { activeWatch = parentWatch = new JDKDirectoryWatch(dir, exec, this::filter); parentWatch.start(); } - logger.debug("Started file watch for {} (in reality a watch on {}): {}", file, dir, parentWatch); + logger.debug("Started file watch for {} (in reality a watch on {}): {}", path, dir, parentWatch); } catch (IOException e) { - throw new IOException("Could not register file watcher for: " + file, e); + throw new IOException("Could not register file watcher for: " + path, e); } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index 8d0e4fa0..ea3bb769 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -26,7 +26,6 @@ */ package engineering.swat.watch.impl.jdk; -import java.io.Closeable; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -48,28 +47,22 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import engineering.swat.watch.ActiveWatch; import engineering.swat.watch.WatchEvent; -public class JDKRecursiveDirectoryWatch implements ActiveWatch { +public class JDKRecursiveDirectoryWatch extends JDKBaseWatch { private final Logger logger = LogManager.getLogger(); - private final Path root; - private final Executor exec; - private final Consumer eventHandler; private final ConcurrentMap activeWatches = new ConcurrentHashMap<>(); public JDKRecursiveDirectoryWatch(Path directory, Executor exec, Consumer eventHandler) { - this.root = directory; - this.exec = exec; - this.eventHandler = eventHandler; + super(directory, exec, eventHandler); } public void start() throws IOException { try { - logger.debug("Starting recursive watch for: {}", root); - registerInitialWatches(root); + logger.debug("Starting recursive watch for: {}", path); + registerInitialWatches(path); } catch (IOException e) { - throw new IOException("Could not register directory watcher for: " + root, e); + throw new IOException("Could not register directory watcher for: " + path, e); } } @@ -115,7 +108,7 @@ private void handleCreate(WatchEvent ev) { } private void handleOverflow(WatchEvent ev) { - logger.info("Overflow detected, rescanning to find missed entries in {}", root); + logger.info("Overflow detected, rescanning to find missed entries in {}", path); CompletableFuture .completedFuture(ev.calculateFullPath()) .thenApplyAsync(this::syncAfterOverflow, exec) @@ -181,9 +174,9 @@ private void addNewDirectory(Path dir) throws IOException { /** Make sure that the events are relative to the actual root of the recursive watcher */ private Consumer relocater(Path subRoot) { - final Path newRelative = root.relativize(subRoot); + final Path newRelative = path.relativize(subRoot); return ev -> { - var rewritten = new WatchEvent(ev.getKind(), root, newRelative.resolve(ev.getRelativePath())); + var rewritten = new WatchEvent(ev.getKind(), path, newRelative.resolve(ev.getRelativePath())); processEvents(rewritten); }; } @@ -208,7 +201,7 @@ public FileVisitResult preVisitDirectory(Path subdir, BasicFileAttributes attrs) hasFiles = false; if (!seenDirs.contains(subdir)) { if (!subdir.equals(subRoot)) { - events.add(new WatchEvent(WatchEvent.Kind.CREATED, root, root.relativize(subdir))); + events.add(new WatchEvent(WatchEvent.Kind.CREATED, path, path.relativize(subdir))); } return super.preVisitDirectory(subdir, attrs); } @@ -222,7 +215,7 @@ public FileVisitResult preVisitDirectory(Path subdir, BasicFileAttributes attrs) @Override public FileVisitResult postVisitDirectory(Path subdir, IOException exc) throws IOException { if (hasFiles) { - events.add(new WatchEvent(WatchEvent.Kind.MODIFIED, root, root.relativize(subdir))); + events.add(new WatchEvent(WatchEvent.Kind.MODIFIED, path, path.relativize(subdir))); } return super.postVisitDirectory(subdir, exc); } @@ -232,10 +225,10 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO if (!seenFiles.contains(file)) { hasFiles = true; - var relative = root.relativize(file); - events.add(new WatchEvent(WatchEvent.Kind.CREATED, root, relative)); + var relative = path.relativize(file); + events.add(new WatchEvent(WatchEvent.Kind.CREATED, path, relative)); if (attrs.size() > 0) { - events.add(new WatchEvent(WatchEvent.Kind.MODIFIED, root, relative)); + events.add(new WatchEvent(WatchEvent.Kind.MODIFIED, path, relative)); } seenFiles.add(file); } From 8a97a4a51bf0eaff32621f7855ae26adef539263 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 31 Jan 2025 12:11:48 +0100 Subject: [PATCH 03/17] Extract start method from from `JDK...Watch` classes into base class --- .../swat/watch/impl/jdk/JDKBaseWatch.java | 26 +++++++++ .../watch/impl/jdk/JDKDirectoryWatch.java | 38 ++++++------- .../swat/watch/impl/jdk/JDKFileWatch.java | 56 ++++++++----------- .../impl/jdk/JDKRecursiveDirectoryWatch.java | 11 +++- 4 files changed, 74 insertions(+), 57 deletions(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java index 5f357fe4..67f7b24c 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java @@ -1,13 +1,18 @@ package engineering.swat.watch.impl.jdk; +import java.io.IOException; import java.nio.file.Path; import java.util.concurrent.Executor; import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import engineering.swat.watch.ActiveWatch; import engineering.swat.watch.WatchEvent; public abstract class JDKBaseWatch implements ActiveWatch { + private final Logger logger = LogManager.getLogger(); protected final Path path; protected final Executor exec; @@ -18,4 +23,25 @@ protected JDKBaseWatch(Path path, Executor exec, Consumer eventHandl this.exec = exec; this.eventHandler = eventHandler; } + + public void start() throws IOException { + try { + if (!runIfFirstTime()) { + throw new IllegalStateException("Could not restart already-started watch for: " + path); + } + logger.debug("Started watch for: {}", path); + } catch (Exception e) { + throw new IOException("Could not start watch for: " + path, e); + } + } + + /** + * Runs this watch if it's the first time. Intended to be called by method + * `start`. + * + * @return `true` iff it's the first time this method is called + * @throws IOException When an I/O exception of some sort (e.g., a nested + * watch failed to start) has occurred + */ + protected abstract boolean runIfFirstTime() throws IOException; } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java index eb592b8a..93df8364 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java @@ -45,7 +45,7 @@ public class JDKDirectoryWatch extends JDKBaseWatch { private final Logger logger = LogManager.getLogger(); - private volatile @MonotonicNonNull Closeable activeWatch; + private volatile @MonotonicNonNull Closeable bundledWatch; private final boolean nativeRecursive; private static final BundledSubscription>> @@ -60,25 +60,6 @@ public JDKDirectoryWatch(Path directory, Executor exec, Consumer eve this.nativeRecursive = nativeRecursive; } - synchronized boolean safeStart() throws IOException { - if (activeWatch != null) { - return false; - } - activeWatch = BUNDLED_JDK_WATCHERS.subscribe(new SubscriptionKey(path, nativeRecursive), this::handleChanges); - return true; - } - - public void start() throws IOException { - try { - if (!safeStart()) { - throw new IllegalStateException("Cannot start a watcher twice"); - } - logger.debug("Started watch for: {}", path); - } catch (IOException e) { - throw new IOException("Could not register directory watcher for: " + path, e); - } - } - private void handleChanges(List> events) { exec.execute(() -> { for (var ev : events) { @@ -114,11 +95,24 @@ else if (ev.kind() == StandardWatchEventKinds.OVERFLOW) { return new WatchEvent(kind, path, path); } + // -- JDKBaseWatch -- + @Override public synchronized void close() throws IOException { - if (activeWatch != null) { + if (bundledWatch != null) { logger.trace("Closing watch for: {}", this.path); - activeWatch.close(); + bundledWatch.close(); + } + } + + @Override + protected synchronized boolean runIfFirstTime() throws IOException { + if (bundledWatch != null) { + return false; } + + var key = new SubscriptionKey(path, nativeRecursive); + bundledWatch = BUNDLED_JDK_WATCHERS.subscribe(key, this::handleChanges); + return true; } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index 88fc187f..5dd3eef3 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -26,7 +26,6 @@ */ package engineering.swat.watch.impl.jdk; -import java.io.Closeable; import java.io.IOException; import java.nio.file.Path; import java.util.concurrent.Executor; @@ -45,43 +44,20 @@ */ public class JDKFileWatch extends JDKBaseWatch { private final Logger logger = LogManager.getLogger(); + private final Path parent; private final Path fileName; - private volatile @MonotonicNonNull Closeable activeWatch; + private volatile @MonotonicNonNull JDKDirectoryWatch parentWatch; public JDKFileWatch(Path file, Executor exec, Consumer eventHandler) { super(file, exec, eventHandler); - this.fileName = file.getFileName(); - if (this.fileName == null) { - throw new IllegalArgumentException("Cannot pass in a root path"); + this.parent = path.getParent(); + this.fileName = path.getFileName(); + if (parent == null || fileName == null) { + throw new IllegalArgumentException("The root path is not a valid path for a file watch"); } - } - - /** - * Start the file watcher, but only do it once - * @throws IOException - */ - public void start() throws IOException { - try { - var dir = path.getParent(); - if (dir == null) { - throw new IllegalArgumentException("cannot watch a single entry that is on the root"); - - } - assert !dir.equals(path); - JDKDirectoryWatch parentWatch; - synchronized(this) { - if (activeWatch != null) { - throw new IOException("Cannot start an already started watch"); - } - activeWatch = parentWatch = new JDKDirectoryWatch(dir, exec, this::filter); - parentWatch.start(); - } - logger.debug("Started file watch for {} (in reality a watch on {}): {}", path, dir, parentWatch); - } catch (IOException e) { - throw new IOException("Could not register file watcher for: " + path, e); - } + assert !parent.equals(path); } private void filter(WatchEvent event) { @@ -90,10 +66,24 @@ private void filter(WatchEvent event) { } } + // -- JDKBaseWatch -- + @Override public synchronized void close() throws IOException { - if (activeWatch != null) { - activeWatch.close(); + if (parentWatch != null) { + parentWatch.close(); } } + + @Override + protected synchronized boolean runIfFirstTime() throws IOException { + if (parentWatch != null) { + return false; + } + + parentWatch = new JDKDirectoryWatch(parent, exec, this::filter); + parentWatch.start(); + logger.debug("File watch (for: {}) is in reality a directory watch (for: {}) with a filter (for: {})", path, parent, fileName); + return true; + } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index ea3bb769..96d375d3 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -162,7 +162,7 @@ public FileVisitResult postVisitDirectory(Path subdir, IOException exc) throws I private void addNewDirectory(Path dir) throws IOException { var watcher = activeWatches.computeIfAbsent(dir, d -> new JDKDirectoryWatch(d, exec, relocater(dir))); try { - if (!watcher.safeStart()) { + if (!watcher.runIfFirstTime()) { logger.debug("We lost the race on starting a nested watcher, that shouldn't be a problem, but it's a very busy, so we might have lost a few events in {}", dir); } } catch (IOException ex) { @@ -309,7 +309,7 @@ private void detectedMissingEntries(Path dir, ArrayList events, Hash } } - + // -- JDKBaseWatch -- @Override public void close() throws IOException { @@ -334,4 +334,11 @@ public void close() throws IOException { throw firstFail; } } + + @Override + protected boolean runIfFirstTime() throws IOException { + logger.debug("Running recursive watch for: {}", path); + registerInitialWatches(path); + return false; + } } From 28ba48c453a63b430bc92f1f23d4dc1fb6321351 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 31 Jan 2025 12:40:25 +0100 Subject: [PATCH 04/17] Move event translation function from `JDKDirectoryWatch` to base class (because it's a more general utility) --- .../swat/watch/impl/jdk/JDKBaseWatch.java | 27 +++++++++++++++++++ .../watch/impl/jdk/JDKDirectoryWatch.java | 24 ----------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java index 67f7b24c..a3cf6446 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java @@ -2,11 +2,13 @@ import java.io.IOException; import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; import java.util.concurrent.Executor; import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; import engineering.swat.watch.ActiveWatch; import engineering.swat.watch.WatchEvent; @@ -35,6 +37,31 @@ public void start() throws IOException { } } + protected WatchEvent translate(java.nio.file.WatchEvent jdkEvent) { + WatchEvent.Kind kind; + if (jdkEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) { + kind = WatchEvent.Kind.CREATED; + } + else if (jdkEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { + kind = WatchEvent.Kind.MODIFIED; + } + else if (jdkEvent.kind() == StandardWatchEventKinds.ENTRY_DELETE) { + kind = WatchEvent.Kind.DELETED; + } + else if (jdkEvent.kind() == StandardWatchEventKinds.OVERFLOW) { + kind = WatchEvent.Kind.OVERFLOW; + } + else { + throw new IllegalArgumentException("Unexpected watch event: " + jdkEvent); + } + var rootPath = path; + var relativePath = kind == WatchEvent.Kind.OVERFLOW ? rootPath : (@Nullable Path)jdkEvent.context(); + + var event = new WatchEvent(kind, rootPath, relativePath); + logger.trace("Translated: {} to {}", jdkEvent, event); + return event; + } + /** * Runs this watch if it's the first time. Intended to be called by method * `start`. diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java index 93df8364..ab7f997b 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java @@ -29,7 +29,6 @@ import java.io.Closeable; import java.io.IOException; import java.nio.file.Path; -import java.nio.file.StandardWatchEventKinds; import java.util.List; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -37,7 +36,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; import engineering.swat.watch.WatchEvent; import engineering.swat.watch.impl.util.BundledSubscription; @@ -73,28 +71,6 @@ private void handleChanges(List> events) { }); } - private WatchEvent translate(java.nio.file.WatchEvent ev) { - WatchEvent.Kind kind; - if (ev.kind() == StandardWatchEventKinds.ENTRY_CREATE) { - kind = WatchEvent.Kind.CREATED; - } - else if (ev.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { - kind = WatchEvent.Kind.MODIFIED; - } - else if (ev.kind() == StandardWatchEventKinds.ENTRY_DELETE) { - kind = WatchEvent.Kind.DELETED; - } - else if (ev.kind() == StandardWatchEventKinds.OVERFLOW) { - kind = WatchEvent.Kind.OVERFLOW; - } - else { - throw new IllegalArgumentException("Unexpected watch event: " + ev); - } - var path = kind == WatchEvent.Kind.OVERFLOW ? this.path : (@Nullable Path)ev.context(); - logger.trace("Translated: {} to {} at {}", ev, kind, path); - return new WatchEvent(kind, path, path); - } - // -- JDKBaseWatch -- @Override From a9251f0a8a56a9bc128eee56844484132ad82354 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 31 Jan 2025 12:46:16 +0100 Subject: [PATCH 05/17] Use extracted start method in base class to simplify `Watcher` --- .../java/engineering/swat/watch/Watcher.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/java/engineering/swat/watch/Watcher.java b/src/main/java/engineering/swat/watch/Watcher.java index b3bf3728..7e71f7cc 100644 --- a/src/main/java/engineering/swat/watch/Watcher.java +++ b/src/main/java/engineering/swat/watch/Watcher.java @@ -37,6 +37,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import engineering.swat.watch.impl.jdk.JDKBaseWatch; import engineering.swat.watch.impl.jdk.JDKDirectoryWatch; import engineering.swat.watch.impl.jdk.JDKFileWatch; import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch; @@ -156,35 +157,31 @@ public ActiveWatch start() throws IOException { if (this.eventHandler == EMPTY_HANDLER) { throw new IllegalStateException("There is no onEvent handler defined"); } + var result = createWatch(); + result.start(); + return result; + } + + private JDKBaseWatch createWatch() { switch (scope) { case PATH_AND_CHILDREN: { - var result = new JDKDirectoryWatch(path, executor, this.eventHandler, false); - result.start(); - return result; + return new JDKDirectoryWatch(path, executor, eventHandler, false); } case PATH_AND_ALL_DESCENDANTS: { try { - var result = new JDKDirectoryWatch(path, executor, this.eventHandler, true); - result.start(); - return result; + return new JDKDirectoryWatch(path, executor, eventHandler, true); } catch (Throwable ex) { // no native support, use the simulation logger.debug("Not possible to register the native watcher, using fallback for {}", path); logger.trace(ex); - var result = new JDKRecursiveDirectoryWatch(path, executor, this.eventHandler); - result.start(); - return result; + return new JDKRecursiveDirectoryWatch(path, executor, eventHandler); } } case PATH_ONLY: { - var result = new JDKFileWatch(path, executor, this.eventHandler); - result.start(); - return result; + return new JDKFileWatch(path, executor, eventHandler); } - default: throw new IllegalStateException("Not supported yet"); } } - } From 175bf5e1fdc98e39723c8dd67dd37e5c4ae37307 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Tue, 11 Feb 2025 11:20:35 +0100 Subject: [PATCH 06/17] Fix exception handler logic to use the fallback recursive directory watch --- .../java/engineering/swat/watch/Watcher.java | 24 ++++++++++++------- .../impl/jdk/JDKRecursiveDirectoryWatch.java | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/engineering/swat/watch/Watcher.java b/src/main/java/engineering/swat/watch/Watcher.java index 7e71f7cc..480eade6 100644 --- a/src/main/java/engineering/swat/watch/Watcher.java +++ b/src/main/java/engineering/swat/watch/Watcher.java @@ -157,31 +157,37 @@ public ActiveWatch start() throws IOException { if (this.eventHandler == EMPTY_HANDLER) { throw new IllegalStateException("There is no onEvent handler defined"); } - var result = createWatch(); - result.start(); - return result; - } - private JDKBaseWatch createWatch() { + JDKBaseWatch result; + switch (scope) { case PATH_AND_CHILDREN: { - return new JDKDirectoryWatch(path, executor, eventHandler, false); + result = new JDKDirectoryWatch(path, executor, eventHandler, false); + result.start(); + break; } case PATH_AND_ALL_DESCENDANTS: { try { - return new JDKDirectoryWatch(path, executor, eventHandler, true); + result = new JDKDirectoryWatch(path, executor, eventHandler, true); + result.start(); } catch (Throwable ex) { // no native support, use the simulation logger.debug("Not possible to register the native watcher, using fallback for {}", path); logger.trace(ex); - return new JDKRecursiveDirectoryWatch(path, executor, eventHandler); + result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler); + result.start(); } + break; } case PATH_ONLY: { - return new JDKFileWatch(path, executor, eventHandler); + result = new JDKFileWatch(path, executor, eventHandler); + result.start(); + break; } default: throw new IllegalStateException("Not supported yet"); } + + return result; } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index 96d375d3..0123d488 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -339,6 +339,6 @@ public void close() throws IOException { protected boolean runIfFirstTime() throws IOException { logger.debug("Running recursive watch for: {}", path); registerInitialWatches(path); - return false; + return true; } } From b9761bb0b1d58c716e4d247e971d21b1d41bd714 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Tue, 11 Feb 2025 12:25:43 +0100 Subject: [PATCH 07/17] Fix Checker Framework errors --- .../engineering/swat/watch/impl/jdk/JDKFileWatch.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index 5dd3eef3..546c8bc3 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -51,13 +51,17 @@ public class JDKFileWatch extends JDKBaseWatch { public JDKFileWatch(Path file, Executor exec, Consumer eventHandler) { super(file, exec, eventHandler); - this.parent = path.getParent(); - this.fileName = path.getFileName(); + // Use local variables to check null before field assignments (Checker + // Framework doesn't like it the other way around) + var parent = path.getParent(); + var fileName = path.getFileName(); if (parent == null || fileName == null) { throw new IllegalArgumentException("The root path is not a valid path for a file watch"); } - assert !parent.equals(path); + + this.parent = parent; + this.fileName = fileName; } private void filter(WatchEvent event) { From b216116a19325e7e4831c3354affcb551ef06021 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Tue, 11 Feb 2025 12:26:04 +0100 Subject: [PATCH 08/17] Add license --- .../swat/watch/impl/jdk/JDKBaseWatch.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java index a3cf6446..9fe5f639 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java @@ -1,3 +1,29 @@ +/* + * BSD 2-Clause License + * + * Copyright (c) 2023, Swat.engineering + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ package engineering.swat.watch.impl.jdk; import java.io.IOException; From f9904b60ddf8261516119b7e37a8957aa316dd47 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 14 Feb 2025 10:07:42 +0100 Subject: [PATCH 09/17] Remove now-redundant start method in `JDKRecursiveDirectoryWatch` --- .../impl/jdk/JDKRecursiveDirectoryWatch.java | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index 0123d488..d55fef43 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -57,27 +57,15 @@ public JDKRecursiveDirectoryWatch(Path directory, Executor exec, Consumer Date: Fri, 14 Feb 2025 10:12:52 +0100 Subject: [PATCH 10/17] Fix some warnings in the test code --- .../java/engineering/swat/watch/APIErrorsTests.java | 2 +- .../java/engineering/swat/watch/DeleteLockTests.java | 2 +- .../engineering/swat/watch/RecursiveWatchTests.java | 7 +++---- .../engineering/swat/watch/SingleDirectoryTests.java | 10 ++++------ .../java/engineering/swat/watch/SingleFileTests.java | 4 ++-- src/test/java/engineering/swat/watch/SmokeTests.java | 7 +++---- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/test/java/engineering/swat/watch/APIErrorsTests.java b/src/test/java/engineering/swat/watch/APIErrorsTests.java index ffa5b44c..67291671 100644 --- a/src/test/java/engineering/swat/watch/APIErrorsTests.java +++ b/src/test/java/engineering/swat/watch/APIErrorsTests.java @@ -37,7 +37,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class APIErrorsTests { +class APIErrorsTests { private TestDirectory testDir; diff --git a/src/test/java/engineering/swat/watch/DeleteLockTests.java b/src/test/java/engineering/swat/watch/DeleteLockTests.java index 1996515c..0169eedc 100644 --- a/src/test/java/engineering/swat/watch/DeleteLockTests.java +++ b/src/test/java/engineering/swat/watch/DeleteLockTests.java @@ -51,7 +51,7 @@ void setup() throws IOException { } @AfterEach - void cleanup() throws IOException { + void cleanup() { if (testDir != null) { testDir.close(); } diff --git a/src/test/java/engineering/swat/watch/RecursiveWatchTests.java b/src/test/java/engineering/swat/watch/RecursiveWatchTests.java index 655a2b8d..9a309cfa 100644 --- a/src/test/java/engineering/swat/watch/RecursiveWatchTests.java +++ b/src/test/java/engineering/swat/watch/RecursiveWatchTests.java @@ -31,7 +31,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -56,14 +55,14 @@ void setup() throws IOException { } @AfterEach - void cleanup() throws IOException { + void cleanup() { if (testDir != null) { testDir.close(); } } @BeforeAll - static void setupEverything() throws IOException { + static void setupEverything() { Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT); } @@ -123,7 +122,7 @@ void correctRelativePathIsReported() throws IOException { } @Test - void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedException { + void deleteOfFileInDirectoryShouldBeVisible() throws IOException { var target = testDir.getTestFiles() .stream() .filter(p -> !p.getParent().equals(testDir.getTestDirectory())) diff --git a/src/test/java/engineering/swat/watch/SingleDirectoryTests.java b/src/test/java/engineering/swat/watch/SingleDirectoryTests.java index 63ec141b..72d0c656 100644 --- a/src/test/java/engineering/swat/watch/SingleDirectoryTests.java +++ b/src/test/java/engineering/swat/watch/SingleDirectoryTests.java @@ -30,8 +30,6 @@ import java.io.IOException; import java.nio.file.Files; -import java.time.Duration; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.awaitility.Awaitility; @@ -42,7 +40,7 @@ import engineering.swat.watch.WatchEvent.Kind; -public class SingleDirectoryTests { +class SingleDirectoryTests { private TestDirectory testDir; @BeforeEach @@ -51,7 +49,7 @@ void setup() throws IOException { } @AfterEach - void cleanup() throws IOException { + void cleanup() { if (testDir != null) { testDir.close(); } @@ -63,7 +61,7 @@ static void setupEverything() { } @Test - void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedException { + void deleteOfFileInDirectoryShouldBeVisible() throws IOException { var target = testDir.getTestFiles().get(0); var seenDelete = new AtomicBoolean(false); var seenCreate = new AtomicBoolean(false); @@ -91,7 +89,7 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedExc } @Test - void alternativeAPITest() throws IOException, InterruptedException { + void alternativeAPITest() throws IOException { var target = testDir.getTestFiles().get(0); var seenDelete = new AtomicBoolean(false); var seenCreate = new AtomicBoolean(false); diff --git a/src/test/java/engineering/swat/watch/SingleFileTests.java b/src/test/java/engineering/swat/watch/SingleFileTests.java index 71a2ab93..206bc9bf 100644 --- a/src/test/java/engineering/swat/watch/SingleFileTests.java +++ b/src/test/java/engineering/swat/watch/SingleFileTests.java @@ -40,7 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class SingleFileTests { +class SingleFileTests { private TestDirectory testDir; @BeforeEach @@ -49,7 +49,7 @@ void setup() throws IOException { } @AfterEach - void cleanup() throws IOException { + void cleanup() { if (testDir != null) { testDir.close(); } diff --git a/src/test/java/engineering/swat/watch/SmokeTests.java b/src/test/java/engineering/swat/watch/SmokeTests.java index 29cfe506..86242623 100644 --- a/src/test/java/engineering/swat/watch/SmokeTests.java +++ b/src/test/java/engineering/swat/watch/SmokeTests.java @@ -32,7 +32,6 @@ import java.io.IOException; import java.nio.file.Files; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.awaitility.Awaitility; @@ -51,7 +50,7 @@ void setup() throws IOException { } @AfterEach - void cleanup() throws IOException { + void cleanup() { if (testDir != null) { testDir.close(); } @@ -63,7 +62,7 @@ static void setupEverything() { } @Test - void watchDirectory() throws IOException, InterruptedException { + void watchDirectory() throws IOException { var changed = new AtomicBoolean(false); var target = testDir.getTestFiles().get(0); var watchConfig = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN) @@ -77,7 +76,7 @@ void watchDirectory() throws IOException, InterruptedException { } @Test - void watchRecursiveDirectory() throws IOException, InterruptedException { + void watchRecursiveDirectory() throws IOException { var changed = new AtomicBoolean(false); var target = testDir.getTestFiles().stream() .filter(p -> !p.getParent().equals(testDir.getTestDirectory())) From b87ea2c503e41cadd67a536a31aca4f8a6748efc Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 14 Feb 2025 10:16:39 +0100 Subject: [PATCH 11/17] Fix some warnings in the test code --- src/test/java/engineering/swat/watch/TortureTests.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/test/java/engineering/swat/watch/TortureTests.java b/src/test/java/engineering/swat/watch/TortureTests.java index 5704d864..bfc4f9da 100644 --- a/src/test/java/engineering/swat/watch/TortureTests.java +++ b/src/test/java/engineering/swat/watch/TortureTests.java @@ -39,7 +39,6 @@ import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; @@ -56,8 +55,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import engineering.swat.watch.WatchEvent.Kind; - class TortureTests { private final Logger logger = LogManager.getLogger(); @@ -70,7 +67,7 @@ void setup() throws IOException { } @AfterEach - void cleanup() throws IOException { + void cleanup() { if (testDir != null) { testDir.close(); } @@ -90,7 +87,7 @@ private final class IOGenerator { } } - private final static int BURST_SIZE = 1000; + private static final int BURST_SIZE = 1000; private void startJob(final Path root, Random r, Executor exec) { exec.execute(() -> { From 1e9c9caa42a501892493f7efa6af430327154e4b Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 14 Feb 2025 10:21:01 +0100 Subject: [PATCH 12/17] Rename a few variables --- .../swat/watch/impl/jdk/JDKDirectoryWatch.java | 10 +++++----- .../watch/impl/jdk/JDKRecursiveDirectoryWatch.java | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java index ab7f997b..a9e5cc85 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java @@ -43,8 +43,8 @@ public class JDKDirectoryWatch extends JDKBaseWatch { private final Logger logger = LogManager.getLogger(); - private volatile @MonotonicNonNull Closeable bundledWatch; private final boolean nativeRecursive; + private volatile @MonotonicNonNull Closeable bundledJDKWatcher; private static final BundledSubscription>> BUNDLED_JDK_WATCHERS = new BundledSubscription<>(JDKPoller::register); @@ -75,20 +75,20 @@ private void handleChanges(List> events) { @Override public synchronized void close() throws IOException { - if (bundledWatch != null) { + if (bundledJDKWatcher != null) { logger.trace("Closing watch for: {}", this.path); - bundledWatch.close(); + bundledJDKWatcher.close(); } } @Override protected synchronized boolean runIfFirstTime() throws IOException { - if (bundledWatch != null) { + if (bundledJDKWatcher != null) { return false; } var key = new SubscriptionKey(path, nativeRecursive); - bundledWatch = BUNDLED_JDK_WATCHERS.subscribe(key, this::handleChanges); + bundledJDKWatcher = BUNDLED_JDK_WATCHERS.subscribe(key, this::handleChanges); return true; } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index d55fef43..694ae461 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -148,10 +148,10 @@ public FileVisitResult postVisitDirectory(Path subdir, IOException exc) throws I } private void addNewDirectory(Path dir) throws IOException { - var watcher = activeWatches.computeIfAbsent(dir, d -> new JDKDirectoryWatch(d, exec, relocater(dir))); + var watch = activeWatches.computeIfAbsent(dir, d -> new JDKDirectoryWatch(d, exec, relocater(dir))); try { - if (!watcher.runIfFirstTime()) { - logger.debug("We lost the race on starting a nested watcher, that shouldn't be a problem, but it's a very busy, so we might have lost a few events in {}", dir); + if (!watch.runIfFirstTime()) { + logger.debug("We lost the race on starting a nested watch, that shouldn't be a problem, but it's a very busy, so we might have lost a few events in {}", dir); } } catch (IOException ex) { activeWatches.remove(dir); @@ -160,7 +160,7 @@ private void addNewDirectory(Path dir) throws IOException { } } - /** Make sure that the events are relative to the actual root of the recursive watcher */ + /** Make sure that the events are relative to the actual root of the recursive watch */ private Consumer relocater(Path subRoot) { final Path newRelative = path.relativize(subRoot); return ev -> { From 377df564d35a87226703c019a2073714ab485baf Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Fri, 14 Feb 2025 10:22:26 +0100 Subject: [PATCH 13/17] Revert change that didn't improve code quality that much --- .../java/engineering/swat/watch/Watcher.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/engineering/swat/watch/Watcher.java b/src/main/java/engineering/swat/watch/Watcher.java index 480eade6..f0ab6321 100644 --- a/src/main/java/engineering/swat/watch/Watcher.java +++ b/src/main/java/engineering/swat/watch/Watcher.java @@ -158,36 +158,33 @@ public ActiveWatch start() throws IOException { throw new IllegalStateException("There is no onEvent handler defined"); } - JDKBaseWatch result; - switch (scope) { case PATH_AND_CHILDREN: { - result = new JDKDirectoryWatch(path, executor, eventHandler, false); + var result = new JDKDirectoryWatch(path, executor, eventHandler, false); result.start(); - break; + return result; } case PATH_AND_ALL_DESCENDANTS: { try { - result = new JDKDirectoryWatch(path, executor, eventHandler, true); + var result = new JDKDirectoryWatch(path, executor, eventHandler, true); result.start(); + return result; } catch (Throwable ex) { // no native support, use the simulation logger.debug("Not possible to register the native watcher, using fallback for {}", path); logger.trace(ex); - result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler); + var result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler); result.start(); + return result; } - break; } case PATH_ONLY: { - result = new JDKFileWatch(path, executor, eventHandler); + var result = new JDKFileWatch(path, executor, eventHandler); result.start(); - break; + return result; } default: throw new IllegalStateException("Not supported yet"); } - - return result; } } From 5c4b337681c35bced51ecff61e5815d4c46c6ffc Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 17 Feb 2025 14:18:57 +0100 Subject: [PATCH 14/17] Fix relative path of overflow events --- src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java index 9fe5f639..2cdde483 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java @@ -81,7 +81,7 @@ else if (jdkEvent.kind() == StandardWatchEventKinds.OVERFLOW) { throw new IllegalArgumentException("Unexpected watch event: " + jdkEvent); } var rootPath = path; - var relativePath = kind == WatchEvent.Kind.OVERFLOW ? rootPath : (@Nullable Path)jdkEvent.context(); + var relativePath = kind == WatchEvent.Kind.OVERFLOW ? Path.of("") : (@Nullable Path)jdkEvent.context(); var event = new WatchEvent(kind, rootPath, relativePath); logger.trace("Translated: {} to {}", jdkEvent, event); From 8da56d1319450118d430094ea148a4624b0e8147 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 17 Feb 2025 14:20:12 +0100 Subject: [PATCH 15/17] Make `JDKBaseWatch` responsible for run-only-once management --- .../swat/watch/impl/jdk/JDKBaseWatch.java | 36 +++++++++++++------ .../watch/impl/jdk/JDKDirectoryWatch.java | 8 ++--- .../swat/watch/impl/jdk/JDKFileWatch.java | 8 ++--- .../impl/jdk/JDKRecursiveDirectoryWatch.java | 3 +- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java index 2cdde483..b1c4f81c 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java @@ -30,6 +30,7 @@ import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; @@ -45,6 +46,7 @@ public abstract class JDKBaseWatch implements ActiveWatch { protected final Path path; protected final Executor exec; protected final Consumer eventHandler; + protected final AtomicBoolean running = new AtomicBoolean(); protected JDKBaseWatch(Path path, Executor exec, Consumer eventHandler) { this.path = path; @@ -63,6 +65,30 @@ public void start() throws IOException { } } + /** + * Runs this watch. + * + * @throws IOException When an I/O exception of some sort has occurred + * (e.g., a nested watch failed to start) + */ + protected abstract void run() throws IOException; + + /** + * Runs this watch if it's the first time. + * + * @return `true` iff it's the first time this method is called + * @throws IOException When an I/O exception of some sort has occurred + * (e.g., a nested watch failed to start) + */ + protected boolean runIfFirstTime() throws IOException { + if (running.compareAndSet(false, true)) { + run(); + return true; + } else { + return false; + } + } + protected WatchEvent translate(java.nio.file.WatchEvent jdkEvent) { WatchEvent.Kind kind; if (jdkEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) { @@ -87,14 +113,4 @@ else if (jdkEvent.kind() == StandardWatchEventKinds.OVERFLOW) { logger.trace("Translated: {} to {}", jdkEvent, event); return event; } - - /** - * Runs this watch if it's the first time. Intended to be called by method - * `start`. - * - * @return `true` iff it's the first time this method is called - * @throws IOException When an I/O exception of some sort (e.g., a nested - * watch failed to start) has occurred - */ - protected abstract boolean runIfFirstTime() throws IOException; } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java index a9e5cc85..d543eab8 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java @@ -82,13 +82,9 @@ public synchronized void close() throws IOException { } @Override - protected synchronized boolean runIfFirstTime() throws IOException { - if (bundledJDKWatcher != null) { - return false; - } - + protected synchronized void run() throws IOException { + assert bundledJDKWatcher == null; var key = new SubscriptionKey(path, nativeRecursive); bundledJDKWatcher = BUNDLED_JDK_WATCHERS.subscribe(key, this::handleChanges); - return true; } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index 546c8bc3..f677aec8 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -80,14 +80,10 @@ public synchronized void close() throws IOException { } @Override - protected synchronized boolean runIfFirstTime() throws IOException { - if (parentWatch != null) { - return false; - } - + protected synchronized void run() throws IOException { + assert parentWatch == null; parentWatch = new JDKDirectoryWatch(parent, exec, this::filter); parentWatch.start(); logger.debug("File watch (for: {}) is in reality a directory watch (for: {}) with a filter (for: {})", path, parent, fileName); - return true; } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index 694ae461..79e8cff4 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -324,9 +324,8 @@ public void close() throws IOException { } @Override - protected boolean runIfFirstTime() throws IOException { + protected void run() throws IOException { logger.debug("Running recursive watch for: {}", path); registerInitialWatches(path); - return true; } } From 5cf298c82bb2b41e07d34de7331dc01a107d8123 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 17 Feb 2025 14:20:50 +0100 Subject: [PATCH 16/17] Simplify code to initialize paths in constructor of `JDKFileWatch` --- .../swat/watch/impl/jdk/JDKFileWatch.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index f677aec8..a883eeb6 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -34,6 +34,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import engineering.swat.watch.WatchEvent; @@ -51,17 +52,17 @@ public class JDKFileWatch extends JDKBaseWatch { public JDKFileWatch(Path file, Executor exec, Consumer eventHandler) { super(file, exec, eventHandler); - // Use local variables to check null before field assignments (Checker - // Framework doesn't like it the other way around) - var parent = path.getParent(); - var fileName = path.getFileName(); - if (parent == null || fileName == null) { - throw new IllegalArgumentException("The root path is not a valid path for a file watch"); - } + var message = "The root path is not a valid path for a file watch"; + this.parent = requireNonNull(path.getParent(), message); + this.fileName = requireNonNull(path.getFileName(), message); assert !parent.equals(path); + } - this.parent = parent; - this.fileName = fileName; + private static Path requireNonNull(@Nullable Path p, String message) { + if (p == null) { + throw new IllegalArgumentException(message); + } + return p; } private void filter(WatchEvent event) { From fee22fd5f27670204fb107201a8d88cecbefe5f4 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 17 Feb 2025 15:43:25 +0100 Subject: [PATCH 17/17] Rename in `JDKBaseWatch`: (a) `start` to `open`; (b) `run` to `start` --- .../java/engineering/swat/watch/Watcher.java | 9 ++++----- .../swat/watch/impl/jdk/JDKBaseWatch.java | 18 +++++++++--------- .../swat/watch/impl/jdk/JDKDirectoryWatch.java | 2 +- .../swat/watch/impl/jdk/JDKFileWatch.java | 4 ++-- .../impl/jdk/JDKRecursiveDirectoryWatch.java | 4 ++-- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/main/java/engineering/swat/watch/Watcher.java b/src/main/java/engineering/swat/watch/Watcher.java index f0ab6321..c176dfdb 100644 --- a/src/main/java/engineering/swat/watch/Watcher.java +++ b/src/main/java/engineering/swat/watch/Watcher.java @@ -37,7 +37,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import engineering.swat.watch.impl.jdk.JDKBaseWatch; import engineering.swat.watch.impl.jdk.JDKDirectoryWatch; import engineering.swat.watch.impl.jdk.JDKFileWatch; import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch; @@ -161,26 +160,26 @@ public ActiveWatch start() throws IOException { switch (scope) { case PATH_AND_CHILDREN: { var result = new JDKDirectoryWatch(path, executor, eventHandler, false); - result.start(); + result.open(); return result; } case PATH_AND_ALL_DESCENDANTS: { try { var result = new JDKDirectoryWatch(path, executor, eventHandler, true); - result.start(); + result.open(); return result; } catch (Throwable ex) { // no native support, use the simulation logger.debug("Not possible to register the native watcher, using fallback for {}", path); logger.trace(ex); var result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler); - result.start(); + result.open(); return result; } } case PATH_ONLY: { var result = new JDKFileWatch(path, executor, eventHandler); - result.start(); + result.open(); return result; } default: diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java index b1c4f81c..fb90755a 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java @@ -46,7 +46,7 @@ public abstract class JDKBaseWatch implements ActiveWatch { protected final Path path; protected final Executor exec; protected final Consumer eventHandler; - protected final AtomicBoolean running = new AtomicBoolean(); + protected final AtomicBoolean started = new AtomicBoolean(); protected JDKBaseWatch(Path path, Executor exec, Consumer eventHandler) { this.path = path; @@ -54,9 +54,9 @@ protected JDKBaseWatch(Path path, Executor exec, Consumer eventHandl this.eventHandler = eventHandler; } - public void start() throws IOException { + public void open() throws IOException { try { - if (!runIfFirstTime()) { + if (!startIfFirstTime()) { throw new IllegalStateException("Could not restart already-started watch for: " + path); } logger.debug("Started watch for: {}", path); @@ -66,23 +66,23 @@ public void start() throws IOException { } /** - * Runs this watch. + * Starts this watch. * * @throws IOException When an I/O exception of some sort has occurred * (e.g., a nested watch failed to start) */ - protected abstract void run() throws IOException; + protected abstract void start() throws IOException; /** - * Runs this watch if it's the first time. + * Starts this watch if it's the first time. * * @return `true` iff it's the first time this method is called * @throws IOException When an I/O exception of some sort has occurred * (e.g., a nested watch failed to start) */ - protected boolean runIfFirstTime() throws IOException { - if (running.compareAndSet(false, true)) { - run(); + protected boolean startIfFirstTime() throws IOException { + if (started.compareAndSet(false, true)) { + start(); return true; } else { return false; diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java index d543eab8..fcb6004e 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java @@ -82,7 +82,7 @@ public synchronized void close() throws IOException { } @Override - protected synchronized void run() throws IOException { + protected synchronized void start() throws IOException { assert bundledJDKWatcher == null; var key = new SubscriptionKey(path, nativeRecursive); bundledJDKWatcher = BUNDLED_JDK_WATCHERS.subscribe(key, this::handleChanges); diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index a883eeb6..bd27b83c 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -81,10 +81,10 @@ public synchronized void close() throws IOException { } @Override - protected synchronized void run() throws IOException { + protected synchronized void start() throws IOException { assert parentWatch == null; parentWatch = new JDKDirectoryWatch(parent, exec, this::filter); - parentWatch.start(); + parentWatch.open(); logger.debug("File watch (for: {}) is in reality a directory watch (for: {}) with a filter (for: {})", path, parent, fileName); } } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java index 79e8cff4..ec8b16b7 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKRecursiveDirectoryWatch.java @@ -150,7 +150,7 @@ public FileVisitResult postVisitDirectory(Path subdir, IOException exc) throws I private void addNewDirectory(Path dir) throws IOException { var watch = activeWatches.computeIfAbsent(dir, d -> new JDKDirectoryWatch(d, exec, relocater(dir))); try { - if (!watch.runIfFirstTime()) { + if (!watch.startIfFirstTime()) { logger.debug("We lost the race on starting a nested watch, that shouldn't be a problem, but it's a very busy, so we might have lost a few events in {}", dir); } } catch (IOException ex) { @@ -324,7 +324,7 @@ public void close() throws IOException { } @Override - protected void run() throws IOException { + protected void start() throws IOException { logger.debug("Running recursive watch for: {}", path); registerInitialWatches(path); }