2626 */
2727package engineering .swat .watch .impl .mac ;
2828
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 .ITEM_RENAMED ;
37- import static engineering .swat .watch .impl .mac .apis .FileSystemEvents .FSEventStreamEventFlag .MUST_SCAN_SUB_DIRS ;
38- import static java .nio .file .StandardWatchEventKinds .ENTRY_CREATE ;
39- import static java .nio .file .StandardWatchEventKinds .ENTRY_DELETE ;
40- import static java .nio .file .StandardWatchEventKinds .ENTRY_MODIFY ;
41- import static java .nio .file .StandardWatchEventKinds .OVERFLOW ;
42-
4329import java .io .Closeable ;
4430import java .io .IOException ;
45- import java .nio .file .Files ;
4631import java .nio .file .Path ;
47- import java .util .Arrays ;
48-
49- import org .checkerframework .checker .nullness .qual .Nullable ;
50-
51- import com .sun .jna .Memory ;
52- import com .sun .jna .Native ;
53- import com .sun .jna .Pointer ;
54- import com .sun .jna .platform .mac .CoreFoundation ;
55- import com .sun .jna .platform .mac .CoreFoundation .CFArrayRef ;
56- import com .sun .jna .platform .mac .CoreFoundation .CFIndex ;
57- import com .sun .jna .platform .mac .CoreFoundation .CFStringRef ;
58-
59- import engineering .swat .watch .impl .mac .apis .DispatchObjects ;
60- import engineering .swat .watch .impl .mac .apis .DispatchQueue ;
61- import engineering .swat .watch .impl .mac .apis .FileSystemEvents ;
62- import engineering .swat .watch .impl .mac .apis .FileSystemEvents .FSEventStreamCallback ;
6332
6433// Note: This file is designed to be the only place in this package where JNA is
6534// used and/or the native APIs are invoked. If the need to do so arises outside
7948 * </p>
8049 */
8150class NativeEventStream implements Closeable {
82-
83- // Native APIs
84- private static final CoreFoundation CF = CoreFoundation .INSTANCE ;
85- private static final DispatchObjects DO = DispatchObjects .INSTANCE ;
86- private static final DispatchQueue DQ = DispatchQueue .INSTANCE ;
87- private static final FileSystemEvents FSE = FileSystemEvents .INSTANCE ;
88-
89- // Native memory
90- private @ Nullable FSEventStreamCallback callback ; // Keep reference to avoid premature GC'ing
91- private @ Nullable Pointer stream ;
92- private @ Nullable Pointer queue ;
93- // Note: These fields aren't volatile, as all reads/write from/to them are
94- // inside synchronized blocks. Be careful to not break this invariant.
51+ static {
52+ NativeLibrary .load ();
53+ }
9554
9655 private final Path path ;
9756 private final NativeEventHandler handler ;
9857 private volatile boolean closed ;
58+ private volatile long nativeWatch ;
9959
10060 public NativeEventStream (Path path , NativeEventHandler handler ) throws IOException {
10161 this .path = path .toRealPath (); // Resolve symbolic links
@@ -110,88 +70,7 @@ public synchronized void open() {
11070 closed = false ;
11171 }
11272
113- // Allocate native memory
114- callback = createCallback ();
115- stream = createFSEventStream (callback );
116- queue = createDispatchQueue ();
117-
118- // Start the stream
119- var streamNonNull = stream ;
120- if (streamNonNull != null ) {
121- FSE .FSEventStreamSetDispatchQueue (streamNonNull , queue );
122- FSE .FSEventStreamStart (streamNonNull );
123- }
124- }
125-
126- private FSEventStreamCallback createCallback () {
127- return new FSEventStreamCallback () {
128- @ Override
129- public void callback (Pointer streamRef , Pointer clientCallBackInfo ,
130- long numEvents , Pointer eventPaths , Pointer eventFlags , Pointer eventIds ) {
131- // This function is called each time native events are issued by
132- // macOS. The purpose of this function is to perform the minimal
133- // amount of processing to hide the native APIs from downstream
134- // consumers, who are offered native events via `handler`.
135-
136- var paths = eventPaths .getStringArray (0 , (int ) numEvents );
137- var flags = eventFlags .getIntArray (0 , (int ) numEvents );
138-
139- for (var i = 0 ; i < numEvents ; i ++) {
140- var context = path .relativize (Path .of (paths [i ]));
141-
142- // Note: Multiple "physical" native events might be
143- // coalesced into a single "logical" native event, so the
144- // following series of checks should be if-statements
145- // (instead of if/else-statements).
146- if (any (flags [i ], ITEM_CREATED .mask )) {
147- handler .handle (ENTRY_CREATE , context );
148- }
149- if (any (flags [i ], ITEM_REMOVED .mask )) {
150- handler .handle (ENTRY_DELETE , context );
151- }
152- if (any (flags [i ], ITEM_MODIFIED .mask | ITEM_INODE_META_MOD .mask )) {
153- handler .handle (ENTRY_MODIFY , context );
154- }
155- if (any (flags [i ], MUST_SCAN_SUB_DIRS .mask )) {
156- handler .handle (OVERFLOW , null );
157- }
158- if (any (flags [i ], ITEM_RENAMED .mask )) {
159- // For now, check if the file exists to determine if the
160- // event pertains to the target of the rename (if it
161- // exists) or to the source (else). This is an
162- // approximation. It might be more accurate to maintain
163- // an internal index (but getting the concurrency right
164- // requires care).
165- if (Files .exists (Path .of (paths [i ]))) {
166- handler .handle (ENTRY_CREATE , context );
167- } else {
168- handler .handle (ENTRY_DELETE , context );
169- }
170- }
171- }
172- }
173-
174- private boolean any (int bits , int mask ) {
175- return (bits & mask ) != 0 ;
176- }
177- };
178- }
179-
180- private Pointer createFSEventStream (FSEventStreamCallback callback ) {
181- try (var pathsToWatch = new Strings (path .toString ())) {
182- var allocator = CF .CFAllocatorGetDefault ();
183- var context = Pointer .NULL ;
184- var sinceWhen = FSE .FSEventsGetCurrentEventId ();
185- var latency = 0.15 ;
186- var flags = NO_DEFER .mask | WATCH_ROOT .mask | FILE_EVENTS .mask ;
187- return FSE .FSEventStreamCreate (allocator , callback , context , pathsToWatch .toCFArray (), sinceWhen , latency , flags );
188- }
189- }
190-
191- private Pointer createDispatchQueue () {
192- var label = "engineering.swat.watch" ;
193- var attr = Pointer .NULL ;
194- return DQ .dispatch_queue_create (label , attr );
73+ nativeWatch = NativeLibrary .start (path .toString (), handler );
19574 }
19675
19776 // -- Closeable --
@@ -204,97 +83,6 @@ public synchronized void close() {
20483 closed = true ;
20584 }
20685
207- var streamNonNull = stream ;
208- var queueNonNull = queue ;
209- if (streamNonNull != null && queueNonNull != null ) {
210-
211- // Stop the stream
212- FSE .FSEventStreamStop (streamNonNull );
213- FSE .FSEventStreamSetDispatchQueue (streamNonNull , Pointer .NULL );
214- FSE .FSEventStreamInvalidate (streamNonNull );
215-
216- // Deallocate native memory
217- DO .dispatch_release (queueNonNull );
218- FSE .FSEventStreamRelease (streamNonNull );
219- queue = null ;
220- stream = null ;
221- callback = null ;
222- }
223- }
224- }
225-
226- /**
227- * Array of strings in native memory, needed to create a new native event stream
228- * (i.e., the {@code pathsToWatch} argument of {@code FSEventStreamCreate} is an
229- * array of strings).
230- */
231- class Strings implements AutoCloseable {
232-
233- // Native APIs
234- private static final CoreFoundation CF = CoreFoundation .INSTANCE ;
235-
236- // Native memory
237- private final CFStringRef [] strings ;
238- private final CFArrayRef array ;
239-
240- private volatile boolean closed = false ;
241-
242- public Strings (String ... strings ) {
243- // Allocate native memory
244- this .strings = createCFStrings (strings );
245- this .array = createCFArray (this .strings );
246- }
247-
248- public CFArrayRef toCFArray () {
249- if (closed ) {
250- throw new IllegalStateException ("Strings are already deallocated" );
251- } else {
252- return array ;
253- }
254- }
255-
256- private static CFStringRef [] createCFStrings (String [] pathsToWatch ) {
257- return Arrays .stream (pathsToWatch )
258- .map (CFStringRef ::createCFString )
259- .toArray (CFStringRef []::new );
260- }
261-
262- private static CFArrayRef createCFArray (CFStringRef [] strings ) {
263- var n = strings .length ;
264- var size = Native .getNativeSize (CFStringRef .class );
265-
266- // Create a temporary array of pointers to the strings (automatically
267- // freed when `values` goes out of scope)
268- var values = new Memory (n * size );
269- for (int i = 0 ; i < n ; i ++) {
270- values .setPointer (i * size , strings [i ].getPointer ());
271- }
272-
273- // Create a permanent array based on the temporary array
274- var alloc = CF .CFAllocatorGetDefault ();
275- var numValues = new CFIndex (n );
276- var callBacks = Pointer .NULL ;
277- return CF .CFArrayCreate (alloc , values , numValues , callBacks );
278- }
279-
280- // -- AutoCloseable --
281-
282- @ Override
283- public void close () {
284- if (closed ) {
285- throw new IllegalStateException ("Strings are already deallocated" );
286- } else {
287- closed = true ;
288- }
289-
290- // Deallocate native memory
291- for (var s : strings ) {
292- if (s != null ) {
293- s .release ();
294- }
295- }
296- if (array != null ) {
297- array .release ();
298- }
86+ NativeLibrary .stop (nativeWatch );
29987 }
30088}
0 commit comments