From 0c16dae933a21b2bc313137664e8834a845d68e4 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 21 Apr 2025 09:04:51 +0200 Subject: [PATCH 01/13] Add interfaces to access native macOS APIs (Dispatch Objects; Dispatch Queue; File System Events) --- pom.xml | 10 ++ .../watch/impl/mac/apis/DispatchObjects.java | 21 +++ .../watch/impl/mac/apis/DispatchQueue.java | 23 +++ .../watch/impl/mac/apis/FileSystemEvents.java | 167 ++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 src/main/java/engineering/swat/watch/impl/mac/apis/DispatchObjects.java create mode 100644 src/main/java/engineering/swat/watch/impl/mac/apis/DispatchQueue.java create mode 100644 src/main/java/engineering/swat/watch/impl/mac/apis/FileSystemEvents.java diff --git a/pom.xml b/pom.xml index ffda2a4b..ef5d81a1 100644 --- a/pom.xml +++ b/pom.xml @@ -222,6 +222,16 @@ log4j-core ${log4j.version} + + net.java.dev.jna + jna + 5.16.0 + + + net.java.dev.jna + jna-platform + 5.16.0 + 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 new file mode 100644 index 00000000..79e72435 --- /dev/null +++ b/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchObjects.java @@ -0,0 +1,21 @@ +package engineering.swat.watch.impl.mac.apis; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +/** + * Interface for the "Dispatch Objects" API collection of the "Dispatch" + * framework. + * + * @see https://developer.apple.com/documentation/dispatch/dispatch_objects?language=objc + */ +public interface DispatchObjects extends Library { + DispatchObjects INSTANCE = Native.load("c", DispatchObjects.class); + + /** + * @param object {@code dispatch_object_t} + * @see https://developer.apple.com/documentation/dispatch/1496328-dispatch_release?language=objc + */ + void dispatch_release(Pointer object); +} 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 new file mode 100644 index 00000000..09b34d0e --- /dev/null +++ b/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchQueue.java @@ -0,0 +1,23 @@ +package engineering.swat.watch.impl.mac.apis; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; + +/** + * Interface for the "Dispatch Queue" API collection of the "Dispatch" + * framework. + * + * @see https://developer.apple.com/documentation/dispatch/dispatch_queue?language=objc + */ +public interface DispatchQueue extends Library { + DispatchQueue INSTANCE = Native.load("c", DispatchQueue.class); + + /** + * @param label {@code dispatch_queue_t} + * @param attr {@code const char*} + * @return {@code dispatch_queue_t} + * @see https://developer.apple.com/documentation/dispatch/1453030-dispatch_queue_create?language=objc + */ + Pointer dispatch_queue_create(String label, Pointer attr); +} 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 new file mode 100644 index 00000000..350abb5c --- /dev/null +++ b/src/main/java/engineering/swat/watch/impl/mac/apis/FileSystemEvents.java @@ -0,0 +1,167 @@ +package engineering.swat.watch.impl.mac.apis; + +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.platform.mac.CoreFoundation.CFAllocatorRef; +import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef; +import com.sun.jna.platform.mac.CoreFoundation.CFStringRef; + +/** + * Interface for the "File System Events" API collection of the "Core Services" + * framework. + * + * https://developer.apple.com/documentation/coreservices/file_system_events?language=objc + */ +public interface FileSystemEvents extends Library { + FileSystemEvents INSTANCE = Native.load("CoreServices", FileSystemEvents.class); + + // -- Functions -- + + /** + * @param allocator {@code CFAllocator} + * @param callback {@code FSEventStreamCallback} + * @param context {@code FSEventStreamContext} + * @param pathsToWatch {@code CFArray} + * @param sinceWhen {@code FSEventStreamEventId} + * @param latency {@code CFTimeInterval} + * @param flags {@code FSEventStreamCreateFlags} + * @return {@code FSEventStreamRef} + * @see https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate?language=objc + */ + Pointer FSEventStreamCreate(CFAllocatorRef allocator, FSEventStreamCallback callback, + Pointer context, CFArrayRef pathsToWatch, long sinceWhen, double latency, int flags); + + /** + * @param streamRef {@code FSEventStreamRef} + * @see https://developer.apple.com/documentation/coreservices/1446990-fseventstreaminvalidate?language=objc + */ + void FSEventStreamInvalidate(Pointer streamRef); + + /** + * @param streamRef {@code FSEventStreamRef} + * @see https://developer.apple.com/documentation/coreservices/1445989-fseventstreamrelease?language=objc + */ + void FSEventStreamRelease(Pointer streamRef); + + /** + * @param streamRef {@code FSEventStreamRef} + * @param q {@code dispatch_queue_t} + * @see https://developer.apple.com/documentation/coreservices/1444164-fseventstreamsetdispatchqueue?language=objc + */ + void FSEventStreamSetDispatchQueue(Pointer streamRef, Pointer q); + + /** + * @param streamRef {@code FSEventStreamRef} + * @see https://developer.apple.com/documentation/coreservices/1444302-fseventstreamshow?language=objc + */ + boolean FSEventStreamShow(Pointer streamRef); + + /** + * @param streamRef {@code FSEventStreamRef} + * @return {@code Boolean} + * @see https://developer.apple.com/documentation/coreservices/1448000-fseventstreamstart?language=objc + */ + boolean FSEventStreamStart(Pointer streamRef); + + /** + * @param streamRef {@code FSEventStreamRef} + * @see https://developer.apple.com/documentation/coreservices/1447673-fseventstreamstop?language=objc + */ + void FSEventStreamStop(Pointer streamRef); + + /** + * @return {@code FSEventStreamEventId} + * @see https://developer.apple.com/documentation/coreservices/1442917-fseventsgetcurrenteventid?language=objc + */ + long FSEventsGetCurrentEventId(); + + // -- Enumerations -- + + /** + * @see https://developer.apple.com/documentation/coreservices/1455376-fseventstreamcreateflags?language=objc + */ + static enum FSEventStreamCreateFlag { + NONE (0x00000000), + USE_CF_TYPES (0x00000001), + NO_DEFER (0x00000002), + WATCH_ROOT (0x00000004), + IGNORE_SELF (0x00000008), + FILE_EVENTS (0x00000010), + MARK_SELF (0x00000020), + FULL_HISTORY (0x00000080), + USE_EXTENDED_DATA(0x00000040), + WITH_DOC_ID (0x00000100); + + public final int mask; + + private FSEventStreamCreateFlag(int mask) { + this.mask = mask; + } + } + + /** + * @see https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags?language=objc + */ + static enum FSEventStreamEventFlag { + NONE (0x00000000), + MUST_SCAN_SUB_DIRS (0x00000001), + USER_DROPPED (0x00000002), + KERNEL_DROPPED (0x00000004), + EVENT_IDS_WRAPPED (0x00000008), + HISTORY_DONE (0x00000010), + ROOT_CHANGED (0x00000020), + MOUNT (0x00000040), + UNMOUNT (0x00000080), + ITEM_CHANGE_OWNER (0x00004000), + ITEM_CREATED (0x00000100), + ITEM_FINDER_INFO_MOD (0x00002000), + ITEM_INODE_META_MOD (0x00000400), + ITEM_IS_DIR (0x00020000), + ITEM_IS_FILE (0x00010000), + ITEM_IS_HARD_LINK (0x00100000), + ITEM_IS_LAST_HARD_LINK(0x00200000), + ITEM_IS_SYMLINK (0x00040000), + ITEM_MODIFIED (0x00001000), + ITEM_REMOVED (0x00000200), + ITEM_RENAMED (0x00000800), + ITEM_XATTR_MOD (0x00008000), + OWN_EVENT (0x00080000), + ITEM_CLONED (0x00400000); + + public final int mask; + + private FSEventStreamEventFlag(int mask) { + this.mask = mask; + } + } + + // -- Data types -- + + static interface FSEventStreamCallback extends Callback { + /** + * @param streamRef {@code ConstFSEventStreamRef} + * @param clientCallBackInfo {@code void*} + * @param numEvents {@code size_t} + * @param eventPaths {@code void*} + * @param eventFlags {@code FSEventStreamEventFlags*} + * @param eventIds {@code FSEventStreamEventIds*} + * @see https://developer.apple.com/documentation/coreservices/fseventstreamcallback?language=objc + */ + void callback(Pointer streamRef, Pointer clientCallBackInfo, + long numEvents, Pointer eventPaths, Pointer eventFlags, Pointer eventIds); + } + + // -- Constants -- + + /** + * @see https://developer.apple.com/documentation/coreservices/kfseventstreameventextendeddatapathkey?language=objc + */ + static final CFStringRef kFSEventStreamEventExtendedDataPathKey = CFStringRef.createCFString("path"); + + /** + * @see https://developer.apple.com/documentation/coreservices/kfseventstreameventextendedfileidkey?language=objc + */ + static final CFStringRef kFSEventStreamEventExtendedFileIDKey = CFStringRef.createCFString("fileID"); +} From c063710e76c6e6b127c9128811fb8428518e08b4 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 21 Apr 2025 09:07:18 +0200 Subject: [PATCH 02/13] Add smoke test (and a corresponding auxiliary `main`) to start/stop an FS event stream using the native macOS APIs --- .../engineering/swat/watch/impl/mac/APIs.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/test/java/engineering/swat/watch/impl/mac/APIs.java diff --git a/src/test/java/engineering/swat/watch/impl/mac/APIs.java b/src/test/java/engineering/swat/watch/impl/mac/APIs.java new file mode 100644 index 00000000..d5b5ee2e --- /dev/null +++ b/src/test/java/engineering/swat/watch/impl/mac/APIs.java @@ -0,0 +1,180 @@ +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 org.awaitility.Awaitility.await; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.Test; + +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.TestDirectory; +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.FSEventStreamEventFlag; + +class APIs { + private static final Logger LOGGER = LogManager.getLogger(); + + // 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; + + @Test + void smokeTest() throws IOException { + try (var test = new TestDirectory()) { + var ready = new AtomicBoolean(false); + var paths = ConcurrentHashMap. newKeySet(); + + var s = test.getTestDirectory().toString(); + var handler = (MinimalWorkingExample.EventHandler) (path, flags, id) -> { + synchronized (ready) { + while (!ready.get()) { + try { + ready.wait(); + } catch (InterruptedException e) { + LOGGER.error("Unexpected interrupt. Test likely to fail. Event ignored ({}).", prettyPrint(path, flags, id)); + Thread.currentThread().interrupt(); + return; + } + } + } + paths.remove(path); + }; + + try (var mwe = new MinimalWorkingExample(s, handler)) { + var dir = test.getTestDirectory().toRealPath(); + paths.add(Files.writeString(dir.resolve("a.txt"), "foo").toString()); + paths.add(Files.writeString(dir.resolve("b.txt"), "bar").toString()); + paths.add(Files.createFile(dir.resolve("d.txt")).toString()); + + synchronized (ready) { + ready.set(true); + ready.notifyAll(); + } + + await("The event handler has been called").until(paths::isEmpty); + } + } + } + + public static void main(String[] args) throws IOException { + var s = "/Users/sungshik/Desktop/tmp"; + var handler = (MinimalWorkingExample.EventHandler) (path, flags, id) -> { + LOGGER.info(prettyPrint(path, flags, id)); + }; + + try (var mwe = new MinimalWorkingExample(s, handler)) { + // Block the program from terminating until `ENTER` is pressed + new BufferedReader(new InputStreamReader(System.in)).readLine(); + } + } + + private static String prettyPrint(String path, int flags, long id) { + var flagsPrettyPrinted = Stream + .of(FSEventStreamEventFlag.values()) + .filter(f -> (f.mask & flags) == f.mask) + .map(Object::toString) + .collect(Collectors.joining(", ")); + + var format = "path: \"%s\", flags: [%s], id: %s"; + return String.format(format, path, flagsPrettyPrinted, id); + } + + private static class MinimalWorkingExample implements Closeable { + FileSystemEvents.FSEventStreamCallback callback; + Pointer stream; + Pointer queue; + + public MinimalWorkingExample(String s, EventHandler handler) { + + // Allocate singleton array of paths + CFStringRef pathToWatch = CFStringRef.createCFString(s); + CFArrayRef pathsToWatch = null; + { + var values = new Memory(Native.getNativeSize(CFStringRef.class)); + values.setPointer(0, pathToWatch.getPointer()); + pathsToWatch = CF.CFArrayCreate( + CF.CFAllocatorGetDefault(), + values, + new CFIndex(1), + null); + } // Automatically free `values` when it goes out of scope + + // Allocate callback + this.callback = (x1, x2, x3, x4, x5, x6) -> { + var paths = x4.getStringArray(0, (int) x3); + var flags = x5.getIntArray(0, (int) x3); + var ids = x6.getLongArray(0, (int) x3); + for (int i = 0; i < x3; i++) { + handler.handle(paths[i], flags[i], ids[i]); + } + }; + + // Allocate stream + this.stream = FSE.FSEventStreamCreate( + CF.CFAllocatorGetDefault(), + callback, + Pointer.NULL, + pathsToWatch, + FSE.FSEventsGetCurrentEventId(), + 0.15, + NO_DEFER.mask | WATCH_ROOT.mask | FILE_EVENTS.mask); + + // Deallocate array of paths + pathsToWatch.release(); + pathToWatch.release(); + + // Allocate queue + this.queue = DQ.dispatch_queue_create("q", null); + + // Start stream + FSE.FSEventStreamSetDispatchQueue(stream, queue); + FSE.FSEventStreamStart(stream); + FSE.FSEventStreamShow(stream); + } + + @Override + public void close() throws IOException { + + // Stop stream + 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; + } + + @FunctionalInterface + private static interface EventHandler { + void handle(String path, int flags, long id); + } + } +} From 9e0a0c40ff53253f3965b374ac1f2b253f67383d Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Mon, 21 Apr 2025 09:08:55 +0200 Subject: [PATCH 03/13] Add a facade-like class to open/close an FS event stream without exposing the native APIs to the caller --- .../watch/impl/mac/NativeEventHandler.java | 28 ++ .../watch/impl/mac/NativeEventStream.java | 258 ++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java create mode 100644 src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java new file mode 100644 index 00000000..b1f9d28c --- /dev/null +++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java @@ -0,0 +1,28 @@ +package engineering.swat.watch.impl.mac; + +import java.nio.file.WatchEvent; +import java.nio.file.WatchEvent.Kind; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + *

+ * 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} and + * {@code Object} for overflows. This precision is needed to construct + * {@link WatchEvent}s, where the types of {@code kind} and {@code context} + * are correlated. Note: {@link java.util.function.BiConsumer} doesn't give + * the required precision (i.e., its type parameters are initialized only + * once for all calls). + *

+ */ +@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} and * {@code Object} for overflows. This precision is needed to construct - * {@link WatchEvent}s, where the types of {@code kind} and {@code context} - * are correlated. Note: {@link java.util.function.BiConsumer} doesn't give - * the required precision (i.e., its type parameters are initialized only - * once for all calls). + * {@link WatchEvent}s, where the types of {@code kind} and {@code context} need + * to be correlated. Note: {@link java.util.function.BiConsumer} doesn't give + * the required precision (i.e., its type parameters are initialized only once + * for all invocations). *

*/ @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; } }