Skip to content

Commit 73b0238

Browse files
authored
Merge pull request #40 from SWAT-engineering/improved-macos-support/jna
Improved macOS support: JNA
2 parents 7b5ad2b + d17f342 commit 73b0238

File tree

7 files changed

+848
-0
lines changed

7 files changed

+848
-0
lines changed

pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,16 @@
223223
<version>${log4j.version}</version>
224224
<scope>test</scope>
225225
</dependency>
226+
<dependency>
227+
<groupId>net.java.dev.jna</groupId>
228+
<artifactId>jna</artifactId>
229+
<version>5.16.0</version>
230+
</dependency>
231+
<dependency>
232+
<groupId>net.java.dev.jna</groupId>
233+
<artifactId>jna-platform</artifactId>
234+
<version>5.16.0</version>
235+
</dependency>
226236
</dependencies>
227237

228238
<profiles>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* BSD 2-Clause License
3+
*
4+
* Copyright (c) 2023, Swat.engineering
5+
*
6+
* Redistribution and use in source and binary forms, with or without
7+
* modification, are permitted provided that the following conditions are met:
8+
*
9+
* 1. Redistributions of source code must retain the above copyright notice, this
10+
* list of conditions and the following disclaimer.
11+
*
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package engineering.swat.watch.impl.mac;
28+
29+
import java.nio.file.WatchEvent;
30+
import java.nio.file.WatchEvent.Kind;
31+
32+
import org.checkerframework.checker.nullness.qual.Nullable;
33+
34+
/**
35+
* <p>
36+
* Handler for native events, intended to be used in a {@link NativeEventStream}
37+
* callback to construct {@link WatchEvent}s (and propagate them for downstream
38+
* consumption).
39+
* </p>
40+
*
41+
* <p>
42+
* In each invocation, the types of {@code kind} and {@code context} depend
43+
* specifically on the given native event: they're {@code Kind<Path>} and
44+
* {@code Path} for non-overflows, but they're {@code Kind<Object>} and
45+
* {@code Object} for overflows. This precision is needed to construct
46+
* {@link WatchEvent}s, where the types of {@code kind} and {@code context} need
47+
* to be correlated. Note: {@link java.util.function.BiConsumer} doesn't give
48+
* the required precision (i.e., its type parameters are initialized only once
49+
* for all invocations).
50+
* </p>
51+
*/
52+
@FunctionalInterface
53+
interface NativeEventHandler {
54+
<T> void handle(Kind<T> kind, @Nullable T context);
55+
}
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/*
2+
* BSD 2-Clause License
3+
*
4+
* Copyright (c) 2023, Swat.engineering
5+
*
6+
* Redistribution and use in source and binary forms, with or without
7+
* modification, are permitted provided that the following conditions are met:
8+
*
9+
* 1. Redistributions of source code must retain the above copyright notice, this
10+
* list of conditions and the following disclaimer.
11+
*
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package engineering.swat.watch.impl.mac;
28+
29+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.FILE_EVENTS;
30+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.NO_DEFER;
31+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.WATCH_ROOT;
32+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_CREATED;
33+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_INODE_META_MOD;
34+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_MODIFIED;
35+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_REMOVED;
36+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.MUST_SCAN_SUB_DIRS;
37+
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
38+
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
39+
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
40+
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
41+
42+
import java.io.Closeable;
43+
import java.io.IOException;
44+
import java.nio.file.Path;
45+
import java.util.Arrays;
46+
47+
import org.checkerframework.checker.nullness.qual.Nullable;
48+
49+
import com.sun.jna.Memory;
50+
import com.sun.jna.Native;
51+
import com.sun.jna.Pointer;
52+
import com.sun.jna.platform.mac.CoreFoundation;
53+
import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef;
54+
import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
55+
import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
56+
57+
import engineering.swat.watch.impl.mac.apis.DispatchObjects;
58+
import engineering.swat.watch.impl.mac.apis.DispatchQueue;
59+
import engineering.swat.watch.impl.mac.apis.FileSystemEvents;
60+
import engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCallback;
61+
62+
// Note: This file is designed to be the only place in this package where JNA is
63+
// used and/or the native APIs are invoked. If the need to do so arises outside
64+
// this file, consider extending this file to offer the required services
65+
// without exposing JNA and/or the native APIs.
66+
67+
/**
68+
* <p>
69+
* Stream of native events for a path, issued by macOS. It's a facade-like
70+
* object that hides the low-level native APIs behind a higher-level interface.
71+
* </p>
72+
*
73+
* <p>
74+
* Note: Methods {@link #open()} and {@link #close()} synchronize on this object
75+
* to avoid races. The synchronization overhead is expected to be negligible, as
76+
* these methods are expected to be rarely invoked.
77+
* </p>
78+
*/
79+
class NativeEventStream implements Closeable {
80+
81+
// Native APIs
82+
private static final CoreFoundation CF = CoreFoundation.INSTANCE;
83+
private static final DispatchObjects DO = DispatchObjects.INSTANCE;
84+
private static final DispatchQueue DQ = DispatchQueue.INSTANCE;
85+
private static final FileSystemEvents FSE = FileSystemEvents.INSTANCE;
86+
87+
// Native memory
88+
private @Nullable FSEventStreamCallback callback; // Keep reference to avoid premature GC'ing
89+
private @Nullable Pointer stream;
90+
private @Nullable Pointer queue;
91+
// Note: These fields aren't volatile, as all reads/write from/to them are
92+
// inside synchronized blocks. Be careful to not break this invariant.
93+
94+
private final Path path;
95+
private final NativeEventHandler handler;
96+
private volatile boolean closed;
97+
98+
public NativeEventStream(Path path, NativeEventHandler handler) throws IOException {
99+
this.path = path.toRealPath(); // Resolve symbolic links
100+
this.handler = handler;
101+
this.closed = true;
102+
}
103+
104+
public synchronized void open() {
105+
if (!closed) {
106+
return;
107+
} else {
108+
closed = false;
109+
}
110+
111+
// Allocate native memory
112+
callback = createCallback();
113+
stream = createFSEventStream(callback);
114+
queue = createDispatchQueue();
115+
116+
// Start the stream
117+
var streamNonNull = stream;
118+
if (streamNonNull != null) {
119+
FSE.FSEventStreamSetDispatchQueue(streamNonNull, queue);
120+
FSE.FSEventStreamStart(streamNonNull);
121+
}
122+
}
123+
124+
private FSEventStreamCallback createCallback() {
125+
return new FSEventStreamCallback() {
126+
@Override
127+
public void callback(Pointer streamRef, Pointer clientCallBackInfo,
128+
long numEvents, Pointer eventPaths, Pointer eventFlags, Pointer eventIds) {
129+
// This function is called each time native events are issued by
130+
// macOS. The purpose of this function is to perform the minimal
131+
// amount of processing to hide the native APIs from downstream
132+
// consumers, who are offered native events via `handler`.
133+
134+
var paths = eventPaths.getStringArray(0, (int) numEvents);
135+
var flags = eventFlags.getIntArray(0, (int) numEvents);
136+
137+
for (var i = 0; i < numEvents; i++) {
138+
var context = path.relativize(Path.of(paths[i]));
139+
140+
// Note: Multiple "physical" native events might be
141+
// coalesced into a single "logical" native event, so the
142+
// following series of checks should be if-statements
143+
// (instead of if/else-statements).
144+
if (any(flags[i], ITEM_CREATED.mask)) {
145+
handler.handle(ENTRY_CREATE, context);
146+
}
147+
if (any(flags[i], ITEM_REMOVED.mask)) {
148+
handler.handle(ENTRY_DELETE, context);
149+
}
150+
if (any(flags[i], ITEM_MODIFIED.mask | ITEM_INODE_META_MOD.mask)) {
151+
handler.handle(ENTRY_MODIFY, context);
152+
}
153+
if (any(flags[i], MUST_SCAN_SUB_DIRS.mask)) {
154+
handler.handle(OVERFLOW, null);
155+
}
156+
}
157+
}
158+
159+
private boolean any(int bits, int mask) {
160+
return (bits & mask) != 0;
161+
}
162+
};
163+
}
164+
165+
private Pointer createFSEventStream(FSEventStreamCallback callback) {
166+
try (var pathsToWatch = new Strings(path.toString())) {
167+
var allocator = CF.CFAllocatorGetDefault();
168+
var context = Pointer.NULL;
169+
var sinceWhen = FSE.FSEventsGetCurrentEventId();
170+
var latency = 0.15;
171+
var flags = NO_DEFER.mask | WATCH_ROOT.mask | FILE_EVENTS.mask;
172+
return FSE.FSEventStreamCreate(allocator, callback, context, pathsToWatch.toCFArray(), sinceWhen, latency, flags);
173+
}
174+
}
175+
176+
private Pointer createDispatchQueue() {
177+
var label = "engineering.swat.watch";
178+
var attr = Pointer.NULL;
179+
return DQ.dispatch_queue_create(label, attr);
180+
}
181+
182+
// -- Closeable --
183+
184+
@Override
185+
public synchronized void close() {
186+
if (closed) {
187+
return;
188+
} else {
189+
closed = true;
190+
}
191+
192+
var streamNonNull = stream;
193+
var queueNonNull = queue;
194+
if (streamNonNull != null && queueNonNull != null) {
195+
196+
// Stop the stream
197+
FSE.FSEventStreamStop(streamNonNull);
198+
FSE.FSEventStreamSetDispatchQueue(streamNonNull, Pointer.NULL);
199+
FSE.FSEventStreamInvalidate(streamNonNull);
200+
201+
// Deallocate native memory
202+
DO.dispatch_release(queueNonNull);
203+
FSE.FSEventStreamRelease(streamNonNull);
204+
queue = null;
205+
stream = null;
206+
callback = null;
207+
}
208+
}
209+
}
210+
211+
/**
212+
* Array of strings in native memory, needed to create a new native event stream
213+
* (i.e., the {@code pathsToWatch} argument of {@code FSEventStreamCreate} is an
214+
* array of strings).
215+
*/
216+
class Strings implements AutoCloseable {
217+
218+
// Native APIs
219+
private static final CoreFoundation CF = CoreFoundation.INSTANCE;
220+
221+
// Native memory
222+
private final CFStringRef[] strings;
223+
private final CFArrayRef array;
224+
225+
private volatile boolean closed = false;
226+
227+
public Strings(String... strings) {
228+
// Allocate native memory
229+
this.strings = createCFStrings(strings);
230+
this.array = createCFArray(this.strings);
231+
}
232+
233+
public CFArrayRef toCFArray() {
234+
if (closed) {
235+
throw new IllegalStateException("Strings are already deallocated");
236+
} else {
237+
return array;
238+
}
239+
}
240+
241+
private static CFStringRef[] createCFStrings(String[] pathsToWatch) {
242+
return Arrays.stream(pathsToWatch)
243+
.map(CFStringRef::createCFString)
244+
.toArray(CFStringRef[]::new);
245+
}
246+
247+
private static CFArrayRef createCFArray(CFStringRef[] strings) {
248+
var n = strings.length;
249+
var size = Native.getNativeSize(CFStringRef.class);
250+
251+
// Create a temporary array of pointers to the strings (automatically
252+
// freed when `values` goes out of scope)
253+
var values = new Memory(n * size);
254+
for (int i = 0; i < n; i++) {
255+
values.setPointer(i * size, strings[i].getPointer());
256+
}
257+
258+
// Create a permanent array based on the temporary array
259+
var alloc = CF.CFAllocatorGetDefault();
260+
var numValues = new CFIndex(n);
261+
var callBacks = Pointer.NULL;
262+
return CF.CFArrayCreate(alloc, values, numValues, callBacks);
263+
}
264+
265+
// -- AutoCloseable --
266+
267+
@Override
268+
public void close() {
269+
if (closed) {
270+
throw new IllegalStateException("Strings are already deallocated");
271+
} else {
272+
closed = true;
273+
}
274+
275+
// Deallocate native memory
276+
for (var s : strings) {
277+
if (s != null) {
278+
s.release();
279+
}
280+
}
281+
if (array != null) {
282+
array.release();
283+
}
284+
}
285+
}

0 commit comments

Comments
 (0)