Skip to content

Commit 9e0a0c4

Browse files
committed
Add a facade-like class to open/close an FS event stream without exposing the native APIs to the caller
1 parent c063710 commit 9e0a0c4

File tree

2 files changed

+286
-0
lines changed

2 files changed

+286
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package engineering.swat.watch.impl.mac;
2+
3+
import java.nio.file.WatchEvent;
4+
import java.nio.file.WatchEvent.Kind;
5+
6+
import org.checkerframework.checker.nullness.qual.Nullable;
7+
8+
/**
9+
* <p>
10+
* Handler for native events, intended to be used for the construction of
11+
* JDK's {@link WatchEvent}s (and continue downstream consumption).
12+
* </p>
13+
*
14+
* <p>
15+
* In each call, the types of {@code kind} and {@code context} depend
16+
* specifically on the given native event: they're {@code Kind<Path>} and
17+
* {@code Path} for non-overflows, but they're {@code Kind<Object>} and
18+
* {@code Object} for overflows. This precision is needed to construct
19+
* {@link WatchEvent}s, where the types of {@code kind} and {@code context}
20+
* are correlated. Note: {@link java.util.function.BiConsumer} doesn't give
21+
* the required precision (i.e., its type parameters are initialized only
22+
* once for all calls).
23+
* </p>
24+
*/
25+
@FunctionalInterface
26+
public interface NativeEventHandler {
27+
<T> void handle(Kind<T> kind, @Nullable T context);
28+
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package engineering.swat.watch.impl.mac;
2+
3+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.FILE_EVENTS;
4+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.NO_DEFER;
5+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.WATCH_ROOT;
6+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_CREATED;
7+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_INODE_META_MOD;
8+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_MODIFIED;
9+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_REMOVED;
10+
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.MUST_SCAN_SUB_DIRS;
11+
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
12+
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
13+
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
14+
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
15+
16+
import java.io.Closeable;
17+
import java.io.IOException;
18+
import java.nio.file.Path;
19+
20+
import org.checkerframework.checker.nullness.qual.Nullable;
21+
22+
import com.sun.jna.Memory;
23+
import com.sun.jna.Native;
24+
import com.sun.jna.Pointer;
25+
import com.sun.jna.platform.mac.CoreFoundation;
26+
import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef;
27+
import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
28+
import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
29+
30+
import engineering.swat.watch.impl.mac.apis.DispatchObjects;
31+
import engineering.swat.watch.impl.mac.apis.DispatchQueue;
32+
import engineering.swat.watch.impl.mac.apis.FileSystemEvents;
33+
import engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCallback;
34+
35+
// Note: This file is designed to be the only place in this package where JNA is
36+
// used and/or the native APIs are called. If the need to do so arises outside
37+
// this file, consider extending this file to offer the required services
38+
// without exposing JNA and/or the native APIs.
39+
40+
/**
41+
* <p>
42+
* Stream of native events for a path, issued by macOS.
43+
* </p>
44+
*
45+
* <p>
46+
* Note: Methods {@link #open()} and {@link #close()} synchronize on this object
47+
* to avoid races. The synchronization overhead is expected to be negligible, as
48+
* these methods are expected to be rarely called.
49+
* </p>
50+
*/
51+
public class NativeEventStream implements Closeable {
52+
53+
// Native APIs
54+
private static final CoreFoundation CF = CoreFoundation.INSTANCE;
55+
private static final DispatchObjects DO = DispatchObjects.INSTANCE;
56+
private static final DispatchQueue DQ = DispatchQueue.INSTANCE;
57+
private static final FileSystemEvents FSE = FileSystemEvents.INSTANCE;
58+
59+
// Native memory (automatically deallocated when set to `null`)
60+
private volatile @Nullable FSEventStreamCallback callback;
61+
private volatile @Nullable Pointer stream;
62+
private volatile @Nullable Pointer queue;
63+
64+
private final Path path;
65+
private final NativeEventHandler handler;
66+
private volatile boolean closed;
67+
68+
public NativeEventStream(Path path, NativeEventHandler handler) throws IOException {
69+
this.path = path.toRealPath(); // Resolve symbolic links
70+
this.handler = handler;
71+
this.closed = true;
72+
}
73+
74+
public synchronized void open() {
75+
if (!closed) {
76+
throw new IllegalStateException("Stream already open");
77+
} else {
78+
closed = false;
79+
}
80+
81+
// Allocate native memory. (Checker Framework: The local variables are
82+
// `@NonNull` copies of the `@Nullable` fields.)
83+
var callback = this.callback = createCallback(path, handler);
84+
var stream = this.stream = createFSEventStream(path, callback);
85+
var queue = this.queue = createDispatchQueue();
86+
87+
// Start the stream
88+
FSE.FSEventStreamSetDispatchQueue(stream, queue);
89+
FSE.FSEventStreamStart(stream);
90+
}
91+
92+
private static FSEventStreamCallback createCallback(Path path, NativeEventHandler handler) {
93+
return new FSEventStreamCallback() {
94+
@Override
95+
public void callback(Pointer streamRef, Pointer clientCallBackInfo,
96+
long numEvents, Pointer eventPaths, Pointer eventFlags, Pointer eventIds) {
97+
// This function is called each time native events are issued by
98+
// macOS. The purpose of this function is to perform the minimal
99+
// amount of processing to hide the native APIs from downstream
100+
// consumers, who are offered native events via `handler`.
101+
102+
var paths = eventPaths.getStringArray(0, (int) numEvents);
103+
var flags = eventFlags.getIntArray(0, (int) numEvents);
104+
105+
for (var i = 0; i < numEvents; i++) {
106+
var context = path.relativize(Path.of(paths[i]));
107+
108+
// Note: Multiple "physical" native events might be merged
109+
// into a single "logical" native event, so the following
110+
// series of checks should be if-statements (instead of
111+
// if/else-statements).
112+
if (any(flags[i], ITEM_CREATED.mask)) {
113+
handler.handle(ENTRY_CREATE, context);
114+
}
115+
if (any(flags[i], ITEM_REMOVED.mask)) {
116+
handler.handle(ENTRY_DELETE, context);
117+
}
118+
if (any(flags[i], ITEM_MODIFIED.mask | ITEM_INODE_META_MOD.mask)) {
119+
handler.handle(ENTRY_MODIFY, context);
120+
}
121+
if (any(flags[i], MUST_SCAN_SUB_DIRS.mask)) {
122+
handler.handle(OVERFLOW, null);
123+
}
124+
}
125+
}
126+
127+
private boolean any(int bits, int mask) {
128+
return (bits & mask) != 0;
129+
}
130+
};
131+
}
132+
133+
private static Pointer createFSEventStream(Path path, FSEventStreamCallback callback) {
134+
try (
135+
var pathsToWatch = new Strings(path.toString());
136+
) {
137+
var allocator = CF.CFAllocatorGetDefault();
138+
var context = Pointer.NULL;
139+
var sinceWhen = FSE.FSEventsGetCurrentEventId();
140+
var latency = 0.15;
141+
var flags = NO_DEFER.mask | WATCH_ROOT.mask | FILE_EVENTS.mask;
142+
return FSE.FSEventStreamCreate(allocator, callback, context, pathsToWatch.toCFArray(), sinceWhen, latency, flags);
143+
}
144+
}
145+
146+
private static Pointer createDispatchQueue() {
147+
var label = "engineering.swat.watch";
148+
var attr = Pointer.NULL;
149+
return DQ.dispatch_queue_create(label, attr);
150+
}
151+
152+
// -- Closeable --
153+
154+
@Override
155+
public synchronized void close() {
156+
if (closed) {
157+
throw new IllegalStateException("Stream is already closed");
158+
} else {
159+
closed = true;
160+
}
161+
162+
// Stop the stream
163+
if (stream != null) {
164+
var streamNonNull = stream; // Checker Framework: `@NonNull` copy of `@Nullable` field
165+
FSE.FSEventStreamStop(streamNonNull);
166+
FSE.FSEventStreamSetDispatchQueue(streamNonNull, Pointer.NULL);
167+
FSE.FSEventStreamInvalidate(streamNonNull);
168+
FSE.FSEventStreamRelease(streamNonNull);
169+
}
170+
if (queue != null) {
171+
DO.dispatch_release(queue);
172+
}
173+
174+
// Deallocate native memory
175+
callback = null;
176+
stream = null;
177+
queue = null;
178+
}
179+
}
180+
181+
/**
182+
* Array of strings in native memory, needed to create a new native event stream
183+
* (i.e., the {@code pathsToWatch} argument of {@code FSEventStreamCreate} is an
184+
* array of strings).
185+
*/
186+
class Strings implements AutoCloseable {
187+
188+
// Native APIs
189+
private static final CoreFoundation CF = CoreFoundation.INSTANCE;
190+
191+
// Native memory
192+
private final CFStringRef[] strings;
193+
private final CFArrayRef array;
194+
195+
private volatile boolean closed = false;
196+
197+
public Strings(String... strings) {
198+
// Allocate native memory
199+
this.strings = createCFStrings(strings);
200+
this.array = createCFArray(this.strings);
201+
}
202+
203+
public CFArrayRef toCFArray() {
204+
if (closed) {
205+
throw new IllegalStateException("Paths already deallocated");
206+
}
207+
return array;
208+
}
209+
210+
private static CFStringRef[] createCFStrings(String[] pathsToWatch) {
211+
var n = pathsToWatch.length;
212+
213+
var strings = new CFStringRef[n];
214+
for (int i = 0; i < n; i++) {
215+
strings[i] = CFStringRef.createCFString(pathsToWatch[i]);
216+
}
217+
return strings;
218+
}
219+
220+
private static CFArrayRef createCFArray(CFStringRef[] strings) {
221+
var n = strings.length;
222+
var size = Native.getNativeSize(CFStringRef.class);
223+
224+
// Create a temporary array of pointers to the strings (automatically
225+
// freed when `values` goes out of scope)
226+
var values = new Memory(n * size);
227+
for (int i = 0; i < n; i++) {
228+
values.setPointer(i * size, strings[i].getPointer());
229+
}
230+
231+
// Create a permanent array based on the temporary array
232+
var alloc = CF.CFAllocatorGetDefault();
233+
var numValues = new CFIndex(n);
234+
var callBacks = Pointer.NULL;
235+
return CF.CFArrayCreate(alloc, values, numValues, callBacks);
236+
}
237+
238+
// -- AutoCloseable --
239+
240+
@Override
241+
public void close() {
242+
if (closed) {
243+
throw new IllegalStateException("Paths already deallocated");
244+
} else {
245+
closed = true;
246+
}
247+
248+
// Deallocate native memory
249+
for (var s : strings) {
250+
if (s != null) {
251+
s.release();
252+
}
253+
}
254+
if (array != null) {
255+
array.release();
256+
}
257+
}
258+
}

0 commit comments

Comments
 (0)