-
Notifications
You must be signed in to change notification settings - Fork 0
Improved macOS support: JNA #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c16dae
c063710
9e0a0c4
7490e1b
d33858f
ca8ff64
ccce387
c6f901f
9aed20f
0798afd
3589680
c22ca44
d17f342
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /* | ||
| * BSD 2-Clause License | ||
| * | ||
| * Copyright (c) 2023, Swat.engineering | ||
| * | ||
| * Redistribution and use in source and binary forms, with or without | ||
| * modification, are permitted provided that the following conditions are met: | ||
| * | ||
| * 1. Redistributions of source code must retain the above copyright notice, this | ||
| * list of conditions and the following disclaimer. | ||
| * | ||
| * 2. Redistributions in binary form must reproduce the above copyright notice, | ||
| * this list of conditions and the following disclaimer in the documentation | ||
| * and/or other materials provided with the distribution. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
| * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
| * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
| * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
| * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
| * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
| package engineering.swat.watch.impl.mac; | ||
|
|
||
| import java.nio.file.WatchEvent; | ||
| import java.nio.file.WatchEvent.Kind; | ||
|
|
||
| import org.checkerframework.checker.nullness.qual.Nullable; | ||
|
|
||
| /** | ||
| * <p> | ||
| * Handler for native events, intended to be used in a {@link NativeEventStream} | ||
| * callback to construct {@link WatchEvent}s (and propagate them for downstream | ||
| * consumption). | ||
| * </p> | ||
| * | ||
| * <p> | ||
| * In each invocation, the types of {@code kind} and {@code context} depend | ||
| * specifically on the given native event: they're {@code Kind<Path>} and | ||
| * {@code Path} for non-overflows, but they're {@code Kind<Object>} and | ||
| * {@code Object} for overflows. This precision is needed to construct | ||
| * {@link WatchEvent}s, where the types of {@code kind} and {@code context} need | ||
| * to be correlated. Note: {@link java.util.function.BiConsumer} doesn't give | ||
| * the required precision (i.e., its type parameters are initialized only once | ||
| * for all invocations). | ||
| * </p> | ||
| */ | ||
| @FunctionalInterface | ||
| interface NativeEventHandler { | ||
| <T> void handle(Kind<T> kind, @Nullable T context); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| /* | ||
| * 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.WATCH_ROOT; | ||
| import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_CREATED; | ||
| import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_INODE_META_MOD; | ||
| import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_MODIFIED; | ||
| import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_REMOVED; | ||
| import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.MUST_SCAN_SUB_DIRS; | ||
| import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; | ||
| import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; | ||
| import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; | ||
| import static java.nio.file.StandardWatchEventKinds.OVERFLOW; | ||
|
|
||
| import java.io.Closeable; | ||
| import java.io.IOException; | ||
| import java.nio.file.Path; | ||
| import 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. | ||
|
|
||
| /** | ||
| * <p> | ||
| * Stream of native events for a path, issued by macOS. It's a facade-like | ||
| * object that hides the low-level native APIs behind a higher-level interface. | ||
| * </p> | ||
| * | ||
| * <p> | ||
| * Note: Methods {@link #open()} and {@link #close()} synchronize on this object | ||
| * to avoid races. The synchronization overhead is expected to be negligible, as | ||
| * these methods are expected to be rarely invoked. | ||
| * </p> | ||
| */ | ||
| 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. | ||
|
|
||
| private final Path path; | ||
| private final NativeEventHandler handler; | ||
| private volatile boolean closed; | ||
|
|
||
| public NativeEventStream(Path path, NativeEventHandler handler) throws IOException { | ||
| this.path = path.toRealPath(); // Resolve symbolic links | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't we also store the original requested path? So that our events generated are in the context of the original path, and not the resolved path? or otherwise, we should make it an argument requirement?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All paths returned to consumers of the stream are relative to the root of the watch scope (so they can be used as For instance, the original But, the paths in the incoming events are these: So, we cannot relativize to the original Then, we can relativize to |
||
| this.handler = handler; | ||
| this.closed = true; | ||
| } | ||
|
|
||
| public synchronized void open() { | ||
| if (!closed) { | ||
| return; | ||
| } else { | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might be wrong, but it reads like we should make sure that this function does not consume a lot of time? As it's a native callback? If that is true, I think we should publish the event to a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternative solution as discussed: make the class package private. |
||
| // 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); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: instead of all the local variables, it can also be written as: |
||
| } | ||
| } | ||
|
|
||
| private Pointer createDispatchQueue() { | ||
| var label = "engineering.swat.watch"; | ||
| var attr = Pointer.NULL; | ||
| return DQ.dispatch_queue_create(label, attr); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: inline the parameters |
||
| } | ||
|
|
||
| // -- Closeable -- | ||
|
|
||
| @Override | ||
| public synchronized void close() { | ||
| if (closed) { | ||
| return; | ||
| } else { | ||
| 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) { | ||
sungshik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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(); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think they should be volatile, as their value will get ready by multiple threads, and you don't want thread-local caches. Since they could point towards bad pointers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All reads/writes to these fields are inside synchronized blocks. Comment added to clarify.