diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 323a962..b479c1e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,12 +13,12 @@ jobs: strategy: matrix: os: - - image: ubuntu-latest - - image: macos-latest - mac-backend: jdk + #- image: ubuntu-latest + #- image: macos-latest + #mac-backend: jdk - image: macos-latest mac-backend: fsevents - - image: windows-latest + #- image: windows-latest jdk: [11, 17, 21] fail-fast: false @@ -32,9 +32,12 @@ jobs: java-version: ${{ matrix.jdk }} distribution: 'temurin' cache: 'maven' + #- uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: ./update-rust-jni-libs.sh + if: ${{ matrix.os.image == 'macos-latest' }} - name: test - run: mvn -B clean test "-Dwatch.mac.backend=${{ matrix.os.mac-backend }}" + run: mvn -B clean test "-Dwatch.mac.backend=${{ matrix.os.mac-backend }}" -Dtest=Basic env: DELAY_FACTOR: 3 diff --git a/.gitignore b/.gitignore index 5ef0a82..f0df188 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 511a5a2..1205af9 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,14 @@ + + + src/main/resources + + src/main/rust/**/*.* + + + org.apache.maven.plugins diff --git a/src/main/java/engineering/swat/watch/impl/mac/jni/FileSystemEvents.java b/src/main/java/engineering/swat/watch/impl/mac/jni/FileSystemEvents.java new file mode 100644 index 0000000..107736f --- /dev/null +++ b/src/main/java/engineering/swat/watch/impl/mac/jni/FileSystemEvents.java @@ -0,0 +1,54 @@ +package engineering.swat.watch.impl.mac.jni; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.WatchEvent; +import java.util.List; + +/** + * Start a recursive watch on a folder + */ +public class FileSystemEvents implements Closeable { + static { + NativeLibrary.load(); + } + + private native long start(String path, Runnable signal); + private native void stop(long watchId); + private native boolean anyEvents(long watchId); + private native List> pollEvents(long watchId); + + + private volatile boolean closed = false; + private final long activeWatch; + + public FileSystemEvents(Path path, Runnable notifyNewEvents) { + activeWatch = start(path.toString(), notifyNewEvents); + } + + public boolean anyEvents() throws IOException { + if (closed) { + throw new IOException("Watch is already closed"); + } + return anyEvents(activeWatch); + } + + public List> pollEvents() throws IOException { + if (closed) { + throw new IOException("Watch is already closed"); + } + return pollEvents(activeWatch); + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + stop(activeWatch); + } + + +} diff --git a/src/main/java/engineering/swat/watch/impl/mac/jni/NativeLibrary.java b/src/main/java/engineering/swat/watch/impl/mac/jni/NativeLibrary.java new file mode 100644 index 0000000..1a3c8af --- /dev/null +++ b/src/main/java/engineering/swat/watch/impl/mac/jni/NativeLibrary.java @@ -0,0 +1,73 @@ +package engineering.swat.watch.impl.mac.jni; + +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; + +class NativeLibrary { + private 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 = FileSystemEvents.class.getResource(path); + if (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 = FileSystemEvents.class.getResourceAsStream(path)) { + 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/resources/.gitignore b/src/main/resources/.gitignore new file mode 100644 index 0000000..e5d0467 --- /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 0000000..9552259 --- /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 0000000..67261de --- /dev/null +++ b/src/main/rust/Cargo.lock @@ -0,0 +1,370 @@ +# 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 = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +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 = [ + "bitflags", + "core-foundation 0.10.1", + "crossbeam-channel", + "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 0000000..c857c24 --- /dev/null +++ b/src/main/rust/Cargo.toml @@ -0,0 +1,22 @@ +[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"] } +bitflags = "2.9.1" # unsure if we need bitflags +jni = "0.21.1" +crossbeam-channel = "0.5.15" # more extensive channel api than build in diff --git a/src/main/rust/src/fs_monitor.rs b/src/main/rust/src/fs_monitor.rs new file mode 100644 index 0000000..747329b --- /dev/null +++ b/src/main/rust/src/fs_monitor.rs @@ -0,0 +1,159 @@ +#![cfg(target_os = "macos")] +#![deny( + trivial_numeric_casts, + unstable_features, + unused_import_braces, + unused_qualifications +)] + +use crossbeam_channel as chan; + +use std::{os::raw::{c_char, c_void}, sync::atomic::{AtomicBool, Ordering}}; + +use dispatch2::ffi::{dispatch_object_t, dispatch_queue_t, DISPATCH_QUEUE_SERIAL}; +use core_foundation::{ + array::CFArray, + base::{kCFAllocatorDefault, TCFType}, + string::CFString, +}; +use fsevent_sys::{self as fs, kFSEventStreamCreateFlagFileEvents, kFSEventStreamCreateFlagNoDefer, kFSEventStreamCreateFlagWatchRoot}; +use std::{ + ffi::CStr, ptr +}; + +struct CallbackContext { + new_events: Box, + channel: chan::Sender +} + +pub struct NativeEventStream { + since_when: fs::FSEventStreamEventId, + closed: AtomicBool, + path : CFArray, + queue: dispatch_queue_t, + stream: Option, + receiver: chan::Receiver, + sender_heap: *mut CallbackContext, +} + + +pub struct Event { + pub id: fs::FSEventStreamEventId, + pub flags: fs::FSEventStreamEventFlags, + pub path: String +} + + + +const FLAGS : fs::FSEventStreamCreateFlags + = kFSEventStreamCreateFlagNoDefer + | kFSEventStreamCreateFlagWatchRoot + | kFSEventStreamCreateFlagFileEvents; + + +impl NativeEventStream { + pub fn new(path: String, new_events: impl Fn() + 'static) -> Self { + let (s, r) : (chan::Sender, chan::Receiver) = chan::unbounded(); + eprintln!("New watch requested for: {}", &path); + Self { + since_when: unsafe { fs::FSEventsGetCurrentEventId() }, + closed: AtomicBool::new(false), + path: CFArray::from_CFTypes(&[CFString::new(&path)]), + queue: unsafe { dispatch2::ffi::dispatch_queue_create(ptr::null(), DISPATCH_QUEUE_SERIAL)}, + stream: None, + receiver: r, + sender_heap: Box::into_raw(Box::new(CallbackContext{ new_events: Box::new(new_events), channel: s })) + } + } + + pub fn start(&mut self) { + unsafe { + eprintln!("Creating stream: {}", self.since_when); + let stream = fs::FSEventStreamCreate( + kCFAllocatorDefault, + callback, + &fs::FSEventStreamContext { + version: 0, + info: self.sender_heap 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); + + eprintln!("Connecting stream with queue"); + fs::FSEventStreamSetDispatchQueue(stream, self.queue); + eprintln!("Starting stream"); + fs::FSEventStreamStart(stream); + }; + } + pub fn stop(&self) { + if self.closed.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_err() { + // we weren't the first one to close it + return; + } + match self.stream { + Some(stream) => unsafe{ + fs::FSEventStreamStop(stream); + fs::FSEventStreamSetDispatchQueue(stream, ptr::null_mut()); + fs::FSEventStreamInvalidate(stream); + dispatch2::ffi::dispatch_release(self.queue as dispatch_object_t); + fs::FSEventStreamRelease(stream); + } + None => unsafe { + dispatch2::ffi::dispatch_release(self.queue as dispatch_object_t); + } + }; + } + + pub(crate) fn poll_all(&self) -> chan::TryIter { + self.receiver.try_iter() + } + + pub(crate) fn is_empty(&self) -> bool { + self.receiver.is_empty() + } + +} + +extern "C" fn release_context(info: *mut c_void) { + let ctx_ptr = info as *mut CallbackContext; + unsafe{ drop(Box::from_raw( ctx_ptr)); } +} + +extern "C" fn callback( + _stream_ref: fs::FSEventStreamRef, + info: *mut c_void, + num_events: usize, + event_paths: *mut c_void, + event_flags: *const fs::FSEventStreamEventFlags, + event_ids: *const fs::FSEventStreamEventId, +) { + let ctx = unsafe{ &mut *(info as *mut CallbackContext) }; + eprintln!("ctx restored: {:?}", "foo"); + + let event_paths = event_paths as *const *const c_char; + + for i in 0..num_events { + unsafe{ + ctx.channel.send(Event { + id: *event_ids.add(i), + flags: *event_flags.add(i), + path: CStr::from_ptr(*event_paths.add(i)) + .to_str() + .expect("Invalid UTF8 string.") + .to_string() + }); + } + } + + if num_events > 0 { + eprintln!("Sending message to java"); + (ctx.new_events)(); + } +} diff --git a/src/main/rust/src/lib.rs b/src/main/rust/src/lib.rs new file mode 100644 index 0000000..5b28bb3 --- /dev/null +++ b/src/main/rust/src/lib.rs @@ -0,0 +1,146 @@ +mod fs_monitor; + +use jni::{Executor, JNIEnv}; + +use jni::objects::{GlobalRef, JClass, JMethodID, JObject, JString}; + +use jni::sys::{jboolean, jlong, jobject, jstring, jvalue, JNI_FALSE, JNI_TRUE}; + +use crate::fs_monitor::{Event, NativeEventStream}; + +struct Runnable { + recv: GlobalRef, + class: GlobalRef, // just making sure we don't lose reference to this Runnable class + method: JMethodID, + executor: Executor, +} + + + +impl Runnable { + pub fn new<'local>(env: &mut JNIEnv<'local>, local_recv: JObject<'local>) -> Result { + let recv = env.new_global_ref(local_recv)?; + let class = env.new_global_ref(env.get_object_class(&recv)?)?; + let method = env.get_method_id(&class, "run", "()V")?; + let executor = Executor::new(Into::into(env.get_java_vm()?)); + + Ok(Self { + recv, + class, + method, + executor + }) + } + + pub fn run<'local>(&self) { + self.executor.with_attached(|env: &mut JNIEnv<'_>| -> Result<(), jni::errors::Error> { + unsafe{ + env.call_method_unchecked( + self.recv.as_obj(), + self.method, + jni::signature::ReturnType::Primitive(jni::signature::Primitive::Void), + &[])?; + Ok(()) + } + }); + } +} + + +#[unsafe(no_mangle)] +#[allow(unused_variables)] +pub extern "system" fn Java_engineering_swat_watch_impl_mac_jni_FileSystemEvents_start<'local>( + mut env: JNIEnv<'local>, + class: JClass<'local>, + path: JString<'local>, + signal: JObject<'local>, +) -> jlong { + + let signaller = Runnable::new(&mut env, signal).expect("We should be able to build a runnable reference"); + + let mut mon = NativeEventStream::new( + env.get_string(&path).expect("Should not fail to get string").into(), + move || signaller.run() + ); + 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_jni_FileSystemEvents_stop<'local>( + mut 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 we took it back into the box +} + + +struct ArrayList<'local> { + pub value: JObject<'local>, + add_method: JMethodID +} + +impl<'local> ArrayList<'local> { + pub fn new(mut env: JNIEnv<'local>) -> Result { + let class = env.find_class("java/util/ArrayList")?; + let value = env.new_object(&class, "()V", &[])?; + let add_method = env.get_method_id(&class, "add", "(Ljava/lang/Object;)Z")?; + Ok(Self { + value, + add_method + }) + } + + pub fn add(&self, mut env: JNIEnv<'local>, val: JObject<'local>) -> Result<(), jni::errors::Error> { + unsafe{ + env.call_method_unchecked( + &self.value, + &self.add_method, + jni::signature::ReturnType::Primitive(jni::signature::Primitive::Boolean), + &[jvalue { l: val.as_raw() }] + )?; + Ok(()) + } + } +} + +#[unsafe(no_mangle)] +#[allow(unused_variables)] +pub extern "system" fn Java_engineering_swat_watch_impl_mac_jni_FileSystemEvents_pollEvents<'local>( + mut env: JNIEnv<'local>, + class: JClass<'local>, + stream: jlong, +) -> JObject<'local> { + let mon_ptr = stream as *mut NativeEventStream; + let mon = unsafe { &mut *mon_ptr }; + + let result = ArrayList::new(env).expect("We should be able to allocate array list"); + // TODO: translate events to `WatchKey` instances (most likely our own version of them?) + // and fill them into the result + // get the events from mon.poll_all(); + return result.value; +} + +#[unsafe(no_mangle)] +#[allow(unused_variables)] +pub extern "system" fn Java_engineering_swat_watch_impl_mac_jni_FileSystemEvents_anyEvents<'local>( + mut env: JNIEnv<'local>, + class: JClass<'local>, + stream: jlong, +) -> jboolean { + let mon_ptr = stream as *mut NativeEventStream; + let mon = unsafe { &mut *mon_ptr }; + + if mon.is_empty() { + JNI_TRUE + } else { + JNI_FALSE + } +} diff --git a/src/test/java/engineering/swat/watch/impl/mac/jni/Basic.java b/src/test/java/engineering/swat/watch/impl/mac/jni/Basic.java new file mode 100644 index 0000000..4b7665b --- /dev/null +++ b/src/test/java/engineering/swat/watch/impl/mac/jni/Basic.java @@ -0,0 +1,52 @@ +package engineering.swat.watch.impl.mac.jni; + +import static org.awaitility.Awaitility.await; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.awaitility.Awaitility; +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 Basic { + + 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 signalsAreSend() throws IOException { + var signaled = new AtomicBoolean(false); + try (var watch = new FileSystemEvents(testDir.getTestDirectory(), () -> signaled.set(true))) { + 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 0000000..67925a9 --- /dev/null +++ b/update-rust-jni-libs.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + + +set -euxo pipefail + +RESOURCES="../resources/engineering/swat/watch/jni/" + +cd src/main/rust + + +function build() { + rustup target add $1 + cargo build --target $1 + mkdir -p "$RESOURCES/$2/" + cp "target/$1/debug/librust_fsevents_jni.dylib" "$RESOURCES/$2/" +} + +build "x86_64-apple-darwin" "macos-x64" +build "aarch64-apple-darwin" "macos-aarch64" +