|
| 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