diff --git a/.editorconfig b/.editorconfig
index 1f494b0c..fec076ea 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -15,3 +15,6 @@ end_of_line = lf
[*.java]
indent_size = 4
max_line_length = 120
+
+[*.rs]
+indent_size = 4
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 323a962e..e0d3254a 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -32,6 +32,9 @@ jobs:
java-version: ${{ matrix.jdk }}
distribution: 'temurin'
cache: 'maven'
+ #- uses: actions-rust-lang/setup-rust-toolchain@v1
+ - run: ./update-rust-jni-libs.sh -r
+ if: ${{ matrix.os.image == 'macos-latest' }}
- name: test
run: mvn -B clean test "-Dwatch.mac.backend=${{ matrix.os.mac-backend }}"
diff --git a/.gitignore b/.gitignore
index 5ef0a829..f0df1881 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,4 @@ replay_pid*
# release plugin state files
/pom.xml.releaseBackup
-/release.properties
\ No newline at end of file
+/release.properties
diff --git a/pom.xml b/pom.xml
index 53c096f7..fe7fc25e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,13 +74,20 @@
3.49.5
5.13.4
2.25.1
- 5.17.0
11
11
fsevents
+
+
+ src/main/resources
+
+ src/main/rust/**/*.*
+
+
+
org.apache.maven.plugins
@@ -147,6 +154,9 @@
+
+ DOUBLESLASH_STYLE
+
@@ -170,6 +180,11 @@
+
+
+ src/main/resources/**
+
+
org.apache.maven.plugins
@@ -229,16 +244,6 @@
${log4j.version}
test
-
- net.java.dev.jna
- jna
- ${jna.version}
-
-
- net.java.dev.jna
- jna-platform
- ${jna.version}
-
diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKPoller.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKPoller.java
index 22f947bb..0a1c4a03 100644
--- a/src/main/java/engineering/swat/watch/impl/jdk/JDKPoller.java
+++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKPoller.java
@@ -56,6 +56,7 @@
import engineering.swat.watch.DaemonThreadPool;
import engineering.swat.watch.impl.mac.MacWatchService;
+import engineering.swat.watch.impl.mac.NativeLibrary;
import engineering.swat.watch.impl.util.SubscriptionKey;
/**
@@ -189,7 +190,7 @@ public Watchable newWatchable(Path path) {
static final Platform CURRENT = current(); // Assumption: the platform doesn't change
private static Platform current() {
- if (com.sun.jna.Platform.isMac()) {
+ if (NativeLibrary.isMac()) {
var key = "engineering.swat.java-watch.mac";
var val = System.getProperty(key);
if (val != null) {
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 bf3e88f4..5fe5297a 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventHandler.java
@@ -26,8 +26,13 @@
*/
package engineering.swat.watch.impl.mac;
+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.nio.file.Path;
import java.nio.file.WatchEvent;
-import java.nio.file.WatchEvent.Kind;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -51,5 +56,36 @@
*/
@FunctionalInterface
interface NativeEventHandler {
- void handle(Kind kind, @Nullable T context);
+ void handle(java.nio.file.WatchEvent.Kind kind, @Nullable T context);
+
+ default void handle(int kindOrdinal, String rootPath, String relativePath) {
+ if (kindOrdinal < Kind.values().length) {
+ var kind = Kind.values()[kindOrdinal];
+ switch (kind) {
+ case CREATE:
+ handle(ENTRY_CREATE, toContext(rootPath, relativePath));
+ break;
+ case MODIFY:
+ handle(ENTRY_MODIFY, toContext(rootPath, relativePath));
+ break;
+ case DELETE:
+ handle(ENTRY_DELETE, toContext(rootPath, relativePath));
+ break;
+ case OVERFLOW:
+ handle(OVERFLOW, null);
+ break;
+ }
+ }
+ }
+
+ static Path toContext(String rootPath, String relativePath) {
+ return Path.of(rootPath).relativize(Path.of(relativePath));
+ }
+}
+
+enum Kind { // Order of values needs to be consistent with enum `Kind` in Rust
+ OVERFLOW,
+ CREATE,
+ DELETE,
+ MODIFY;
}
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 58856731..25b6071d 100644
--- a/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java
@@ -26,45 +26,13 @@
*/
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.ITEM_RENAMED;
-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.Files;
import java.nio.file.Path;
-import java.util.Arrays;
-
-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 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.
+// Note: This file is designed to be the only place in this package where native
+// APIs are invoked. If the need to do so arises outside this file, consider
+// extending this file to offer the required services without the native APIs.
/**
*
@@ -79,23 +47,14 @@
*
*/
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
- 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.
+ static {
+ NativeLibrary.load();
+ }
private final Path path;
private final NativeEventHandler handler;
private volatile boolean closed;
+ private volatile long nativeWatch;
public NativeEventStream(Path path, NativeEventHandler handler) throws IOException {
this.path = path.toRealPath(); // Resolve symbolic links
@@ -110,88 +69,7 @@ public synchronized void open() {
closed = false;
}
- // Allocate native memory
- callback = createCallback();
- stream = createFSEventStream(callback);
- queue = createDispatchQueue();
-
- // Start the stream
- var streamNonNull = stream;
- if (streamNonNull != null) {
- FSE.FSEventStreamSetDispatchQueue(streamNonNull, queue);
- FSE.FSEventStreamStart(streamNonNull);
- }
- }
-
- private FSEventStreamCallback createCallback() {
- 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
- // 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);
- }
- 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);
- }
- if (any(flags[i], ITEM_RENAMED.mask)) {
- // For now, check if the file exists to determine if the
- // event pertains to the target of the rename (if it
- // exists) or to the source (else). This is an
- // approximation. It might be more accurate to maintain
- // an internal index (but getting the concurrency right
- // requires care).
- if (Files.exists(Path.of(paths[i]))) {
- handler.handle(ENTRY_CREATE, context);
- } else {
- handler.handle(ENTRY_DELETE, context);
- }
- }
- }
- }
-
- private boolean any(int bits, int mask) {
- return (bits & mask) != 0;
- }
- };
- }
-
- private Pointer createFSEventStream(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 Pointer createDispatchQueue() {
- var label = "engineering.swat.watch";
- var attr = Pointer.NULL;
- return DQ.dispatch_queue_create(label, attr);
+ nativeWatch = NativeLibrary.start(path.toString(), handler);
}
// -- Closeable --
@@ -204,97 +82,6 @@ public synchronized void close() {
closed = true;
}
- 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;
- }
- }
-}
-
-/**
- * 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("Strings are already deallocated");
- } else {
- return array;
- }
- }
-
- private static CFStringRef[] createCFStrings(String[] pathsToWatch) {
- return Arrays.stream(pathsToWatch)
- .map(CFStringRef::createCFString)
- .toArray(CFStringRef[]::new);
- }
-
- 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("Strings are already deallocated");
- } else {
- closed = true;
- }
-
- // Deallocate native memory
- for (var s : strings) {
- if (s != null) {
- s.release();
- }
- }
- if (array != null) {
- array.release();
- }
+ NativeLibrary.stop(nativeWatch);
}
}
diff --git a/src/main/java/engineering/swat/watch/impl/mac/NativeLibrary.java b/src/main/java/engineering/swat/watch/impl/mac/NativeLibrary.java
new file mode 100644
index 00000000..7b8eeed3
--- /dev/null
+++ b/src/main/java/engineering/swat/watch/impl/mac/NativeLibrary.java
@@ -0,0 +1,103 @@
+/*
+ * 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 java.nio.file.attribute.PosixFilePermission.*;
+
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+
+public class NativeLibrary {
+
+ public static native long start(String path, NativeEventHandler handler);
+ public static native void stop(long watchId);
+
+ public static boolean isMac() {
+ var os = System.getProperty("os.name");
+ return os != null && (os.toLowerCase().contains("mac") || os.toLowerCase().contains("darwin"));
+ }
+
+ private static boolean isAarch64() {
+ var arch = System.getProperty("os.arch");
+ return arch != null && arch.toLowerCase().equals("aarch64");
+ }
+
+ private static volatile boolean loaded = false;
+ public static void load() {
+ if (loaded) {
+ return;
+ }
+ try {
+ if (!isMac()) {
+ throw new IllegalStateException("We should not be loading FileSystemEvents api on non mac machines");
+ }
+ String path = "/engineering/swat/watch/jni/";
+ if (isAarch64()) {
+ path += "macos-aarch64/";
+ }
+ else {
+ path += "macos-x64/";
+ }
+ path += "librust_fsevents_jni.dylib";
+
+ loadLibrary(path);
+ } finally {
+ loaded = true;
+ }
+ }
+
+ private static FileAttribute> PRIVATE_FILE = PosixFilePermissions.asFileAttribute(Set.of(OWNER_READ, OWNER_WRITE , OWNER_EXECUTE));
+
+ private static void loadLibrary(String path) {
+ try {
+ var localFile = NativeLibrary.class.getResource(path);
+ if (localFile != null && localFile.getProtocol().equals("file")) {
+ System.load(localFile.getPath());
+ return;
+ }
+ // in most cases the file is inside of a jar
+ // so we have to copy it out and load that file instead
+ var localCopy = Files.createTempFile("watch", ".dylib", PRIVATE_FILE);
+ localCopy.toFile().deleteOnExit();
+ try (var libStream = NativeLibrary.class.getResourceAsStream(path)) {
+ if (libStream != null) {
+ try (var writer = Files.newOutputStream(localCopy, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
+ libStream.transferTo(writer);
+ }
+ System.load(localCopy.toString());
+ }
+ }
+ }
+ catch (Throwable e) {
+ throw new IllegalStateException("We could not load: " + path, e);
+ }
+ }
+}
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
deleted file mode 100644
index 41bb5ee6..00000000
--- a/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchObjects.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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;
-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
deleted file mode 100644
index 747d87a5..00000000
--- a/src/main/java/engineering/swat/watch/impl/mac/apis/DispatchQueue.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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;
-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
deleted file mode 100644
index d81a0447..00000000
--- a/src/main/java/engineering/swat/watch/impl/mac/apis/FileSystemEvents.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * 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;
-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");
-}
diff --git a/src/main/resources/.gitignore b/src/main/resources/.gitignore
new file mode 100644
index 00000000..e5d04671
--- /dev/null
+++ b/src/main/resources/.gitignore
@@ -0,0 +1 @@
+*.dylib
diff --git a/src/main/rust/.gitignore b/src/main/rust/.gitignore
new file mode 100644
index 00000000..9552259c
--- /dev/null
+++ b/src/main/rust/.gitignore
@@ -0,0 +1,7 @@
+/target
+/debug
+**/*.rs.bk
+*.pdb
+*.log
+*.d
+*.dylib
diff --git a/src/main/rust/Cargo.lock b/src/main/rust/Cargo.lock
new file mode 100644
index 00000000..043a44e3
--- /dev/null
+++ b/src/main/rust/Cargo.lock
@@ -0,0 +1,353 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "dispatch2"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "fsevent-sys"
+version = "5.0.0"
+source = "git+https://github.com/octplane/fsevent-rust.git?rev=refs%2Fpull%2F44%2Fhead#93167444410b3576d6c49ae773becde1d6b45b37"
+dependencies = [
+ "core-foundation 0.9.4",
+ "dispatch2",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rust-fsevents-jni"
+version = "0.1.0"
+dependencies = [
+ "core-foundation 0.10.1",
+ "dispatch2",
+ "fsevent-sys",
+ "jni",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/src/main/rust/Cargo.toml b/src/main/rust/Cargo.toml
new file mode 100644
index 00000000..96c7d670
--- /dev/null
+++ b/src/main/rust/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "rust-fsevents-jni"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type = ["cdylib"]
+
+[profile.release]
+strip = true
+lto = true
+codegen-units = 1
+
+[dependencies]
+# we need this PR as it contains an updated version that targets core-foundation
+fsevent-sys = { git = "https://github.com/octplane/fsevent-rust.git", rev= "refs/pull/44/head"}
+core-foundation = "0.10.1"
+# we need this specific version of dispatch2, that still exposes the native type
+dispatch2 = { version = "0.2.0", default-features = false, features = ["alloc"] }
+jni = "0.21.1"
diff --git a/src/main/rust/src/fs_monitor.rs b/src/main/rust/src/fs_monitor.rs
new file mode 100644
index 00000000..a41b270e
--- /dev/null
+++ b/src/main/rust/src/fs_monitor.rs
@@ -0,0 +1,182 @@
+//
+// 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.
+//
+
+#![cfg(target_os = "macos")]
+#![deny(
+ trivial_numeric_casts,
+ unstable_features,
+ unused_import_braces,
+ unused_qualifications
+)]
+
+use std::{
+ ffi::CStr,
+ ptr,
+ fs,
+ os::raw::{c_char, c_void},
+ sync::atomic::{AtomicBool, Ordering},
+};
+use dispatch2::ffi::{dispatch_object_t, dispatch_queue_t, DISPATCH_QUEUE_SERIAL, dispatch_queue_create};
+use core_foundation::{
+ array::CFArray,
+ base::{kCFAllocatorDefault, TCFType},
+ string::CFString,
+};
+use fsevent_sys::{self as fse};
+
+pub enum Kind { // Ordinals need to be consistent with enum `Kind` in Java
+ OVERFLOW=0,
+ CREATE=1,
+ DELETE=2,
+ MODIFY=3,
+}
+
+pub struct NativeEventStream {
+ since_when: fse::FSEventStreamEventId,
+ closed: AtomicBool,
+ path : CFArray,
+ queue: dispatch_queue_t,
+ stream: Option,
+ info: *mut ContextInfo,
+}
+
+impl NativeEventStream {
+ pub fn new(path: String, handler: impl Fn(Kind, &String) + 'static) -> Self {
+ Self {
+ since_when: unsafe { fse::FSEventsGetCurrentEventId() },
+ closed: AtomicBool::new(false),
+ path: CFArray::from_CFTypes(&[CFString::new(&path)]),
+ queue: unsafe { dispatch_queue_create(ptr::null(), DISPATCH_QUEUE_SERIAL) },
+ stream: None,
+ info: Box::into_raw(Box::new(ContextInfo{ handler: Box::new(handler) })),
+ }
+ }
+
+ pub fn start(&mut self) {
+ unsafe {
+ let stream = fse::FSEventStreamCreate(
+ kCFAllocatorDefault,
+ callback,
+ &fse::FSEventStreamContext {
+ version: 0,
+ info: self.info as *mut _,
+ retain: None,
+ release: Some(release_context),
+ copy_description: None
+ },
+ self.path.as_concrete_TypeRef(),
+ self.since_when,
+ 0.15,
+ FLAGS);
+
+ self.stream = Some(stream);
+
+ fse::FSEventStreamSetDispatchQueue(stream, self.queue);
+ fse::FSEventStreamStart(stream);
+ };
+ }
+
+ pub fn stop(&self) {
+ if self.closed.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() {
+ return; // The stream has already been closed
+ }
+ match self.stream {
+ Some(stream) => unsafe{
+ fse::FSEventStreamStop(stream);
+ fse::FSEventStreamSetDispatchQueue(stream, ptr::null_mut());
+ fse::FSEventStreamInvalidate(stream);
+ dispatch2::ffi::dispatch_release(self.queue as dispatch_object_t);
+ fse::FSEventStreamRelease(stream);
+ }
+ None => unsafe {
+ dispatch2::ffi::dispatch_release(self.queue as dispatch_object_t);
+ }
+ };
+ }
+}
+
+struct ContextInfo {
+ handler: Box,
+}
+
+const FLAGS : fse::FSEventStreamCreateFlags
+ = fse::kFSEventStreamCreateFlagNoDefer
+ | fse::kFSEventStreamCreateFlagWatchRoot
+ | fse::kFSEventStreamCreateFlagFileEvents;
+
+extern "C" fn release_context(info: *mut c_void) {
+ let ctx_ptr = info as *mut ContextInfo;
+ unsafe{ drop(Box::from_raw( ctx_ptr)); }
+}
+
+extern "C" fn callback(
+ _stream_ref: fse::FSEventStreamRef,
+ info: *mut c_void,
+ num_events: usize,
+ event_paths: *mut c_void,
+ event_flags: *const fse::FSEventStreamEventFlags,
+ _event_ids: *const fse::FSEventStreamEventId,
+) {
+ let info = unsafe{ &mut *(info as *mut ContextInfo) };
+ let handler = info.handler.as_ref();
+
+ let event_paths = event_paths as *const *const c_char;
+ for i in 0..num_events {
+ // TODO: We're currently going from C strings to Rust strings to JNI
+ // strings. If possible, go directly from C strings to JNI strings.
+ let path = unsafe { CStr::from_ptr(*event_paths.add(i)).to_str().unwrap().to_string() };
+ let flags: fse::FSEventStreamEventFlags = unsafe { *event_flags.add(i) };
+
+ // 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 flags & fse::kFSEventStreamEventFlagItemCreated != 0 {
+ handler(Kind::CREATE, &path);
+ }
+ if flags & fse::kFSEventStreamEventFlagItemRemoved != 0 {
+ handler(Kind::DELETE, &path);
+ }
+ if flags & (fse::kFSEventStreamEventFlagItemModified | fse::kFSEventStreamEventFlagItemInodeMetaMod) != 0 {
+ handler(Kind::MODIFY, &path);
+ }
+ if flags & fse::kFSEventStreamEventFlagMustScanSubDirs != 0 {
+ handler(Kind::OVERFLOW, &path);
+ }
+ if flags & fse::kFSEventStreamEventFlagItemRenamed != 0 {
+ // For now, check if the file exists to determine if the event
+ // pertains to the target of the rename (if it exists) or to the
+ // source (else). This is an approximation. It might be more
+ // accurate to maintain an internal index (but getting the
+ // concurrency right requires care).
+ if fs::exists(&path).unwrap_or(false) {
+ handler(Kind::CREATE, &path);
+ } else {
+ handler(Kind::DELETE, &path);
+ }
+ }
+ }
+}
diff --git a/src/main/rust/src/lib.rs b/src/main/rust/src/lib.rs
new file mode 100644
index 00000000..b7acbd3d
--- /dev/null
+++ b/src/main/rust/src/lib.rs
@@ -0,0 +1,104 @@
+//
+// 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.
+//
+
+mod fs_monitor;
+
+use jni::{Executor, JNIEnv};
+use jni::objects::{GlobalRef, JClass, JMethodID, JObject, JString, JValue};
+use jni::sys::{jint, jlong};
+
+use crate::fs_monitor::{Kind, NativeEventStream};
+
+#[allow(dead_code)]
+struct HandlerExecutor {
+ executor: Executor,
+ obj: GlobalRef,
+ method: JMethodID,
+ path: String,
+ class: GlobalRef, // Ensure the reference to the class (of `obj`) isn't lost
+}
+
+impl HandlerExecutor {
+ pub fn new<'local>(env: &mut JNIEnv<'local>, path: String, obj: JObject<'local>) -> Result {
+ let executor = Executor::new(Into::into(env.get_java_vm()?));
+ let obj = env.new_global_ref(obj)?;
+ let class = env.new_global_ref(env.get_object_class(&obj)?)?;
+ let method = env.get_method_id(
+ &class, "handle", "(ILjava/lang/String;Ljava/lang/String;)V")?;
+
+ Ok(Self { executor, obj, method, path, class })
+ }
+
+ pub fn execute<'local>(&self, kind: Kind, path: &String) {
+ self.executor.with_attached(|env: &mut JNIEnv<'_>| -> Result<(), jni::errors::Error> {
+ unsafe {
+ env.call_method_unchecked(
+ self.obj.as_obj(),
+ self.method,
+ jni::signature::ReturnType::Primitive(jni::signature::Primitive::Void),
+ &[
+ JValue::from(kind as jint).as_jni(),
+ JValue::from(&env.new_string(&self.path).unwrap()).as_jni(),
+ JValue::from(&env.new_string(&path).unwrap()).as_jni(),
+ ]
+ )?;
+ }
+ Ok(())
+ }).unwrap();
+ }
+}
+
+#[unsafe(no_mangle)]
+#[allow(unused_variables)]
+pub extern "system" fn Java_engineering_swat_watch_impl_mac_NativeLibrary_start<'local>(
+ mut env: JNIEnv<'local>,
+ class: JClass<'local>,
+ path: JString<'local>,
+ handler: JObject<'local>,
+) -> jlong
+{
+ let path: String = env.get_string(&path).expect("Should not fail to get string").into();
+ let handler_executor = HandlerExecutor::new(&mut env, path.clone(), handler).unwrap();
+ let handler = move |kind: Kind, path: &String| handler_executor.execute(kind, path);
+ let mut mon = NativeEventStream::new(path, handler);
+ mon.start();
+ Box::into_raw(Box::new(mon)) as jlong
+}
+
+#[unsafe(no_mangle)]
+#[allow(unused_variables)]
+pub extern "system" fn Java_engineering_swat_watch_impl_mac_NativeLibrary_stop<'local>(
+ env: JNIEnv<'local>,
+ class: JClass<'local>,
+ stream: jlong,
+)
+{
+ let mon_ptr = stream as *mut NativeEventStream;
+ let mon = unsafe { Box::from_raw(mon_ptr) };
+ mon.stop();
+ // After this, the mon will be released, as it has been taken out of the box
+}
diff --git a/src/test/java/engineering/swat/watch/impl/mac/APIs.java b/src/test/java/engineering/swat/watch/impl/mac/APIs.java
deleted file mode 100644
index 4b408b1a..00000000
--- a/src/test/java/engineering/swat/watch/impl/mac/APIs.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/*
- * 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;
-import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.NO_DEFER;
-import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.USE_CF_TYPES;
-import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.USE_EXTENDED_DATA;
-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 org.junit.jupiter.api.condition.EnabledOnOs;
-import org.junit.jupiter.api.condition.OS;
-
-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.CFDictionaryRef;
-import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
-import com.sun.jna.platform.mac.CoreFoundation.CFNumberRef;
-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;
-
-@EnabledOnOs({OS.MAC})
-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, inode, 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, inode, flags, id));
- Thread.currentThread().interrupt();
- return;
- }
- }
- }
- paths.remove(path);
- };
-
- try (var mwe = new MinimalWorkingExample(s, handler, true)) {
- 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 = args[0];
- var handler = (MinimalWorkingExample.EventHandler) (path, inode, flags, id) -> {
- LOGGER.info(prettyPrint(path, inode, flags, id));
- };
- var useExtendedData = args.length >= 2 && Boolean.parseBoolean(args[1]);
-
- try (var mwe = new MinimalWorkingExample(s, handler, useExtendedData)) {
- // Block the program from terminating until `ENTER` is pressed
- new BufferedReader(new InputStreamReader(System.in)).readLine();
- }
- }
-
- private static String prettyPrint(String path, long inode, 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\", inode: %s, flags: [%s], id: %s";
- return String.format(format, path, inode, flagsPrettyPrinted, id);
- }
-
- private static class MinimalWorkingExample implements Closeable {
- private FileSystemEvents.FSEventStreamCallback callback;
- private Pointer stream;
- private Pointer queue;
-
- public MinimalWorkingExample(String s, EventHandler handler, boolean useExtendedData) {
-
- // 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 inodes = new long[(int) x3];
- var flags = x5.getIntArray(0, (int) x3);
- var ids = x6.getLongArray(0, (int) x3);
-
- if (useExtendedData) {
- var extendedData = new CFArrayRef(x4);
- for (int i = 0; i < x3; i++) {
- var dictionary = new CFDictionaryRef(extendedData.getValueAtIndex(i));
- var dictionaryPath = dictionary.getValue(FileSystemEvents.kFSEventStreamEventExtendedDataPathKey);
- var dictionaryInode = dictionary.getValue(FileSystemEvents.kFSEventStreamEventExtendedFileIDKey);
- paths[i] = dictionaryPath == null ? null : new CFStringRef(dictionaryPath).stringValue();
- inodes[i] = dictionaryInode == null ? 0 : new CFNumberRef(dictionaryInode).longValue();
- }
- }
-
- for (int i = 0; i < x3; i++) {
- handler.handle(paths[i], inodes[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 |
- (useExtendedData ? USE_EXTENDED_DATA.mask | USE_CF_TYPES.mask : 0));
-
- // 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);
-
- // Deallocate queue, stream, and callback
- DO.dispatch_release(queue);
- FSE.FSEventStreamRelease(stream);
- queue = null;
- stream = null;
- callback = null;
- }
-
- @FunctionalInterface
- private static interface EventHandler {
- void handle(String path, long inode, int flags, long id);
- }
- }
-}
diff --git a/src/test/java/engineering/swat/watch/impl/mac/NativeEventStreamTests.java b/src/test/java/engineering/swat/watch/impl/mac/NativeEventStreamTests.java
new file mode 100644
index 00000000..174ac5fb
--- /dev/null
+++ b/src/test/java/engineering/swat/watch/impl/mac/NativeEventStreamTests.java
@@ -0,0 +1,86 @@
+/*
+ * 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 org.awaitility.Awaitility.await;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.WatchEvent.Kind;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.awaitility.Awaitility;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import engineering.swat.watch.TestDirectory;
+import engineering.swat.watch.TestHelper;
+
+@EnabledOnOs({OS.MAC})
+public class NativeEventStreamTests {
+
+ private TestDirectory testDir;
+
+ @BeforeEach
+ void setup() throws IOException {
+ testDir = new TestDirectory();
+ }
+
+ @AfterEach
+ void cleanup() {
+ if (testDir != null) {
+ testDir.close();
+ }
+ }
+
+ @BeforeAll
+ static void setupEverything() {
+ Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT);
+ }
+
+ @Test
+ void signalsAreSent() throws IOException, InterruptedException {
+ var signaled = new AtomicBoolean(false);
+ try (var stream = new NativeEventStream(testDir.getTestDirectory(),
+ new NativeEventHandler() {
+ @Override
+ public void handle(Kind kind, @Nullable T context) {
+ signaled.set(true);
+ }
+ }
+ )) {
+ stream.open();
+ Files.write(testDir.getTestFiles().get(0), "Hello".getBytes());
+ await("Signal received").untilTrue(signaled);
+ }
+ }
+}
diff --git a/update-rust-jni-libs.sh b/update-rust-jni-libs.sh
new file mode 100755
index 00000000..cb0ee6b5
--- /dev/null
+++ b/update-rust-jni-libs.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+set -euxo pipefail
+
+# Run Cargo in debug mode by default
+CARGO_OPTIONS=""
+TARGET_SUBDIR="debug"
+
+while getopts ":r" OPTION; do
+ case $OPTION in
+ r) # Run Cargo in release mode
+ CARGO_OPTIONS="--release"
+ TARGET_SUBDIR="release"
+ esac
+done
+
+RESOURCES="../resources/engineering/swat/watch/jni/"
+
+cd src/main/rust
+
+function build() {
+ rustup target add $1
+ cargo build --target $1 $CARGO_OPTIONS
+ mkdir -p "$RESOURCES/$2/"
+ cp "target/$1/$TARGET_SUBDIR/librust_fsevents_jni.dylib" "$RESOURCES/$2/"
+}
+
+build "x86_64-apple-darwin" "macos-x64"
+build "aarch64-apple-darwin" "macos-aarch64"