+ * Handler for native events, intended to be used for the construction of
+ * JDK's {@link WatchEvent}s (and continue downstream consumption).
+ *
+ *
+ *
+ * In each call, the types of {@code kind} and {@code context} depend
+ * specifically on the given native event: they're {@code Kind} and
+ * {@code Path} for non-overflows, but they're {@code Kind
+ */
+@FunctionalInterface
+public interface NativeEventHandler {
+ void handle(Kind kind, @Nullable T context);
+}
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
new file mode 100644
index 00000000..ec094f3b
--- /dev/null
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -0,0 +1,258 @@
+package engineering.swat.watch.impl.mac;
+
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.FILE_EVENTS;
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.NO_DEFER;
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.WATCH_ROOT;
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_CREATED;
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_INODE_META_MOD;
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_MODIFIED;
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_REMOVED;
+import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.MUST_SCAN_SUB_DIRS;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import com.sun.jna.Memory;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.platform.mac.CoreFoundation;
+import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef;
+import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
+import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
+
+import engineering.swat.watch.impl.mac.apis.DispatchObjects;
+import engineering.swat.watch.impl.mac.apis.DispatchQueue;
+import engineering.swat.watch.impl.mac.apis.FileSystemEvents;
+import engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCallback;
+
+// Note: This file is designed to be the only place in this package where JNA is
+// used and/or the native APIs are called. If the need to do so arises outside
+// this file, consider extending this file to offer the required services
+// without exposing JNA and/or the native APIs.
+
+/**
+ *
+ * Stream of native events for a path, issued by macOS.
+ *
+ *
+ *
+ * Note: Methods {@link #open()} and {@link #close()} synchronize on this object
+ * to avoid races. The synchronization overhead is expected to be negligible, as
+ * these methods are expected to be rarely called.
+ *
+ */
+public class NativeEventStream implements Closeable {
+
+ // Native APIs
+ private static final CoreFoundation CF = CoreFoundation.INSTANCE;
+ private static final DispatchObjects DO = DispatchObjects.INSTANCE;
+ private static final DispatchQueue DQ = DispatchQueue.INSTANCE;
+ private static final FileSystemEvents FSE = FileSystemEvents.INSTANCE;
+
+ // Native memory (automatically deallocated when set to `null`)
+ private volatile @Nullable FSEventStreamCallback callback;
+ private volatile @Nullable Pointer stream;
+ private volatile @Nullable Pointer queue;
+
+ private final Path path;
+ private final NativeEventHandler handler;
+ private volatile boolean closed;
+
+ public NativeEventStream(Path path, NativeEventHandler handler) throws IOException {
+ this.path = path.toRealPath(); // Resolve symbolic links
+ this.handler = handler;
+ this.closed = true;
+ }
+
+ public synchronized void open() {
+ if (!closed) {
+ throw new IllegalStateException("Stream already open");
+ } else {
+ closed = false;
+ }
+
+ // Allocate native memory. (Checker Framework: The local variables are
+ // `@NonNull` copies of the `@Nullable` fields.)
+ var callback = this.callback = createCallback(path, handler);
+ var stream = this.stream = createFSEventStream(path, callback);
+ var queue = this.queue = createDispatchQueue();
+
+ // Start the stream
+ FSE.FSEventStreamSetDispatchQueue(stream, queue);
+ FSE.FSEventStreamStart(stream);
+ }
+
+ private static FSEventStreamCallback createCallback(Path path, NativeEventHandler handler) {
+ return new FSEventStreamCallback() {
+ @Override
+ public void callback(Pointer streamRef, Pointer clientCallBackInfo,
+ long numEvents, Pointer eventPaths, Pointer eventFlags, Pointer eventIds) {
+ // This function is called each time native events are issued by
+ // macOS. The purpose of this function is to perform the minimal
+ // amount of processing to hide the native APIs from downstream
+ // consumers, who are offered native events via `handler`.
+
+ var paths = eventPaths.getStringArray(0, (int) numEvents);
+ var flags = eventFlags.getIntArray(0, (int) numEvents);
+
+ for (var i = 0; i < numEvents; i++) {
+ var context = path.relativize(Path.of(paths[i]));
+
+ // Note: Multiple "physical" native events might be merged
+ // into a single "logical" native event, so the following
+ // series of checks should be if-statements (instead of
+ // if/else-statements).
+ if (any(flags[i], ITEM_CREATED.mask)) {
+ handler.handle(ENTRY_CREATE, context);
+ }
+ if (any(flags[i], ITEM_REMOVED.mask)) {
+ handler.handle(ENTRY_DELETE, context);
+ }
+ if (any(flags[i], ITEM_MODIFIED.mask | ITEM_INODE_META_MOD.mask)) {
+ handler.handle(ENTRY_MODIFY, context);
+ }
+ if (any(flags[i], MUST_SCAN_SUB_DIRS.mask)) {
+ handler.handle(OVERFLOW, null);
+ }
+ }
+ }
+
+ private boolean any(int bits, int mask) {
+ return (bits & mask) != 0;
+ }
+ };
+ }
+
+ private static Pointer createFSEventStream(Path path, FSEventStreamCallback callback) {
+ try (
+ var pathsToWatch = new Strings(path.toString());
+ ) {
+ var allocator = CF.CFAllocatorGetDefault();
+ var context = Pointer.NULL;
+ var sinceWhen = FSE.FSEventsGetCurrentEventId();
+ var latency = 0.15;
+ var flags = NO_DEFER.mask | WATCH_ROOT.mask | FILE_EVENTS.mask;
+ return FSE.FSEventStreamCreate(allocator, callback, context, pathsToWatch.toCFArray(), sinceWhen, latency, flags);
+ }
+ }
+
+ private static Pointer createDispatchQueue() {
+ var label = "engineering.swat.watch";
+ var attr = Pointer.NULL;
+ return DQ.dispatch_queue_create(label, attr);
+ }
+
+ // -- Closeable --
+
+ @Override
+ public synchronized void close() {
+ if (closed) {
+ throw new IllegalStateException("Stream is already closed");
+ } else {
+ closed = true;
+ }
+
+ // Stop the stream
+ if (stream != null) {
+ var streamNonNull = stream; // Checker Framework: `@NonNull` copy of `@Nullable` field
+ FSE.FSEventStreamStop(streamNonNull);
+ FSE.FSEventStreamSetDispatchQueue(streamNonNull, Pointer.NULL);
+ FSE.FSEventStreamInvalidate(streamNonNull);
+ FSE.FSEventStreamRelease(streamNonNull);
+ }
+ if (queue != null) {
+ DO.dispatch_release(queue);
+ }
+
+ // Deallocate native memory
+ callback = null;
+ stream = null;
+ queue = null;
+ }
+}
+
+/**
+ * Array of strings in native memory, needed to create a new native event stream
+ * (i.e., the {@code pathsToWatch} argument of {@code FSEventStreamCreate} is an
+ * array of strings).
+ */
+class Strings implements AutoCloseable {
+
+ // Native APIs
+ private static final CoreFoundation CF = CoreFoundation.INSTANCE;
+
+ // Native memory
+ private final CFStringRef[] strings;
+ private final CFArrayRef array;
+
+ private volatile boolean closed = false;
+
+ public Strings(String... strings) {
+ // Allocate native memory
+ this.strings = createCFStrings(strings);
+ this.array = createCFArray(this.strings);
+ }
+
+ public CFArrayRef toCFArray() {
+ if (closed) {
+ throw new IllegalStateException("Paths already deallocated");
+ }
+ return array;
+ }
+
+ private static CFStringRef[] createCFStrings(String[] pathsToWatch) {
+ var n = pathsToWatch.length;
+
+ var strings = new CFStringRef[n];
+ for (int i = 0; i < n; i++) {
+ strings[i] = CFStringRef.createCFString(pathsToWatch[i]);
+ }
+ return strings;
+ }
+
+ private static CFArrayRef createCFArray(CFStringRef[] strings) {
+ var n = strings.length;
+ var size = Native.getNativeSize(CFStringRef.class);
+
+ // Create a temporary array of pointers to the strings (automatically
+ // freed when `values` goes out of scope)
+ var values = new Memory(n * size);
+ for (int i = 0; i < n; i++) {
+ values.setPointer(i * size, strings[i].getPointer());
+ }
+
+ // Create a permanent array based on the temporary array
+ var alloc = CF.CFAllocatorGetDefault();
+ var numValues = new CFIndex(n);
+ var callBacks = Pointer.NULL;
+ return CF.CFArrayCreate(alloc, values, numValues, callBacks);
+ }
+
+ // -- AutoCloseable --
+
+ @Override
+ public void close() {
+ if (closed) {
+ throw new IllegalStateException("Paths already deallocated");
+ } else {
+ closed = true;
+ }
+
+ // Deallocate native memory
+ for (var s : strings) {
+ if (s != null) {
+ s.release();
+ }
+ }
+ if (array != null) {
+ array.release();
+ }
+ }
+}
From 7490e1b8e658664b5e869ee593b095539e52b490 Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Mon, 21 Apr 2025 09:17:28 +0200
Subject: [PATCH 04/13] Add license
---
.../watch/impl/mac/NativeEventHandler.java | 26 +++++++++++++++++++
.../watch/impl/mac/NativeEventStream.java | 26 +++++++++++++++++++
.../watch/impl/mac/apis/DispatchObjects.java | 26 +++++++++++++++++++
.../watch/impl/mac/apis/DispatchQueue.java | 26 +++++++++++++++++++
.../watch/impl/mac/apis/FileSystemEvents.java | 26 +++++++++++++++++++
.../engineering/swat/watch/impl/mac/APIs.java | 26 +++++++++++++++++++
6 files changed, 156 insertions(+)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
index b1f9d28c..665888e3 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.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.mac;
import java.nio.file.WatchEvent;
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index ec094f3b..489edc48 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.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.mac;
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.FILE_EVENTS;
diff --git a/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchObjects.java b/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchObjects.java
index 79e72435..41bb5ee6 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchObjects.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchObjects.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.mac.apis;
import com.sun.jna.Library;
diff --git a/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchQueue.java b/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchQueue.java
index 09b34d0e..747d87a5 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchQueue.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchQueue.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.mac.apis;
import com.sun.jna.Library;
diff --git a/src/main/java/engineering/swat/watch/impl/mac/apis/FileSystemEvents.java b/src/main/java/engineering/swat/watch/impl/mac/apis/FileSystemEvents.java
index 350abb5c..d81a0447 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/apis/FileSystemEvents.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/apis/FileSystemEvents.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.mac.apis;
import com.sun.jna.Callback;
diff --git a/src/test/java/engineering/swat/watch/impl/mac/APIs.java b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
index d5b5ee2e..6742d437 100644
--- a/src/test/java/engineering/swat/watch/impl/mac/APIs.java
+++ b/src/test/java/engineering/swat/watch/impl/mac/APIs.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.mac;
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.FILE_EVENTS;
From d33858fc4756bc83121a07c8dbacdf733607c49d Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Mon, 21 Apr 2025 11:02:35 +0200
Subject: [PATCH 05/13] Improve comments
---
.../swat/watch/impl/mac/NativeEventHandler.java | 15 ++++++++-------
.../swat/watch/impl/mac/NativeEventStream.java | 7 ++++---
.../engineering/swat/watch/impl/mac/APIs.java | 8 ++++----
3 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
index 665888e3..8e79bd68 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
@@ -33,19 +33,20 @@
/**
*
- * Handler for native events, intended to be used for the construction of
- * JDK's {@link WatchEvent}s (and continue downstream consumption).
+ * Handler for native events, intended to be used in a {@link NativeEventStream}
+ * callback to construct {@link WatchEvent}s (and propagate them for downstream
+ * consumption).
*
*
*
- * In each call, the types of {@code kind} and {@code context} depend
+ * In each invocation, the types of {@code kind} and {@code context} depend
* specifically on the given native event: they're {@code Kind} and
* {@code Path} for non-overflows, but they're {@code Kind
*/
@FunctionalInterface
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index 489edc48..e8395594 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -59,19 +59,20 @@
import engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCallback;
// Note: This file is designed to be the only place in this package where JNA is
-// used and/or the native APIs are called. If the need to do so arises outside
+// used and/or the native APIs are invoked. If the need to do so arises outside
// this file, consider extending this file to offer the required services
// without exposing JNA and/or the native APIs.
/**
*
- * Stream of native events for a path, issued by macOS.
+ * Stream of native events for a path, issued by macOS. It's a facade-like
+ * object that hides the low-level native APIs behind a higher-level interface.
*
*
*
* Note: Methods {@link #open()} and {@link #close()} synchronize on this object
* to avoid races. The synchronization overhead is expected to be negligible, as
- * these methods are expected to be rarely called.
+ * these methods are expected to be rarely invoked.
*
*/
public class NativeEventStream implements Closeable {
diff --git a/src/test/java/engineering/swat/watch/impl/mac/APIs.java b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
index 6742d437..867fcb4e 100644
--- a/src/test/java/engineering/swat/watch/impl/mac/APIs.java
+++ b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
@@ -107,7 +107,7 @@ void smokeTest() throws IOException {
}
public static void main(String[] args) throws IOException {
- var s = "/Users/sungshik/Desktop/tmp";
+ var s = args[0];
var handler = (MinimalWorkingExample.EventHandler) (path, flags, id) -> {
LOGGER.info(prettyPrint(path, flags, id));
};
@@ -130,9 +130,9 @@ private static String prettyPrint(String path, int flags, long id) {
}
private static class MinimalWorkingExample implements Closeable {
- FileSystemEvents.FSEventStreamCallback callback;
- Pointer stream;
- Pointer queue;
+ private FileSystemEvents.FSEventStreamCallback callback;
+ private Pointer stream;
+ private Pointer queue;
public MinimalWorkingExample(String s, EventHandler handler) {
From ca8ff64b109a641a13e28f6665e8e5294c207878 Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Mon, 21 Apr 2025 11:26:41 +0200
Subject: [PATCH 06/13] Remove unnecessary volatile modifiers
---
.../engineering/swat/watch/impl/mac/NativeEventStream.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index e8395594..7f07df03 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -84,9 +84,9 @@ public class NativeEventStream implements Closeable {
private static final FileSystemEvents FSE = FileSystemEvents.INSTANCE;
// Native memory (automatically deallocated when set to `null`)
- private volatile @Nullable FSEventStreamCallback callback;
- private volatile @Nullable Pointer stream;
- private volatile @Nullable Pointer queue;
+ private @Nullable FSEventStreamCallback callback;
+ private @Nullable Pointer stream;
+ private @Nullable Pointer queue;
private final Path path;
private final NativeEventHandler handler;
From ccce387886260309e4e9a7c21555b5bc8c2beda2 Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Tue, 22 Apr 2025 09:04:05 +0200
Subject: [PATCH 07/13] Make a few static methods non-static to directly use
fields (instead of args)
---
.../watch/impl/mac/NativeEventStream.java | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index 7f07df03..2de79db9 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -100,15 +100,15 @@ public NativeEventStream(Path path, NativeEventHandler handler) throws IOExcepti
public synchronized void open() {
if (!closed) {
- throw new IllegalStateException("Stream already open");
+ throw new IllegalStateException("Stream is already open");
} else {
closed = false;
}
// Allocate native memory. (Checker Framework: The local variables are
// `@NonNull` copies of the `@Nullable` fields.)
- var callback = this.callback = createCallback(path, handler);
- var stream = this.stream = createFSEventStream(path, callback);
+ var callback = this.callback = createCallback();
+ var stream = this.stream = createFSEventStream(callback);
var queue = this.queue = createDispatchQueue();
// Start the stream
@@ -116,7 +116,7 @@ public synchronized void open() {
FSE.FSEventStreamStart(stream);
}
- private static FSEventStreamCallback createCallback(Path path, NativeEventHandler handler) {
+ private FSEventStreamCallback createCallback() {
return new FSEventStreamCallback() {
@Override
public void callback(Pointer streamRef, Pointer clientCallBackInfo,
@@ -132,10 +132,10 @@ public void callback(Pointer streamRef, Pointer clientCallBackInfo,
for (var i = 0; i < numEvents; i++) {
var context = path.relativize(Path.of(paths[i]));
- // Note: Multiple "physical" native events might be merged
- // into a single "logical" native event, so the following
- // series of checks should be if-statements (instead of
- // if/else-statements).
+ // Note: Multiple "physical" native events might be
+ // coalesced into a single "logical" native event, so the
+ // following series of checks should be if-statements
+ // (instead of if/else-statements).
if (any(flags[i], ITEM_CREATED.mask)) {
handler.handle(ENTRY_CREATE, context);
}
@@ -157,7 +157,7 @@ private boolean any(int bits, int mask) {
};
}
- private static Pointer createFSEventStream(Path path, FSEventStreamCallback callback) {
+ private Pointer createFSEventStream(FSEventStreamCallback callback) {
try (
var pathsToWatch = new Strings(path.toString());
) {
@@ -170,7 +170,7 @@ private static Pointer createFSEventStream(Path path, FSEventStreamCallback call
}
}
- private static Pointer createDispatchQueue() {
+ private Pointer createDispatchQueue() {
var label = "engineering.swat.watch";
var attr = Pointer.NULL;
return DQ.dispatch_queue_create(label, attr);
From c6f901f48688fc01c927b2f3931e91ede8d4e1bb Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Tue, 22 Apr 2025 09:11:33 +0200
Subject: [PATCH 08/13] Fix exception message
---
.../engineering/swat/watch/impl/mac/NativeEventStream.java | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index 2de79db9..7fc6e98c 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -229,9 +229,10 @@ public Strings(String... strings) {
public CFArrayRef toCFArray() {
if (closed) {
- throw new IllegalStateException("Paths already deallocated");
+ throw new IllegalStateException("Strings are already deallocated");
+ } else {
+ return array;
}
- return array;
}
private static CFStringRef[] createCFStrings(String[] pathsToWatch) {
@@ -267,7 +268,7 @@ private static CFArrayRef createCFArray(CFStringRef[] strings) {
@Override
public void close() {
if (closed) {
- throw new IllegalStateException("Paths already deallocated");
+ throw new IllegalStateException("Strings are already deallocated");
} else {
closed = true;
}
From 9aed20fe11b1bb7683aae0ed3994fa83c318d630 Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Tue, 22 Apr 2025 15:35:41 +0200
Subject: [PATCH 09/13] Enable macOS-specific test only on macOS
---
src/test/java/engineering/swat/watch/impl/mac/APIs.java | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/test/java/engineering/swat/watch/impl/mac/APIs.java b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
index 867fcb4e..a79716fd 100644
--- a/src/test/java/engineering/swat/watch/impl/mac/APIs.java
+++ b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
@@ -44,6 +44,8 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
import com.sun.jna.Memory;
import com.sun.jna.Native;
@@ -59,6 +61,7 @@
import engineering.swat.watch.impl.mac.apis.FileSystemEvents;
import engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag;
+@EnabledOnOs({OS.MAC})
class APIs {
private static final Logger LOGGER = LogManager.getLogger();
From 0798afda779be48f2297e2486259a44339f506d8 Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Tue, 22 Apr 2025 15:37:28 +0200
Subject: [PATCH 10/13] Reduce visibility of classes/interfaces where possible
---
.../engineering/swat/watch/impl/mac/NativeEventHandler.java | 2 +-
.../java/engineering/swat/watch/impl/mac/NativeEventStream.java | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
index 8e79bd68..bf3e88f4 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
@@ -50,6 +50,6 @@
*
*/
@FunctionalInterface
-public interface NativeEventHandler {
+interface NativeEventHandler {
void handle(Kind kind, @Nullable T context);
}
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index 7fc6e98c..b4a05216 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -75,7 +75,7 @@
* these methods are expected to be rarely invoked.
*
*/
-public class NativeEventStream implements Closeable {
+class NativeEventStream implements Closeable {
// Native APIs
private static final CoreFoundation CF = CoreFoundation.INSTANCE;
From 3589680cf9083eb8578ea6a7dd91a93ce3a8e80f Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Tue, 22 Apr 2025 15:40:51 +0200
Subject: [PATCH 11/13] Simplify code in a few places (nits)
---
.../watch/impl/mac/NativeEventStream.java | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index b4a05216..92c31752 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -42,6 +42,7 @@
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.Arrays;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -100,7 +101,7 @@ public NativeEventStream(Path path, NativeEventHandler handler) throws IOExcepti
public synchronized void open() {
if (!closed) {
- throw new IllegalStateException("Stream is already open");
+ return;
} else {
closed = false;
}
@@ -158,9 +159,7 @@ private boolean any(int bits, int mask) {
}
private Pointer createFSEventStream(FSEventStreamCallback callback) {
- try (
- var pathsToWatch = new Strings(path.toString());
- ) {
+ try (var pathsToWatch = new Strings(path.toString())) {
var allocator = CF.CFAllocatorGetDefault();
var context = Pointer.NULL;
var sinceWhen = FSE.FSEventsGetCurrentEventId();
@@ -181,7 +180,7 @@ private Pointer createDispatchQueue() {
@Override
public synchronized void close() {
if (closed) {
- throw new IllegalStateException("Stream is already closed");
+ return;
} else {
closed = true;
}
@@ -236,13 +235,9 @@ public CFArrayRef toCFArray() {
}
private static CFStringRef[] createCFStrings(String[] pathsToWatch) {
- var n = pathsToWatch.length;
-
- var strings = new CFStringRef[n];
- for (int i = 0; i < n; i++) {
- strings[i] = CFStringRef.createCFString(pathsToWatch[i]);
- }
- return strings;
+ return Arrays.stream(pathsToWatch)
+ .map(CFStringRef::createCFString)
+ .toArray(CFStringRef[]::new);
}
private static CFArrayRef createCFArray(CFStringRef[] strings) {
From c22ca447cf8a06e05505e8d63cb96efb40c22d48 Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Tue, 22 Apr 2025 16:00:17 +0200
Subject: [PATCH 12/13] Fix confusing comments about deallocation
---
.../swat/watch/impl/mac/NativeEventStream.java | 8 ++++++--
.../java/engineering/swat/watch/impl/mac/APIs.java | 10 +++++-----
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index 92c31752..b7b970a5 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -84,10 +84,12 @@ class NativeEventStream implements Closeable {
private static final DispatchQueue DQ = DispatchQueue.INSTANCE;
private static final FileSystemEvents FSE = FileSystemEvents.INSTANCE;
- // Native memory (automatically deallocated when set to `null`)
- private @Nullable FSEventStreamCallback callback;
+ // Native memory
+ private @Nullable FSEventStreamCallback callback; // Keep reference to avoid premature GC'ing
private @Nullable Pointer stream;
private @Nullable Pointer queue;
+ // Note: These fields aren't volatile, as all reads/write from/to them are
+ // inside synchronized blocks. Be careful to not break this invariant.
private final Path path;
private final NativeEventHandler handler;
@@ -191,6 +193,8 @@ public synchronized void close() {
FSE.FSEventStreamStop(streamNonNull);
FSE.FSEventStreamSetDispatchQueue(streamNonNull, Pointer.NULL);
FSE.FSEventStreamInvalidate(streamNonNull);
+
+ // Deallocate native memory
FSE.FSEventStreamRelease(streamNonNull);
}
if (queue != null) {
diff --git a/src/test/java/engineering/swat/watch/impl/mac/APIs.java b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
index a79716fd..acab53c5 100644
--- a/src/test/java/engineering/swat/watch/impl/mac/APIs.java
+++ b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
@@ -192,13 +192,13 @@ public void close() throws IOException {
FSE.FSEventStreamStop(stream);
FSE.FSEventStreamSetDispatchQueue(stream, Pointer.NULL);
FSE.FSEventStreamInvalidate(stream);
- FSE.FSEventStreamRelease(stream);
- DO.dispatch_release(queue);
// Deallocate queue, stream, and callback
- this.queue = null;
- this.stream = null;
- this.callback = null;
+ DO.dispatch_release(queue);
+ FSE.FSEventStreamRelease(stream);
+ queue = null;
+ stream = null;
+ callback = null;
}
@FunctionalInterface
From d17f342be700c38bcc6aa6733f728440f7ea5d60 Mon Sep 17 00:00:00 2001
From: Sung-Shik Jongmans
Date: Tue, 22 Apr 2025 16:01:34 +0200
Subject: [PATCH 13/13] Fix Checker Framework idioms
---
.../watch/impl/mac/NativeEventStream.java | 36 +++++++++----------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
index b7b970a5..22cad3a1 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -108,15 +108,17 @@ public synchronized void open() {
closed = false;
}
- // Allocate native memory. (Checker Framework: The local variables are
- // `@NonNull` copies of the `@Nullable` fields.)
- var callback = this.callback = createCallback();
- var stream = this.stream = createFSEventStream(callback);
- var queue = this.queue = createDispatchQueue();
+ // Allocate native memory
+ callback = createCallback();
+ stream = createFSEventStream(callback);
+ queue = createDispatchQueue();
// Start the stream
- FSE.FSEventStreamSetDispatchQueue(stream, queue);
- FSE.FSEventStreamStart(stream);
+ var streamNonNull = stream;
+ if (streamNonNull != null) {
+ FSE.FSEventStreamSetDispatchQueue(streamNonNull, queue);
+ FSE.FSEventStreamStart(streamNonNull);
+ }
}
private FSEventStreamCallback createCallback() {
@@ -187,24 +189,22 @@ public synchronized void close() {
closed = true;
}
- // Stop the stream
- if (stream != null) {
- var streamNonNull = stream; // Checker Framework: `@NonNull` copy of `@Nullable` field
+ var streamNonNull = stream;
+ var queueNonNull = queue;
+ if (streamNonNull != null && queueNonNull != null) {
+
+ // Stop the stream
FSE.FSEventStreamStop(streamNonNull);
FSE.FSEventStreamSetDispatchQueue(streamNonNull, Pointer.NULL);
FSE.FSEventStreamInvalidate(streamNonNull);
// Deallocate native memory
+ DO.dispatch_release(queueNonNull);
FSE.FSEventStreamRelease(streamNonNull);
+ queue = null;
+ stream = null;
+ callback = null;
}
- if (queue != null) {
- DO.dispatch_release(queue);
- }
-
- // Deallocate native memory
- callback = null;
- stream = null;
- queue = null;
}
}