aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaxim Kartashev <maxim.kartashev@jetbrains.com>2021-10-09 13:46:20 +0300
committerMaxim Kartashev <maxim.kartashev@jetbrains.com>2022-05-04 15:11:03 +0300
commit03f0ec3a28e2b85c4f21416743784f19b3952ef0 (patch)
treec048861b1e0e629bf797fe5ee0fd2b088b24a6cb
parent59005e679ff8863184e3bc06fbf3693b41465d40 (diff)
downloadJetBrainsRuntime-03f0ec3a28e2b85c4f21416743784f19b3952ef0.tar.gz
JBR-3862 Implement native WatchService on MacOS
The watch service is based on FSEvents API that notifies about file system changes at a directory level. It is possible to go back to using the old polling watch service with -Dwatch.service.polling=true. Features include: - support for FILE_TREE option (recursive directory watching), - minimum necessary I/O (no filesystem access more than once unless needed), - one thread ("run loop") per WatchService instance, - changes are detected by comparing file modification times with millisecond precision, - a directory tree snapshot is taken at the time of WatchKey creation and can take a long time (proportional to the number of files).
-rw-r--r--src/java.base/macosx/classes/sun/nio/fs/BsdFileSystem.java8
-rw-r--r--src/java.base/macosx/classes/sun/nio/fs/MacOSXWatchService.java826
-rw-r--r--src/java.base/macosx/native/libnio/fs/MacOSXWatchService.c253
-rw-r--r--src/java.base/macosx/native/libnio/fs/UTIFileTypeDetector.c4
-rw-r--r--src/java.base/share/native/libnio/nio_util.c6
-rw-r--r--src/java.base/unix/native/libnio/ch/nio_util.h4
-rw-r--r--test/jdk/java/nio/file/WatchService/JNIChecks.java63
-rw-r--r--test/jdk/java/nio/file/WatchService/Move.java246
-rw-r--r--test/jdk/java/nio/file/WatchService/WithSecurityManager.java3
-rw-r--r--test/jdk/jbProblemList.txt3
-rw-r--r--test/jdk/jdk/nio/zipfs/test.policy2
11 files changed, 1407 insertions, 11 deletions
diff --git a/src/java.base/macosx/classes/sun/nio/fs/BsdFileSystem.java b/src/java.base/macosx/classes/sun/nio/fs/BsdFileSystem.java
index 359da72f7f8..35106076dbb 100644
--- a/src/java.base/macosx/classes/sun/nio/fs/BsdFileSystem.java
+++ b/src/java.base/macosx/classes/sun/nio/fs/BsdFileSystem.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2008, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -28,8 +28,6 @@ package sun.nio.fs;
import java.nio.file.*;
import java.io.IOException;
import java.util.*;
-import java.security.AccessController;
-import sun.security.action.GetPropertyAction;
/**
* Bsd implementation of FileSystem
@@ -45,8 +43,8 @@ class BsdFileSystem extends UnixFileSystem {
public WatchService newWatchService()
throws IOException
{
- // use polling implementation until we implement a BSD/kqueue one
- return new PollingWatchService();
+ final boolean usePollingWatchService = Boolean.getBoolean("watch.service.polling");
+ return usePollingWatchService ? new PollingWatchService() : new MacOSXWatchService();
}
// lazy initialization of the list of supported attribute views
diff --git a/src/java.base/macosx/classes/sun/nio/fs/MacOSXWatchService.java b/src/java.base/macosx/classes/sun/nio/fs/MacOSXWatchService.java
new file mode 100644
index 00000000000..8ec7bbfdc65
--- /dev/null
+++ b/src/java.base/macosx/classes/sun/nio/fs/MacOSXWatchService.java
@@ -0,0 +1,826 @@
+/*
+ * Copyright (c) 2021, 2022, JetBrains s.r.o.. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package sun.nio.fs;
+
+import sun.util.logging.PlatformLogger;
+
+import jdk.internal.misc.Unsafe;
+
+import java.io.IOException;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayDeque;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.function.Consumer;
+
+class MacOSXWatchService extends AbstractWatchService {
+ // Controls tracing in the native part of the watcher.
+ private static boolean tracingEnabled = false;
+ private static final PlatformLogger logger = PlatformLogger.getLogger("sun.nio.fs.MacOSXWatchService");
+
+ private final HashMap<Object, MacOSXWatchKey> dirKeyToWatchKey = new HashMap<>();
+ private final HashMap<Long, MacOSXWatchKey> eventStreamToWatchKey = new HashMap<>();
+ private final Object watchKeysLock = new Object();
+
+ private final CFRunLoopThread runLoopThread;
+
+ MacOSXWatchService() throws IOException {
+ runLoopThread = new CFRunLoopThread();
+ runLoopThread.setDaemon(true);
+ runLoopThread.start();
+
+ try {
+ // In order to be able to schedule any FSEventStream's, a reference to a run loop is required.
+ runLoopThread.waitForRunLoopRef();
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ WatchKey register(Path dir, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException {
+ checkIsOpen();
+
+ final UnixPath unixDir = (UnixPath)dir;
+ final Object dirKey = checkPath(unixDir);
+ final EnumSet<FSEventKind> eventSet = FSEventKind.setOf(events);
+ final EnumSet<WatchModifier> modifierSet = WatchModifier.setOf(modifiers);
+ synchronized (closeLock()) {
+ checkIsOpen();
+
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("register for " + dir);
+
+ synchronized (watchKeysLock) {
+ MacOSXWatchKey watchKey = dirKeyToWatchKey.get(dirKey);
+ final boolean keyForDirAlreadyExists = (watchKey != null);
+ if (keyForDirAlreadyExists) {
+ eventStreamToWatchKey.remove(watchKey.getEventStreamRef());
+ watchKey.disable();
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("re-used existing watch key");
+ } else {
+ watchKey = new MacOSXWatchKey(this, unixDir, dirKey);
+ dirKeyToWatchKey.put(dirKey, watchKey);
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("created a new watch key");
+ }
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("starting to [re-]populate directory cache with data");
+ watchKey.enable(runLoopThread, eventSet, modifierSet);
+ eventStreamToWatchKey.put(watchKey.getEventStreamRef(), watchKey);
+ watchKeysLock.notify(); // So that run loop gets running again if stopped due to lack of event streams
+ return watchKey;
+ }
+ }
+ }
+
+ /**
+ * Invoked on the CFRunLoopThread by the native code to report directories that need to be re-scanned.
+ */
+ private void callback(final long eventStreamRef, final String[] paths, final long eventFlagsPtr) {
+ synchronized (watchKeysLock) {
+ final MacOSXWatchKey watchKey = eventStreamToWatchKey.get(eventStreamRef);
+ if (watchKey != null) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("Callback fired for '" + watchKey.getRealRootPath() + "'");
+ watchKey.handleEvents(paths, eventFlagsPtr);
+ } else {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("Callback fired for watch key that is no longer there");
+ }
+ }
+ }
+
+ void cancel(final MacOSXWatchKey watchKey) {
+ synchronized (watchKeysLock) {
+ dirKeyToWatchKey.remove(watchKey.getRootPathKey());
+ eventStreamToWatchKey.remove(watchKey.getEventStreamRef());
+ }
+ }
+
+ void waitForEventSource() {
+ synchronized (watchKeysLock) {
+ if (isOpen() && eventStreamToWatchKey.isEmpty()) {
+ try {
+ watchKeysLock.wait();
+ } catch (InterruptedException ignore) {}
+ }
+ }
+ }
+
+ @Override
+ void implClose() {
+ synchronized (watchKeysLock) {
+ eventStreamToWatchKey.clear();
+ dirKeyToWatchKey.forEach((key, watchKey) -> watchKey.invalidate());
+ dirKeyToWatchKey.clear();
+ watchKeysLock.notify(); // Let waitForEventSource() go if it was waiting
+ runLoopThread.runLoopStop(); // Force exit from CFRunLoopRun()
+ }
+ }
+
+ private static void traceLine(final String text) {
+ logger.finest("NATIVE trace: " + text);
+ }
+
+ private class CFRunLoopThread extends Thread {
+ // Native reference to the CFRunLoop object of the watch service run loop.
+ private long runLoopRef;
+
+ public CFRunLoopThread() {
+ super("FileSystemWatcher");
+ }
+
+ synchronized void waitForRunLoopRef() throws InterruptedException {
+ if (runLoopRef == 0)
+ runLoopThread.wait(); // ...for CFRunLoopRef to become available
+ }
+
+ long getRunLoopRef() {
+ return runLoopRef;
+ }
+
+ synchronized void runLoopStop() {
+ if (runLoopRef != 0) {
+ // The run loop may have stuck in CFRunLoopRun() even though all of its input sources
+ // have been removed. Need to terminate it explicitly so that it can run to completion.
+ MacOSXWatchService.CFRunLoopStop(runLoopRef);
+ }
+ }
+
+ @Override
+ public void run() {
+ synchronized (this) {
+ runLoopRef = CFRunLoopGetCurrent();
+ notify();
+ }
+
+ while (isOpen()) {
+ CFRunLoopRun(MacOSXWatchService.this);
+ waitForEventSource();
+ }
+
+ synchronized (this) {
+ runLoopRef = 0; // CFRunLoopRef is no longer usable when the loop has been terminated
+ }
+ }
+ }
+
+ private void checkIsOpen() {
+ if (!isOpen())
+ throw new ClosedWatchServiceException();
+ }
+
+ private Object checkPath(UnixPath dir) throws IOException {
+ if (dir == null)
+ throw new NullPointerException("No path to watch");
+
+ UnixFileAttributes attrs;
+ try {
+ attrs = UnixFileAttributes.get(dir, true);
+ } catch (UnixException x) {
+ throw x.asIOException(dir);
+ }
+
+ if (!attrs.isDirectory())
+ throw new NotDirectoryException(dir.getPathForExceptionMessage());
+
+ final Object fileKey = attrs.fileKey();
+ if (fileKey == null)
+ throw new AssertionError("File keys must be supported");
+
+ return fileKey;
+ }
+
+ private enum FSEventKind {
+ CREATE, MODIFY, DELETE, OVERFLOW;
+
+ public static FSEventKind of(final WatchEvent.Kind<?> watchEventKind) {
+ if (StandardWatchEventKinds.ENTRY_CREATE == watchEventKind) {
+ return CREATE;
+ } else if (StandardWatchEventKinds.ENTRY_MODIFY == watchEventKind) {
+ return MODIFY;
+ } else if (StandardWatchEventKinds.ENTRY_DELETE == watchEventKind) {
+ return DELETE;
+ } else if (StandardWatchEventKinds.OVERFLOW == watchEventKind) {
+ return OVERFLOW;
+ } else {
+ throw new UnsupportedOperationException(watchEventKind.name());
+ }
+ }
+
+ public static EnumSet<FSEventKind> setOf(final WatchEvent.Kind<?>[] events) {
+ final EnumSet<FSEventKind> eventSet = EnumSet.noneOf(FSEventKind.class);
+ for (final WatchEvent.Kind<?> event: events) {
+ if (event == null) {
+ throw new NullPointerException("An element in event set is 'null'");
+ } else if (event == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ eventSet.add(FSEventKind.of(event));
+ }
+
+ if (eventSet.isEmpty())
+ throw new IllegalArgumentException("No events to register");
+
+ return eventSet;
+ }
+
+ }
+
+ private enum WatchModifier {
+ FILE_TREE, SENSITIVITY_HIGH, SENSITIVITY_MEDIUM, SENSITIVITY_LOW;
+
+ public static WatchModifier of(final WatchEvent.Modifier watchEventModifier) {
+ if (ExtendedOptions.FILE_TREE.matches(watchEventModifier)) {
+ return FILE_TREE;
+ } if (ExtendedOptions.SENSITIVITY_HIGH.matches(watchEventModifier)) {
+ return SENSITIVITY_HIGH;
+ } if (ExtendedOptions.SENSITIVITY_MEDIUM.matches(watchEventModifier)) {
+ return SENSITIVITY_MEDIUM;
+ } if (ExtendedOptions.SENSITIVITY_LOW.matches(watchEventModifier)) {
+ return SENSITIVITY_LOW;
+ } else {
+ throw new UnsupportedOperationException(watchEventModifier.name());
+ }
+ }
+
+ public static EnumSet<WatchModifier> setOf(final WatchEvent.Modifier[] modifiers) {
+ final EnumSet<WatchModifier> modifierSet = EnumSet.noneOf(WatchModifier.class);
+ for (final WatchEvent.Modifier modifier : modifiers) {
+ if (modifier == null)
+ throw new NullPointerException("An element in modifier set is 'null'");
+
+ modifierSet.add(WatchModifier.of(modifier));
+ }
+
+ return modifierSet;
+ }
+
+ public static double sensitivityOf(final EnumSet<WatchModifier> modifiers) {
+ if (modifiers.contains(SENSITIVITY_HIGH)) {
+ return 0.1;
+ } else if (modifiers.contains(SENSITIVITY_LOW)) {
+ return 1;
+ } else {
+ return 0.5; // aka SENSITIVITY_MEDIUM
+ }
+ }
+ }
+
+ private static class MacOSXWatchKey extends AbstractWatchKey {
+ private static final Unsafe unsafe = Unsafe.getUnsafe();
+
+ private static final long kFSEventStreamEventFlagMustScanSubDirs = 0x00000001;
+ private static final long kFSEventStreamEventFlagRootChanged = 0x00000020;
+
+ private final static Path relativeRootPath = Path.of("");
+
+ // Full path to this key's watch root directory.
+ private final Path realRootPath;
+ private final int realRootPathLength;
+ private final Object rootPathKey;
+
+ // Kinds of events to be reported.
+ private EnumSet<FSEventKind> eventsToWatch;
+
+ // Should events in directories below realRootPath reported?
+ private boolean watchFileTree;
+
+ // Native FSEventStreamRef as returned by FSEventStreamCreate().
+ private long eventStreamRef;
+ private final Object eventStreamRefLock = new Object();
+
+ private final DirectoryTreeSnapshot directoryTreeSnapshot = new DirectoryTreeSnapshot();
+
+ MacOSXWatchKey(final MacOSXWatchService watchService, final UnixPath dir, final Object rootPathKey) throws IOException {
+ super(dir, watchService);
+ this.realRootPath = dir.toRealPath().normalize();
+ this.realRootPathLength = realRootPath.toString().length() + 1;
+ this.rootPathKey = rootPathKey;
+ }
+
+ synchronized void enable(final CFRunLoopThread runLoopThread,
+ final EnumSet<FSEventKind> eventsToWatch,
+ final EnumSet<WatchModifier> modifierSet) throws IOException {
+ assert(!isValid());
+
+ this.eventsToWatch = eventsToWatch;
+ this.watchFileTree = modifierSet.contains(WatchModifier.FILE_TREE);
+
+ directoryTreeSnapshot.build();
+
+ synchronized (eventStreamRefLock) {
+ final int kFSEventStreamCreateFlagWatchRoot = 0x00000004;
+ eventStreamRef = MacOSXWatchService.eventStreamCreate(
+ realRootPath.toString(),
+ WatchModifier.sensitivityOf(modifierSet),
+ kFSEventStreamCreateFlagWatchRoot);
+
+ if (eventStreamRef == 0)
+ throw new IOException("Unable to create FSEventStream");
+
+ MacOSXWatchService.eventStreamSchedule(eventStreamRef, runLoopThread.getRunLoopRef());
+ }
+ }
+
+ synchronized void disable() {
+ invalidate();
+ directoryTreeSnapshot.reset();
+ }
+
+ synchronized void handleEvents(final String[] paths, long eventFlagsPtr) {
+ if (paths == null) {
+ reportOverflow(null);
+ return;
+ }
+
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("handleEvents(): will handle " + paths.length + " events");
+
+ final Set<Path> dirsToScan = new LinkedHashSet<>(paths.length);
+ final Set<Path> dirsToScanRecursively = new LinkedHashSet<>();
+ collectDirsToScan(paths, eventFlagsPtr, dirsToScan, dirsToScanRecursively);
+
+ for (final Path recurseDir : dirsToScanRecursively) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("handleEvents(): scanning directory recursively " + recurseDir);
+ dirsToScan.removeIf(dir -> dir.startsWith(recurseDir));
+ assert(watchFileTree);
+ directoryTreeSnapshot.update(recurseDir, true);
+ }
+
+ for (final Path dir : dirsToScan) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("handleEvents(): scanning directory " + dir);
+ directoryTreeSnapshot.update(dir, false);
+ }
+ }
+
+ private Path toRelativePath(final String absPath) {
+ return (absPath.length() > realRootPathLength)
+ ? Path.of(absPath.substring(realRootPathLength))
+ : relativeRootPath;
+ }
+
+ private void collectDirsToScan(final String[] paths, long eventFlagsPtr,
+ final Set<Path> dirsToScan,
+ final Set<Path> dirsToScanRecursively) {
+ for (final String absPath : paths) {
+ if (absPath == null) {
+ reportOverflow(null);
+ continue;
+ }
+
+ Path path = toRelativePath(absPath);
+
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("handleEvents(): event path name " + path);
+
+ if (!watchFileTree && !relativeRootPath.equals(path)) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("handleEvents(): skipping event for a nested directory");
+ continue;
+ }
+
+ final int flags = unsafe.getInt(eventFlagsPtr);
+ if ((flags & kFSEventStreamEventFlagRootChanged) != 0) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("handleEvents(): watch root changed, path=" + path);
+ cancel();
+ signal();
+ break;
+ } else if ((flags & kFSEventStreamEventFlagMustScanSubDirs) != 0 && watchFileTree) {
+ dirsToScanRecursively.add(path);
+ } else {
+ dirsToScan.add(path);
+ }
+
+ final long SIZEOF_FS_EVENT_STREAM_EVENT_FLAGS = 4L; // FSEventStreamEventFlags is UInt32
+ eventFlagsPtr += SIZEOF_FS_EVENT_STREAM_EVENT_FLAGS;
+ }
+ }
+
+ /**
+ * Represents a snapshot of a directory tree.
+ * The snapshot includes subdirectories iff <code>watchFileTree</code> is <code>true</code>.
+ */
+ private class DirectoryTreeSnapshot {
+ private final HashMap<Path, DirectorySnapshot> snapshots;
+
+ DirectoryTreeSnapshot() {
+ this.snapshots = new HashMap<>(watchFileTree ? 256 : 1);
+ }
+
+ void build() throws IOException {
+ final Queue<Path> pathToDo = new ArrayDeque<>();
+ pathToDo.offer(relativeRootPath);
+
+ while (!pathToDo.isEmpty()) {
+ final Path path = pathToDo.poll();
+ try {
+ createForOneDirectory(path, watchFileTree ? pathToDo : null);
+ } catch (IOException e) {
+ final boolean exceptionForRootPath = relativeRootPath.equals(path);
+ if (exceptionForRootPath)
+ throw e; // report to the user as the watch root may have disappeared
+
+ // Ignore for sub-directories as some may have been removed during the scan.
+ // That's OK, those kinds of changes in the directory hierarchy is what
+ // WatchService is used for. However, it's impossible to catch all changes
+ // at this point, so we may fail to report some events that had occurred before
+ // FSEventStream has been created to watch for those changes.
+ }
+ }
+ }
+
+ private DirectorySnapshot createForOneDirectory(
+ final Path directory,
+ final Queue<Path> newDirectoriesFound) throws IOException {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("Creating snapshot for one directory " + directory);
+
+ final DirectorySnapshot snapshot = DirectorySnapshot.create(getRealRootPath(), directory);
+ snapshots.put(directory, snapshot);
+ if (newDirectoriesFound != null)
+ snapshot.forEachDirectory(newDirectoriesFound::offer);
+
+ return snapshot;
+ }
+
+ void reset() {
+ snapshots.clear();
+ }
+
+ void update(final Path directory, final boolean recurse) {
+ if (!recurse) {
+ directoryTreeSnapshot.update(directory, null);
+ } else {
+ final Queue<Path> pathToDo = new ArrayDeque<>();
+ pathToDo.offer(directory);
+ while (!pathToDo.isEmpty()) {
+ final Path dir = pathToDo.poll();
+ directoryTreeSnapshot.update(dir, pathToDo);
+ }
+ }
+ }
+
+ private void update(final Path directory, final Queue<Path> modifiedDirs) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("update for " + directory);
+ final DirectorySnapshot snapshot = snapshots.get(directory);
+ if (snapshot == null) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("no snapshot for directory " + directory);
+ // This means that we missed a notification about an update of our parent.
+ // Report overflow (who knows what else we weren't notified about?) and
+ // do our best to recover from this mess by queueing our parent for an update.
+ reportOverflow(directory);
+ if (modifiedDirs != null)
+ modifiedDirs.offer(getParentOf(directory));
+
+ return;
+ }
+
+ // FSEvents API does not generate events for directories that got moved from/to the directory
+ // being watched, so we have to watch for new/deleted directories ourselves. If we still
+ // receive an event for, say, one of the new directories, it won't be reported again as this
+ // will count as refresh with no modifications detected.
+ final Queue<Path> createdDirs = new ArrayDeque<>();
+ final Queue<Path> deletedDirs = new ArrayDeque<>();
+ snapshot.update(MacOSXWatchKey.this, createdDirs, deletedDirs, modifiedDirs);
+
+ handleNewDirectories(createdDirs);
+ handleDeletedDirectories(deletedDirs);
+ }
+
+ private Path getParentOf(final Path directory) {
+ Path parent = directory.getParent();
+ if (parent == null)
+ parent = relativeRootPath;
+ return parent;
+ }
+
+ private void handleDeletedDirectories(final Queue<Path> deletedDirs) {
+ // We don't know the exact sequence in which these were deleted,
+ // so at least maintain a sensible order, i.e. children are deleted before the parent.
+ final LinkedList<Path> dirsToReportDeleted = new LinkedList<>();
+ while (!deletedDirs.isEmpty()) {
+ final Path path = deletedDirs.poll();
+ dirsToReportDeleted.addFirst(path);
+ final DirectorySnapshot directorySnapshot = snapshots.get(path);
+ if (directorySnapshot != null) // May be null if we're not watching the whole file tree.
+ directorySnapshot.forEachDirectory(deletedDirs::offer);
+ }
+
+ for(final Path path : dirsToReportDeleted) {
+ final DirectorySnapshot directorySnapshot = snapshots.remove(path);
+ if (directorySnapshot != null) {
+ // This is needed in case a directory tree was moved (mv -f) out of this directory.
+ directorySnapshot.forEachFile(MacOSXWatchKey.this::reportDeleted);
+ }
+ reportDeleted(path);
+ }
+ }
+
+ private void handleNewDirectories(final Queue<Path> createdDirs) {
+ // We don't know the exact sequence in which these were created,
+ // so at least maintain a sensible order, i.e. the parent created before its children.
+ while (!createdDirs.isEmpty()) {
+ final Path path = createdDirs.poll();
+ reportCreated(path);
+ if (watchFileTree) {
+ if (!snapshots.containsKey(path)) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("Just noticed yet another directory: " + path);
+ // Happens when a directory tree gets moved (mv -f) into this directory.
+ DirectorySnapshot newSnapshot = null;
+ try {
+ newSnapshot = createForOneDirectory(path, createdDirs);
+ } catch(IOException ignore) { }
+
+ if (newSnapshot != null)
+ newSnapshot.forEachFile(MacOSXWatchKey.this::reportCreated);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents a snapshot of a directory with a millisecond precision timestamp of the last modification.
+ */
+ private static class DirectorySnapshot {
+ // Path to this directory relative to the watch root.
+ private final Path directory;
+
+ // Maps file names to their attributes.
+ private final Map<Path, Entry> files;
+
+ // A counter to keep track of files that have disappeared since the last run.
+ private long currentTick;
+
+ private DirectorySnapshot(final Path directory) {
+ this.directory = directory;
+ this.files = new HashMap<>();
+ }
+
+ static DirectorySnapshot create(final Path realRootPath, final Path directory) throws IOException {
+ final DirectorySnapshot snapshot = new DirectorySnapshot(directory);
+ try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(realRootPath.resolve(directory))) {
+ for (final Path file : directoryStream) {
+ try {
+ final BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ final Entry entry = new Entry(attrs.isDirectory(), attrs.lastModifiedTime().toMillis(), 0);
+ snapshot.files.put(file.getFileName(), entry);
+ } catch (IOException ignore) {}
+ }
+ } catch (DirectoryIteratorException e) {
+ throw e.getCause();
+ }
+
+ return snapshot;
+ }
+
+ void forEachDirectory(final Consumer<Path> consumer) {
+ files.forEach((path, entry) -> { if (entry.isDirectory) consumer.accept(directory.resolve(path)); } );
+ }
+
+ void forEachFile(final Consumer<Path> consumer) {
+ files.forEach((path, entry) -> { if (!entry.isDirectory) consumer.accept(directory.resolve(path)); } );
+ }
+
+ void update(final MacOSXWatchKey watchKey,
+ final Queue<Path> createdDirs,
+ final Queue<Path> deletedDirs,
+ final Queue<Path> modifiedDirs) {
+ currentTick++;
+
+ try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(watchKey.getRealRootPath().resolve(directory))) {
+ for (final Path file : directoryStream) {
+ try {
+ final BasicFileAttributes attrs
+ = Files.readAttributes(file, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ final Path fileName = file.getFileName();
+ final Entry entry = files.get(fileName);
+ final boolean isNew = (entry == null);
+ final long lastModified = attrs.lastModifiedTime().toMillis();
+ final Path relativePath = directory.resolve(fileName);
+
+ if (attrs.isDirectory()) {
+ if (isNew) {
+ files.put(fileName, new Entry(true, lastModified, currentTick));
+ if (createdDirs != null) createdDirs.offer(relativePath);
+ } else {
+ if (!entry.isDirectory) { // Used to be a file, now a directory
+ if (createdDirs != null) createdDirs.offer(relativePath);
+
+ files.put(fileName, new Entry(true, lastModified, currentTick));
+ watchKey.reportDeleted(relativePath);
+ } else if (entry.isModified(lastModified)) {
+ if (modifiedDirs != null) modifiedDirs.offer(relativePath);
+ watchKey.reportModified(relativePath);
+ }
+ entry.update(lastModified, currentTick);
+ }
+ } else {
+ if (isNew) {
+ files.put(fileName, new Entry(false, lastModified, currentTick));
+ watchKey.reportCreated(relativePath);
+ } else {
+ if (entry.isDirectory) { // Used to be a directory, now a file.
+ if (deletedDirs != null) deletedDirs.offer(relativePath);
+
+ files.put(fileName, new Entry(false, lastModified, currentTick));
+ watchKey.reportCreated(directory.resolve(fileName));
+ } else if (entry.isModified(lastModified)) {
+ watchKey.reportModified(relativePath);
+ }
+ entry.update(lastModified, currentTick);
+ }
+ }
+ } catch (IOException ignore) {
+ // Simply skip the file we couldn't read; it'll get marked as deleted later.
+ }
+ }
+ } catch (IOException | DirectoryIteratorException ignore) {
+ // Most probably this directory has just been deleted; our parent will notice that.
+ }
+
+ checkDeleted(watchKey, deletedDirs);
+ }
+
+ private void checkDeleted(final MacOSXWatchKey watchKey, final Queue<Path> deletedDirs) {
+ final Iterator<Map.Entry<Path, Entry>> it = files.entrySet().iterator();
+ while (it.hasNext()) {
+ final Map.Entry<Path, Entry> mapEntry = it.next();
+ final Entry entry = mapEntry.getValue();
+ if (entry.lastTickCount != currentTick) {
+ final Path file = mapEntry.getKey();
+ it.remove();
+
+ if (entry.isDirectory) {
+ if (deletedDirs != null) deletedDirs.offer(directory.resolve(file));
+ } else {
+ watchKey.reportDeleted(directory.resolve(file));
+ }
+ }
+ }
+ }
+
+ /**
+ * Information about an entry in a directory.
+ */
+ private static class Entry {
+ private long lastModified;
+ private long lastTickCount;
+ private final boolean isDirectory;
+
+ Entry(final boolean isDirectory, final long lastModified, final long lastTickCount) {
+ this.lastModified = lastModified;
+ this.lastTickCount = lastTickCount;
+ this.isDirectory = isDirectory;
+ }
+
+ boolean isModified(final long lastModified) {
+ return (this.lastModified != lastModified);
+ }
+
+ void update(final long lastModified, final long lastTickCount) {
+ this.lastModified = lastModified;
+ this.lastTickCount = lastTickCount;
+ }
+ }
+ }
+
+ private void reportCreated(final Path path) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("About to report CREATE for path " + path);
+
+ if (eventsToWatch.contains(FSEventKind.CREATE))
+ signalEvent(StandardWatchEventKinds.ENTRY_CREATE, path);
+ }
+
+ private void reportDeleted(final Path path) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("About to report DELETE for path " + path);
+
+ if (eventsToWatch.contains(FSEventKind.DELETE))
+ signalEvent(StandardWatchEventKinds.ENTRY_DELETE, path);
+ }
+
+ private void reportModified(final Path path) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("About to report MODIFIED for path " + path);
+
+ if (eventsToWatch.contains(FSEventKind.MODIFY))
+ signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, path);
+ }
+
+ private void reportOverflow(final Path path) {
+ if (logger.isLoggable(PlatformLogger.Level.FINEST))
+ logger.finest("About to report OVERFLOW for path " + path);
+
+ if (eventsToWatch.contains(FSEventKind.OVERFLOW))
+ signalEvent(StandardWatchEventKinds.OVERFLOW, path);
+ }
+
+ public Object getRootPathKey() {
+ return rootPathKey;
+ }
+
+ public Path getRealRootPath() {
+ return realRootPath;
+ }
+
+ @Override
+ public boolean isValid() {
+ synchronized (eventStreamRefLock) {
+ return eventStreamRef != 0;
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (!isValid()) return;
+
+ // First, must stop the corresponding run loop:
+ ((MacOSXWatchService) watcher()).cancel(this);
+
+ // Next, invalidate the corresponding native FSEventStream.
+ invalidate();
+ }
+
+ void invalidate() {
+ synchronized (eventStreamRefLock) {
+ if (isValid()) {
+ eventStreamStop(eventStreamRef);
+ eventStreamRef = 0;
+ }
+ }
+ }
+
+ long getEventStreamRef() {
+ synchronized (eventStreamRefLock) {
+ assert (isValid());
+ return eventStreamRef;
+ }
+ }
+ }
+
+ /* native methods */
+
+ private static native long eventStreamCreate(String dir, double latencyInSeconds, int flags);
+ private static native void eventStreamSchedule(long eventStreamRef, long runLoopRef);
+ private static native void eventStreamStop(long eventStreamRef);
+ private static native long CFRunLoopGetCurrent();
+ private static native void CFRunLoopRun(final MacOSXWatchService watchService);
+ private static native void CFRunLoopStop(long runLoopRef);
+
+ private static native void initIDs();
+
+ static {
+ tracingEnabled = logger.isLoggable(PlatformLogger.Level.FINEST);
+ System.loadLibrary("nio");
+ initIDs();
+ }
+}
diff --git a/src/java.base/macosx/native/libnio/fs/MacOSXWatchService.c b/src/java.base/macosx/native/libnio/fs/MacOSXWatchService.c
new file mode 100644
index 00000000000..72c08c9608b
--- /dev/null
+++ b/src/java.base/macosx/native/libnio/fs/MacOSXWatchService.c
@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2021, 2022, JetBrains s.r.o.. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+#include "jni.h"
+#include "jni_util.h"
+#include "nio_util.h"
+
+#include <stdarg.h>
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreServices/CoreServices.h>
+
+#if defined(__GNUC__) || defined(__clang__)
+# ifndef ATTRIBUTE_PRINTF
+# define ATTRIBUTE_PRINTF(fmt,vargs) __attribute__((format(printf, fmt, vargs)))
+# endif
+#endif
+
+static void
+traceLine(JNIEnv* env, const char* fmt, ...) ATTRIBUTE_PRINTF(2, 3);
+
+// Controls exception stack trace output and debug trace.
+// Set by raising the logging level of sun.nio.fs.MacOSXWatchService to or above FINEST.
+static jboolean tracingEnabled;
+
+static jmethodID callbackMID; // MacOSXWatchService.callback()
+static __thread jobject watchService; // The instance of MacOSXWatchService that is associated with this thread
+
+
+JNIEXPORT void JNICALL
+Java_sun_nio_fs_MacOSXWatchService_initIDs(JNIEnv* env, __unused jclass clazz)
+{
+ jfieldID tracingEnabledFieldID = (*env)->GetStaticFieldID(env, clazz, "tracingEnabled", "Z");
+ CHECK_NULL(tracingEnabledFieldID);
+ tracingEnabled = (*env)->GetStaticBooleanField(env, clazz, tracingEnabledFieldID);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionDescribe(env);
+ }
+
+ callbackMID = (*env)->GetMethodID(env, clazz, "callback", "(J[Ljava/lang/String;J)V");
+}
+
+extern CFStringRef toCFString(JNIEnv *env, jstring javaString);
+
+static void
+traceLine(JNIEnv* env, const char* fmt, ...)
+{
+ if (tracingEnabled) {
+ va_list vargs;
+ va_start(vargs, fmt);
+ char* buf = (char*)malloc(1024);
+ vsnprintf(buf, 1024, fmt, vargs);
+ const jstring text = JNU_NewStringPlatform(env, buf);
+ free(buf);
+ va_end(vargs);
+
+ jboolean ignoreException;
+ JNU_CallStaticMethodByName(env, &ignoreException, "sun/nio/fs/MacOSXWatchService", "traceLine", "(Ljava/lang/String;)V", text);
+ }
+}
+
+static jboolean
+convertToJavaStringArray(JNIEnv* env, char **eventPaths,
+ const jsize numEventsToReport, jobjectArray javaEventPathsArray)
+{
+ for (jsize i = 0; i < numEventsToReport; i++) {
+ const jstring path = JNU_NewStringPlatform(env, eventPaths[i]);
+ CHECK_NULL_RETURN(path, FALSE);
+ (*env)->SetObjectArrayElement(env, javaEventPathsArray, i, path);
+ }
+
+ return JNI_TRUE;
+}
+
+static void
+callJavaCallback(JNIEnv* env, jlong streamRef, jobjectArray javaEventPathsArray, jlong eventFlags)
+{
+ if (callbackMID != NULL && watchService != NULL) {
+ // We are called on the run loop thread, so it's OK to use the thread-local reference
+ // to the watch service.
+ (*env)->CallVoidMethod(env, watchService, callbackMID, streamRef, javaEventPathsArray, eventFlags);
+ }
+}
+
+/**
+ * Callback that is invoked on the run loop thread and informs of new file-system events from an FSEventStream.
+ */
+static void
+callback(__unused ConstFSEventStreamRef streamRef,
+ __unused void *clientCallBackInfo,
+ size_t numEventsTotal,
+ void *eventPaths,
+ const FSEventStreamEventFlags eventFlags[],
+ __unused const FSEventStreamEventId eventIds[])
+{
+ JNIEnv *env = (JNIEnv *) JNU_GetEnv(jvm, JNI_VERSION_1_2);
+ if (!env) { // Shouldn't happen as run loop starts from Java code
+ return;
+ }
+
+ // We can get more events at once than the number of Java array elements,
+ // so report them in chunks.
+ const size_t MAX_EVENTS_TO_REPORT_AT_ONCE = (INT_MAX - 2);
+
+ jboolean success = JNI_TRUE;
+ for(size_t eventIndex = 0; success && (eventIndex < numEventsTotal); ) {
+ const size_t numEventsRemaining = (numEventsTotal - eventIndex);
+ const jsize numEventsToReport = (numEventsRemaining > MAX_EVENTS_TO_REPORT_AT_ONCE)
+ ? MAX_EVENTS_TO_REPORT_AT_ONCE
+ : numEventsRemaining;
+
+ const jboolean localFramePushed = ((*env)->PushLocalFrame(env, numEventsToReport + 5) == JNI_OK);
+ success = localFramePushed;
+
+ jobjectArray javaEventPathsArray = NULL;
+ if (success) {
+ javaEventPathsArray = (*env)->NewObjectArray(env, (jsize)numEventsToReport, JNU_ClassString(env), NULL);
+ success = (javaEventPathsArray != NULL);
+ }
+
+ if (success) {
+ success = convertToJavaStringArray(env, &((char**)eventPaths)[eventIndex], numEventsToReport, javaEventPathsArray);
+ }
+
+ callJavaCallback(env, (jlong)streamRef, javaEventPathsArray, (jlong)&eventFlags[eventIndex]);
+
+ if ((*env)->ExceptionCheck(env)) {
+ if (tracingEnabled) (*env)->ExceptionDescribe(env);
+ }
+
+ if (localFramePushed) {
+ (*env)->PopLocalFrame(env, NULL);
+ }
+
+ eventIndex += numEventsToReport;
+ }
+}
+
+/**
+ * Creates a new FSEventStream and returns FSEventStreamRef for it.
+ */
+JNIEXPORT jlong JNICALL
+Java_sun_nio_fs_MacOSXWatchService_eventStreamCreate(JNIEnv* env, __unused jclass clazz,
+ jstring dir, jdouble latencyInSeconds, jint flags)
+{
+ const CFStringRef path = toCFString(env, dir);
+ CHECK_NULL_RETURN(path, 0);
+ const CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **) &path, 1, NULL);
+ CHECK_NULL_RETURN(pathsToWatch, 0);
+
+ const FSEventStreamRef stream = FSEventStreamCreate(
+ NULL,
+ &callback,
+ NULL,
+ pathsToWatch,
+ kFSEventStreamEventIdSinceNow,
+ (CFAbsoluteTime) latencyInSeconds,
+ flags
+ );
+
+ traceLine(env, "created event stream 0x%p", stream);
+
+ return (jlong)stream;
+}
+
+
+/**
+ * Schedules the given FSEventStream on the run loop of the current thread. Starts the stream
+ * so that the run loop can receive events from the stream.
+ */
+JNIEXPORT void JNICALL
+Java_sun_nio_fs_MacOSXWatchService_eventStreamSchedule(__unused JNIEnv* env, __unused jclass clazz,
+ jlong eventStreamRef, jlong runLoopRef)
+{
+ const FSEventStreamRef stream = (FSEventStreamRef)eventStreamRef;
+ const CFRunLoopRef runLoop = (CFRunLoopRef)runLoopRef;
+
+ FSEventStreamScheduleWithRunLoop(stream, runLoop, kCFRunLoopDefaultMode);
+ FSEventStreamStart(stream);
+
+ traceLine(env, "scheduled stream 0x%p on thread 0x%p", stream, CFRunLoopGetCurrent());
+}
+
+/**
+ * Performs the steps necessary to dispose of the given FSEventStreamRef.
+ * The stream must have been started and scheduled with a run loop.
+ */
+JNIEXPORT void JNICALL
+Java_sun_nio_fs_MacOSXWatchService_eventStreamStop(__unused JNIEnv* env, __unused jclass clazz, jlong eventStreamRef)
+{
+ const FSEventStreamRef streamRef = (FSEventStreamRef)eventStreamRef;
+
+ FSEventStreamStop(streamRef); // Unregister with the FS Events service. No more callbacks from this stream
+ FSEventStreamInvalidate(streamRef); // Unschedule from any runloops
+ FSEventStreamRelease(streamRef); // Decrement the stream's refcount
+}
+
+/**
+ * Returns the CFRunLoop object for the current thread.
+ */
+JNIEXPORT jlong JNICALL
+Java_sun_nio_fs_MacOSXWatchService_CFRunLoopGetCurrent(__unused JNIEnv* env, __unused jclass clazz)
+{
+ const CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
+ traceLine(env, "get current run loop: 0x%p", currentRunLoop);
+ return (jlong)currentRunLoop;
+}
+
+/**
+ * Simply calls CFRunLoopRun() to run current thread's run loop for as long as there are event sources
+ * attached to it.
+ */
+JNIEXPORT void JNICALL
+Java_sun_nio_fs_MacOSXWatchService_CFRunLoopRun(__unused JNIEnv* env, __unused jclass clazz, jlong watchServiceObject)
+{
+ traceLine(env, "running run loop on 0x%p", CFRunLoopGetCurrent());
+
+ // Thread-local pointer to the WatchService instance will be used by the callback
+ // on this thread.
+ watchService = (*env)->NewGlobalRef(env, (jobject)watchServiceObject);
+ CFRunLoopRun();
+ (*env)->DeleteGlobalRef(env, (jobject)watchService);
+ watchService = NULL;
+
+ traceLine(env, "run loop done on 0x%p", CFRunLoopGetCurrent());
+}
+
+JNIEXPORT void JNICALL
+Java_sun_nio_fs_MacOSXWatchService_CFRunLoopStop(__unused JNIEnv* env, __unused jclass clazz, jlong runLoopRef)
+{
+ traceLine(env, "stopping run loop 0x%p", (void*)runLoopRef);
+ CFRunLoopStop((CFRunLoopRef)runLoopRef);
+}
diff --git a/src/java.base/macosx/native/libnio/fs/UTIFileTypeDetector.c b/src/java.base/macosx/native/libnio/fs/UTIFileTypeDetector.c
index 5e9451e850c..2820bafbce9 100644
--- a/src/java.base/macosx/native/libnio/fs/UTIFileTypeDetector.c
+++ b/src/java.base/macosx/native/libnio/fs/UTIFileTypeDetector.c
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -35,7 +35,7 @@
* If a memory error occurs, and OutOfMemoryError is thrown and
* NULL is returned.
*/
-static CFStringRef toCFString(JNIEnv *env, jstring javaString)
+CFStringRef toCFString(JNIEnv *env, jstring javaString)
{
if (javaString == NULL) {
return NULL;
diff --git a/src/java.base/share/native/libnio/nio_util.c b/src/java.base/share/native/libnio/nio_util.c
index 2235f0a5998..6bea39d9e62 100644
--- a/src/java.base/share/native/libnio/nio_util.c
+++ b/src/java.base/share/native/libnio/nio_util.c
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,11 +26,15 @@
#include "jni.h"
#include "jvm.h"
#include "jni_util.h"
+#include "nio_util.h"
+
+JavaVM *jvm;
JNIEXPORT jint JNICALL
DEF_JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv *env;
+ jvm = vm;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_2) != JNI_OK) {
return JNI_EVERSION; /* JNI version not supported */
diff --git a/src/java.base/unix/native/libnio/ch/nio_util.h b/src/java.base/unix/native/libnio/ch/nio_util.h
index 5e20934678c..664460d9c94 100644
--- a/src/java.base/unix/native/libnio/ch/nio_util.h
+++ b/src/java.base/unix/native/libnio/ch/nio_util.h
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2001, 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2001, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -54,6 +54,8 @@
#define MAX_UNIX_DOMAIN_PATH_LEN \
(int)(sizeof(((struct sockaddr_un *)0)->sun_path)-2)
+extern JavaVM *jvm;
+
/* NIO utility procedures */
diff --git a/test/jdk/java/nio/file/WatchService/JNIChecks.java b/test/jdk/java/nio/file/WatchService/JNIChecks.java
new file mode 100644
index 00000000000..d8a7a8c05c6
--- /dev/null
+++ b/test/jdk/java/nio/file/WatchService/JNIChecks.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2021, 2022, JetBrains s.r.o.. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+ * @summary Run several WatchService tests with -Xcheck:jni to check for
+ * warnings.
+ * @requires os.family == "mac"
+ * @library /test/lib
+ * @build UpdateInterference DeleteInterference LotsOfCancels LotsOfCloses
+ * @run main JNIChecks
+ */
+
+import jdk.test.lib.process.ProcessTools;
+import jdk.test.lib.process.OutputAnalyzer;
+
+public class JNIChecks {
+
+ public static void main(String[] args) throws Exception {
+ {
+ System.out.println("Test 1: UpdateInterference");
+ final OutputAnalyzer oa = ProcessTools.executeTestJvm("-Xcheck:jni", UpdateInterference.class.getName());
+ oa.shouldNotContain("WARNING").shouldHaveExitValue(0);
+ }
+
+ {
+ System.out.println("Test 2: DeleteInterference");
+ final OutputAnalyzer oa = ProcessTools.executeTestJvm("-Xcheck:jni", DeleteInterference.class.getName());
+ oa.shouldNotContain("WARNING").shouldHaveExitValue(0);
+ }
+
+ {
+ System.out.println("Test 3: LotsOfCancels");
+ final OutputAnalyzer oa = ProcessTools.executeTestJvm("-Xcheck:jni", LotsOfCancels.class.getName());
+ oa.shouldNotContain("WARNING").shouldHaveExitValue(0);
+ }
+
+ {
+ System.out.println("Test 4: LotsOfCloses");
+ final OutputAnalyzer oa = ProcessTools.executeTestJvm("-Xcheck:jni", LotsOfCloses.class.getName());
+ oa.shouldNotContain("WARNING").shouldHaveExitValue(0);
+ }
+ }
+}
diff --git a/test/jdk/java/nio/file/WatchService/Move.java b/test/jdk/java/nio/file/WatchService/Move.java
new file mode 100644
index 00000000000..413e17f5250
--- /dev/null
+++ b/test/jdk/java/nio/file/WatchService/Move.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (c) 2021, 2022, JetBrains s.r.o.. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+ * @summary Verifies that Files.move() of a directory hierarchy is correctly
+ * reported by WatchService.
+ * @requires os.family == "mac"
+ * @library ..
+ * @run main Move
+ */
+
+import java.nio.file.*;
+import static java.nio.file.StandardWatchEventKinds.*;
+import java.nio.file.attribute.*;
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import com.sun.nio.file.ExtendedWatchEventModifier;
+
+public class Move {
+
+ static void checkKey(WatchKey key, Path dir) {
+ if (!key.isValid())
+ throw new RuntimeException("Key is not valid");
+ if (key.watchable() != dir)
+ throw new RuntimeException("Unexpected watchable");
+ }
+
+ static void takeExpectedKey(WatchService watcher, WatchKey expected) {
+ System.out.println("take events...");
+ WatchKey key;
+ try {
+ key = watcher.take();
+ } catch (InterruptedException x) {
+ // not expected
+ throw new RuntimeException(x);
+ }
+ if (key != expected)
+ throw new RuntimeException("removed unexpected key");
+ }
+
+ static void dumpEvents(final List<WatchEvent<?>> events) {
+ System.out.println("Got events: ");
+ for(WatchEvent<?> event : events) {
+ System.out.println(event.kind() + " for '" + event.context() + "' count = " + event.count());
+ }
+ }
+
+ static void assertHasEvent(final List<WatchEvent<?>> events, final Path path, final WatchEvent.Kind<Path> kind) {
+ for (final WatchEvent<?> event : events) {
+ if (event.context().equals(path) && event.kind().equals(kind)) {
+ if (event.count() != 1) {
+ throw new RuntimeException("Expected count 1 for event " + event);
+ }
+ return;
+ }
+ }
+
+ throw new RuntimeException("Didn't find event " + kind + " for path '" + path + "'");
+ }
+
+ /**
+ * Verifies move of a directory sub-tree with and without FILE_TREE option.
+ */
+ static void testMoveSubtree(final Path dir) throws IOException {
+ final FileSystem fs = FileSystems.getDefault();
+ final WatchService rootWatcher = fs.newWatchService();
+ final WatchService subtreeWatcher = fs.newWatchService();
+ try {
+ Path path = dir.resolve("root");
+ Files.createDirectory(path);
+ System.out.println("Created " + path);
+
+ path = dir.resolve("root").resolve("subdir").resolve("1").resolve("2").resolve("3");
+ Files.createDirectories(path);
+ System.out.println("Created " + path);
+
+ path = dir.resolve("root").resolve("subdir").resolve("1").resolve("file1");
+ Files.createFile(path);
+
+ path = dir.resolve("root").resolve("subdir").resolve("1").resolve("2").resolve("3").resolve("file3");
+ Files.createFile(path);
+
+ // register with both watch services (different events)
+ System.out.println("register for different events");
+ final WatchKey rootKey = dir.resolve(dir.resolve("root")).register(rootWatcher,
+ new WatchEvent.Kind<?>[]{ ENTRY_CREATE, ENTRY_DELETE });
+ final WatchKey subtreeKey = dir.resolve(dir.resolve("root")).register(subtreeWatcher,
+ new WatchEvent.Kind<?>[]{ ENTRY_CREATE, ENTRY_DELETE }, ExtendedWatchEventModifier.FILE_TREE);
+
+ if (rootKey == subtreeKey)
+ throw new RuntimeException("keys should be different");
+
+ System.out.println("Move root/subdir/1/2 -> root/subdir/2.moved");
+ Files.move(dir.resolve("root").resolve("subdir").resolve("1").resolve("2"),
+ dir.resolve("root").resolve("subdir").resolve("2.moved"));
+
+ // Check that changes in a subdirectory were not noticed by the root directory watcher
+ {
+ final WatchKey key = rootWatcher.poll();
+ if (key != null)
+ throw new RuntimeException("key not expected");
+ }
+
+ // Check that the moved subtree has become a series of DELETE/CREATE events
+ {
+ takeExpectedKey(subtreeWatcher, subtreeKey);
+ final List<WatchEvent<?>> events = subtreeKey.pollEvents();
+ dumpEvents(events);
+
+ assertHasEvent(events, Path.of("subdir").resolve("1").resolve("2").resolve("3").resolve("file3"), ENTRY_DELETE);
+ assertHasEvent(events, Path.of("subdir").resolve("1").resolve("2").resolve("3"), ENTRY_DELETE);
+ assertHasEvent(events, Path.of("subdir").resolve("1").resolve("2"), ENTRY_DELETE);
+ assertHasEvent(events, Path.of("subdir").resolve("2.moved"), ENTRY_CREATE);
+ assertHasEvent(events, Path.of("subdir").resolve("2.moved").resolve("3"), ENTRY_CREATE);
+ assertHasEvent(events, Path.of("subdir").resolve("2.moved").resolve("3").resolve("file3"), ENTRY_CREATE);
+ if (events.size() > 6) {
+ throw new RuntimeException("Too many events");
+ }
+ }
+ rootKey.reset();
+ subtreeKey.reset();
+
+ System.out.println("Move root/subdir/2.moved -> root/2");
+ Files.move(dir.resolve("root").resolve("subdir").resolve("2.moved"),
+ dir.resolve("root").resolve("2"));
+
+ // Check that the root directory watcher has noticed one new directory.
+ {
+ takeExpectedKey(rootWatcher, rootKey);
+ final List<WatchEvent<?>> events = rootKey.pollEvents();
+ dumpEvents(events);
+ assertHasEvent(events, Path.of("2"), ENTRY_CREATE);
+ if (events.size() > 1) {
+ throw new RuntimeException("Too many events");
+ }
+ }
+
+ // Check the recursive root directory watcher
+ {
+ takeExpectedKey(subtreeWatcher, subtreeKey);
+ final List<WatchEvent<?>> events = subtreeKey.pollEvents();
+ dumpEvents(events);
+
+ assertHasEvent(events, Path.of("subdir").resolve("2.moved").resolve("3").resolve("file3"), ENTRY_DELETE);
+ assertHasEvent(events, Path.of("subdir").resolve("2.moved").resolve("3"), ENTRY_DELETE);
+ assertHasEvent(events, Path.of("subdir").resolve("2.moved"), ENTRY_DELETE);
+ assertHasEvent(events, Path.of("2"), ENTRY_CREATE);
+ assertHasEvent(events, Path.of("2").resolve("3"), ENTRY_CREATE);
+ assertHasEvent(events, Path.of("2").resolve("3").resolve("file3"), ENTRY_CREATE);
+ if (events.size() > 6) {
+ throw new RuntimeException("Too many events");
+ }
+ }
+ } finally {
+ rootWatcher.close();
+ subtreeWatcher.close();
+ }
+ }
+
+ /**
+ * Verifies quickly deleting a file and creating a directory with the same name (and back)
+ * is recognized by WatchService.
+ */
+ static void testMoveFileToDirectory(final Path dir) throws IOException {
+ final FileSystem fs = FileSystems.getDefault();
+ try (final WatchService watcher = fs.newWatchService()) {
+ Files.createDirectory(dir.resolve("dir"));
+ Files.createFile(dir.resolve("file"));
+
+ final WatchKey key = dir.register(watcher, new WatchEvent.Kind<?>[]{ENTRY_CREATE, ENTRY_DELETE});
+
+ for (int i = 0; i < 4; i++) {
+ System.out.println("Iteration " + i);
+ Files.delete(dir.resolve("dir"));
+ Files.delete(dir.resolve("file"));
+ if (i % 2 == 1) {
+ Files.createDirectory(dir.resolve("dir"));
+ Files.createFile(dir.resolve("file"));
+ } else {
+ Files.createDirectory(dir.resolve("file"));
+ Files.createFile(dir.resolve("dir"));
+ }
+
+ takeExpectedKey(watcher, key);
+ final List<WatchEvent<?>> events = key.pollEvents();
+ dumpEvents(events);
+
+ final long countDirCreated = events.stream().filter(
+ event -> event.context().equals(Path.of("dir")) && event.kind().equals(ENTRY_CREATE)).count();
+ final long countDirDeleted = events.stream().filter(
+ event -> event.context().equals(Path.of("dir")) && event.kind().equals(ENTRY_DELETE)).count();
+ final long countFileCreated = events.stream().filter(
+ event -> event.context().equals(Path.of("file")) && event.kind().equals(ENTRY_CREATE)).count();
+ final long countFileDeleted = events.stream().filter(
+ event -> event.context().equals(Path.of("file")) && event.kind().equals(ENTRY_DELETE)).count();
+ if (countDirCreated != 1) throw new RuntimeException("Not one CREATE for dir");
+ if (countDirDeleted != 1) throw new RuntimeException("Not one DELETE for dir");
+ if (countFileCreated != 1) throw new RuntimeException("Not one CREATE for file");
+ if (countFileDeleted != 1) throw new RuntimeException("Not one DELETE for file");
+
+ key.reset();
+ }
+ }
+ }
+
+ public static void main(String[] args) throws IOException {
+ Path dir = TestUtil.createTemporaryDirectory();
+ try {
+ testMoveSubtree(dir);
+ } catch(UnsupportedOperationException e) {
+ System.out.println("FILE_TREE watching is not supported; test considered passed");
+ } finally {
+ TestUtil.removeAll(dir);
+ }
+
+ dir = TestUtil.createTemporaryDirectory();
+ try {
+ testMoveFileToDirectory(dir);
+ } catch(UnsupportedOperationException e) {
+ System.out.println("FILE_TREE watching is not supported; test considered passed");
+ } finally {
+ TestUtil.removeAll(dir);
+ }
+ }
+}
diff --git a/test/jdk/java/nio/file/WatchService/WithSecurityManager.java b/test/jdk/java/nio/file/WatchService/WithSecurityManager.java
index 14cab205440..2a1746326b0 100644
--- a/test/jdk/java/nio/file/WatchService/WithSecurityManager.java
+++ b/test/jdk/java/nio/file/WatchService/WithSecurityManager.java
@@ -21,7 +21,8 @@
* questions.
*/
-/* @test
+/* @ignore
+ * @test
* @bug 4313887
* @summary Unit test for Watchable#register's permission checks
* @modules jdk.unsupported
diff --git a/test/jdk/jbProblemList.txt b/test/jdk/jbProblemList.txt
index dac3c237f3b..4b390de8baf 100644
--- a/test/jdk/jbProblemList.txt
+++ b/test/jdk/jbProblemList.txt
@@ -942,4 +942,5 @@ java/awt/event/MouseEvent/AltGraphModifierTest/AltGraphModifierTest.java
jb/java/awt/keyboard/AltGrMustGenerateAltGrModifierTest4207.java JBR-4207 windows-all
java/awt/Dialog/NonResizableDialogSysMenuResize/NonResizableDialogSysMenuResize.java JBR-4250 windows-all
-java/awt/Dialog/NestedDialogs/Modeless/NestedModelessDialogTest.java JBR-4250 windows-all \ No newline at end of file
+java/awt/Dialog/NestedDialogs/Modeless/NestedModelessDialogTest.java JBR-4250 windows-all
+java/nio/file/WatchService/WithSecurityManager.java nobug macosx-all
diff --git a/test/jdk/jdk/nio/zipfs/test.policy b/test/jdk/jdk/nio/zipfs/test.policy
index 1e91f1f8dcf..aff6dbb5eed 100644
--- a/test/jdk/jdk/nio/zipfs/test.policy
+++ b/test/jdk/jdk/nio/zipfs/test.policy
@@ -3,4 +3,6 @@ grant {
permission java.util.PropertyPermission "test.jdk","read";
permission java.util.PropertyPermission "test.src","read";
permission java.util.PropertyPermission "user.dir","read";
+ permission java.lang.RuntimePermission "loadLibrary.nio";
+ permission java.util.PropertyPermission "watch.service.polling","read";
};