diff options
author | Maxim Kartashev <maxim.kartashev@jetbrains.com> | 2021-10-09 13:46:20 +0300 |
---|---|---|
committer | Maxim Kartashev <maxim.kartashev@jetbrains.com> | 2022-05-04 15:11:03 +0300 |
commit | 03f0ec3a28e2b85c4f21416743784f19b3952ef0 (patch) | |
tree | c048861b1e0e629bf797fe5ee0fd2b088b24a6cb | |
parent | 59005e679ff8863184e3bc06fbf3693b41465d40 (diff) | |
download | JetBrainsRuntime-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.java | 8 | ||||
-rw-r--r-- | src/java.base/macosx/classes/sun/nio/fs/MacOSXWatchService.java | 826 | ||||
-rw-r--r-- | src/java.base/macosx/native/libnio/fs/MacOSXWatchService.c | 253 | ||||
-rw-r--r-- | src/java.base/macosx/native/libnio/fs/UTIFileTypeDetector.c | 4 | ||||
-rw-r--r-- | src/java.base/share/native/libnio/nio_util.c | 6 | ||||
-rw-r--r-- | src/java.base/unix/native/libnio/ch/nio_util.h | 4 | ||||
-rw-r--r-- | test/jdk/java/nio/file/WatchService/JNIChecks.java | 63 | ||||
-rw-r--r-- | test/jdk/java/nio/file/WatchService/Move.java | 246 | ||||
-rw-r--r-- | test/jdk/java/nio/file/WatchService/WithSecurityManager.java | 3 | ||||
-rw-r--r-- | test/jdk/jbProblemList.txt | 3 | ||||
-rw-r--r-- | test/jdk/jdk/nio/zipfs/test.policy | 2 |
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"; }; |