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).
This commit is contained in:
Maxim Kartashev
2021-10-09 13:46:20 +03:00
committed by alexey.ushakov@jetbrains.com
parent c05a9636fd
commit b9fc64d60c
11 changed files with 1408 additions and 11 deletions

View File

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

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

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

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -21,7 +21,8 @@
* questions.
*/
/* @test
/* @ignore
* @test
* @bug 4313887
* @summary Unit test for Watchable#register's permission checks
* @modules jdk.unsupported

View File

@@ -876,4 +876,6 @@ com/sun/java/swing/plaf/windows/Test8173145.java
com/sun/java/swing/plaf/windows/AltFocusIssueTest.java JBR-4197 windows-all
java/awt/event/MouseEvent/AltGraphModifierTest/AltGraphModifierTest.java JBR-4207 windows-all
jb/java/awt/keyboard/AltGrMustGenerateAltGrModifierTest4207.java JBR-4207 windows-all
jb/java/awt/keyboard/AltGrMustGenerateAltGrModifierTest4207.java JBR-4207 windows-all
java/nio/file/WatchService/WithSecurityManager.java nobug macosx-all

View File

@@ -4,4 +4,6 @@ grant {
permission java.util.PropertyPermission "test.src","read";
permission java.util.PropertyPermission "user.dir","read";
permission java.lang.RuntimePermission "accessUserInformation";
permission java.lang.RuntimePermission "loadLibrary.nio";
permission java.util.PropertyPermission "watch.service.polling","read";
};