mirror of
https://github.com/JetBrains/JetBrainsRuntime.git
synced 2025-12-06 09:29:38 +01:00
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:
committed by
alexey.ushakov@jetbrains.com
parent
c05a9636fd
commit
b9fc64d60c
@@ -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
|
||||
|
||||
826
src/java.base/macosx/classes/sun/nio/fs/MacOSXWatchService.java
Normal file
826
src/java.base/macosx/classes/sun/nio/fs/MacOSXWatchService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
253
src/java.base/macosx/native/libnio/fs/MacOSXWatchService.c
Normal file
253
src/java.base/macosx/native/libnio/fs/MacOSXWatchService.c
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
|
||||
63
test/jdk/java/nio/file/WatchService/JNIChecks.java
Normal file
63
test/jdk/java/nio/file/WatchService/JNIChecks.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
246
test/jdk/java/nio/file/WatchService/Move.java
Normal file
246
test/jdk/java/nio/file/WatchService/Move.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@
|
||||
* questions.
|
||||
*/
|
||||
|
||||
/* @test
|
||||
/* @ignore
|
||||
* @test
|
||||
* @bug 4313887
|
||||
* @summary Unit test for Watchable#register's permission checks
|
||||
* @modules jdk.unsupported
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user