Redo file watching refactor.
This reverts commit 12fbba1f4ac0a5be8926f1c25dc8ddef1f1db9eb
but also adds a background thread for keeping resources up-to-date.
This keeps the implementation closer to original but stil uses
much less resources.
Both versions of the patch (with and without a thread) pass
all tests at Google.
Change-Id: I7b5e1473a76742f1bc3e6b6eaeecbf31b69ba3a6
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/ChangedFileAccumulator.java b/dev/core/src/com/google/gwt/dev/resource/impl/ChangedFileAccumulator.java
deleted file mode 100644
index 6341c1f..0000000
--- a/dev/core/src/com/google/gwt/dev/resource/impl/ChangedFileAccumulator.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright 2014 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.dev.resource.impl;
-
-import com.google.gwt.thirdparty.guava.common.collect.BiMap;
-import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
-import com.google.gwt.thirdparty.guava.common.collect.HashMultimap;
-import com.google.gwt.thirdparty.guava.common.collect.Lists;
-import com.google.gwt.thirdparty.guava.common.collect.Multimap;
-import com.google.gwt.thirdparty.guava.common.collect.Sets;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.ClosedWatchServiceException;
-import java.nio.file.FileSystems;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.StandardWatchEventKinds;
-import java.nio.file.WatchEvent;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-
-/**
- * Listens for and accumulates file changes in a recursive directory tree even when new files are
- * changed in new directories.
- * <p>
- * Since it contains an internal thread it can't be garbage collected until it has been explicitly
- * shutdown.
- * <p>
- * Multiple instances can listen to the same directories at the same time.
- */
-public class ChangedFileAccumulator {
-
- /**
- * A runnable that polls for watch events (in a blocking manner). File changes are recorded
- * and listeners are attached to new directories.
- */
- private class WatchEventPoller implements Runnable {
- @Override
- public void run() {
- try {
- while (true) {
- WatchKey watchKey;
- try {
- watchKey = watchService.take();
- } catch (InterruptedException e) {
- // Shutdown has been requested.
- return;
- } catch (ClosedWatchServiceException e) {
- // Shutdown has been requested.
- return;
- }
-
- synchronized (changedFiles) {
- Path containingDirectory = pathsByWatchKey.get(watchKey);
-
- for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
- WatchEvent.Kind<?> eventKind = watchEvent.kind();
-
- if (eventKind == StandardWatchEventKinds.OVERFLOW) {
- setFailed(
- new RuntimeException("Changes occurred faster than they could be recorded."));
- return;
- }
-
- if (!(watchEvent.context() instanceof Path)) {
- continue;
- }
-
- Path changedFileName = (Path) watchEvent.context();
- Path changedPath = containingDirectory.resolve(changedFileName);
-
- if (eventKind == StandardWatchEventKinds.ENTRY_DELETE) {
- recursivelyRemovePath(changedPath);
- continue;
- }
-
- // Maybe listen to newly created directories.
- if (eventKind == StandardWatchEventKinds.ENTRY_CREATE
- && Files.isDirectory(changedPath)) {
- // A new directory and some contained files can be created faster than watches can
- // be attached to new directories. So it is necessary to look for files in what are
- // believed to be "newly created" directories and consider those files changed.
- try {
- recursivelyRegisterListeners(
- changedPath, true /* considerPreexistingFilesChanged */);
- } catch (IOException e) {
- setFailed(e);
- return;
- }
- }
-
- // Record changed files.
- pathsByParentPath.put(changedPath.getParent(), changedPath);
- changedFiles.add(changedPath.toFile().getAbsoluteFile());
- }
-
- // Ensures that future change events will be seen.
- if (!watchKey.reset()) {
- pathsByWatchKey.remove(watchKey);
- }
- }
- }
- } catch (RuntimeException e) {
- setFailed(e);
- }
- }
- }
-
- private final Set<File> changedFiles = Collections.synchronizedSet(Sets.<File> newHashSet());
- private Thread changePollerThread;
- private Exception changePollerException;
- private final BiMap<WatchKey, Path> pathsByWatchKey = HashBiMap.create();
- private final Multimap<Path, Path> pathsByParentPath = HashMultimap.create();
- private final WatchService watchService;
-
- public ChangedFileAccumulator(Path rootDirectory) throws IOException {
- watchService = FileSystems.getDefault().newWatchService();
- recursivelyRegisterListeners(rootDirectory, false /* considerPreexistingFilesChanged */);
- startChangePoller();
- }
-
- /**
- * Returns a sorted copy of the list of files that have changed since the last time changed files
- * were requested.
- */
- public List<File> getAndClearChangedFiles() throws ExecutionException {
- if (isFailed()) {
- throw new ExecutionException(changePollerException);
- }
-
- synchronized (changedFiles) {
- List<File> sortedChangedFiles = Lists.newArrayList(changedFiles);
- changedFiles.clear();
- Collections.sort(sortedChangedFiles);
- return sortedChangedFiles;
- }
- }
-
- public boolean isFailed() {
- return changePollerException != null;
- }
-
- public void shutdown() {
- changePollerThread.interrupt();
- try {
- watchService.close();
- } catch (IOException e) {
- changePollerException = e;
- // Am already trying to stop listening, there's nothing to be done about this failure.
- }
- changedFiles.clear();
- pathsByWatchKey.clear();
- }
-
- private void recursivelyRegisterListeners(final Path directory,
- final boolean considerPreexistingFilesChanged) throws IOException {
- Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
- @Override
- public FileVisitResult preVisitDirectory(Path currentDirectory, BasicFileAttributes attrs)
- throws IOException {
- WatchKey watchKey = currentDirectory.register(watchService,
- StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE,
- StandardWatchEventKinds.ENTRY_MODIFY);
-
- // If the recursive directory scan has gone in a loop because of symlinks.
- if (pathsByWatchKey.containsKey(watchKey)) {
- return FileVisitResult.SKIP_SUBTREE;
- }
-
- if (considerPreexistingFilesChanged) {
- changedFiles.add(currentDirectory.toFile());
- }
- pathsByParentPath.put(currentDirectory.getParent(), currentDirectory);
- pathsByWatchKey.put(watchKey, currentDirectory);
- return FileVisitResult.CONTINUE;
- }
-
- @Override
- public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
- if (attrs.isSymbolicLink()) {
- recursivelyRegisterListeners(Files.readSymbolicLink(file),
- considerPreexistingFilesChanged);
- return FileVisitResult.CONTINUE;
- }
- if (considerPreexistingFilesChanged) {
- changedFiles.add(file.toFile());
- }
- pathsByParentPath.put(file.getParent(), file);
- return FileVisitResult.CONTINUE;
- }
- });
- }
-
- private void recursivelyRemovePath(final Path path) {
- changedFiles.add(path.toFile());
-
- // Stop listening to this path
- WatchKey removed = pathsByWatchKey.inverse().remove(path);
- if (removed != null) {
- removed.cancel();
- }
-
- // Remove the path and recurse into sub directories and files.
- for (Path child : pathsByParentPath.removeAll(path)) {
- recursivelyRemovePath(child);
- }
- }
-
- private void setFailed(Exception caughtException) {
- this.changePollerException = caughtException;
- shutdown();
- }
-
- private void startChangePoller() {
- changePollerThread = new Thread(new WatchEventPoller());
- // Don't prevent JVM shutdown.
- changePollerThread.setDaemon(true);
- changePollerThread.start();
- }
-}
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java b/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java
index e496f21..041bc76 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryClassPathEntry.java
@@ -16,40 +16,17 @@
package com.google.gwt.dev.resource.impl;
import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.dev.util.Util;
-import com.google.gwt.dev.util.collect.Lists;
-import com.google.gwt.dev.util.msg.Message1String;
-import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import java.io.File;
import java.io.IOException;
-import java.util.Collection;
-import java.util.IdentityHashMap;
-import java.util.List;
import java.util.Map;
-import java.util.concurrent.ExecutionException;
/**
* A {@link ClassPathEntry} for a directory on the file system.
*/
public class DirectoryClassPathEntry extends ClassPathEntry {
- private static final String GWT_WATCH_FILE_CHANGES_PROPERTY = "gwt.watchFileChanges";
- private static final boolean WATCH_FILE_CHANGES =
- Boolean.parseBoolean(System.getProperty(GWT_WATCH_FILE_CHANGES_PROPERTY, "true"));
-
- private static class Messages {
- static final Message1String DESCENDING_INTO_DIR = new Message1String(
- TreeLogger.SPAM, "Descending into dir: $0");
-
- static final Message1String INCLUDING_FILE = new Message1String(
- TreeLogger.DEBUG, "Including file: $0");
-
- static final Message1String EXCLUDING_FILE = new Message1String(
- TreeLogger.DEBUG, "Excluding file: $0");
- }
-
/**
* Absolute directory.
*/
@@ -57,20 +34,6 @@
private final String location;
/**
- * A cache of previously collected Resource/Resolution pairs for a given PathPrefixSet.
- */
- private final
- Map<PathPrefixSet, Map<AbstractResource, ResourceResolution>>
- resolutionsByResourcePerPathPrefixSet =
- new IdentityHashMap<PathPrefixSet, Map<AbstractResource, ResourceResolution>>();
-
- /**
- * Whether changed file listening failed either to be started or at some point during execution.
- * If it has failed then no further attempts to restart should be made.
- */
- private boolean listeningFailed;
-
- /**
* @param dir an absolute directory
*/
public DirectoryClassPathEntry(File dir) {
@@ -82,147 +45,23 @@
@Override
public Map<AbstractResource, ResourceResolution> findApplicableResources(TreeLogger logger,
PathPrefixSet pathPrefixSet) {
- if (!WATCH_FILE_CHANGES) {
- return scanRecursiveDirectory(logger, pathPrefixSet);
- }
-
- ensureListening(logger, pathPrefixSet);
-
- if (listeningFailed) {
- return scanRecursiveDirectory(logger, pathPrefixSet);
- }
-
- boolean haveCachedResults = resolutionsByResourcePerPathPrefixSet.containsKey(pathPrefixSet);
- // If this is the first request and thus the cache is empty.
- if (!haveCachedResults) {
- // Then perform a full scan and cache the results.
- return scanRecursiveDirectory(logger, pathPrefixSet);
- } else {
- try {
- return scanChangedFiles(logger, pathPrefixSet);
- } catch (ExecutionException e) {
- listeningFailed = true;
- logger.log(TreeLogger.WARN, "The attempt to retrieve accumulated file changes in " + dir
- + " failed. Will fall back on full directory scans.");
- return scanRecursiveDirectory(logger, pathPrefixSet);
- }
+ try {
+ return ResourceAccumulatorManager.getResources(this, pathPrefixSet);
+ } catch (IOException e) {
+ // Not using logger because there are plenty of places in the compiler that uses
+ // TreeLogger.NULL that causes this problem to be silently ignored.
+ System.err.println("The attempt to retrieve files in " + dir + " failed.");
+ e.printStackTrace();
+ return Maps.newLinkedHashMap();
}
}
- private void ensureListening(TreeLogger logger, PathPrefixSet pathPrefixSet) {
- if (!listeningFailed && !DirectoryPathPrefixChangeManager.isListening(this, pathPrefixSet)) {
- try {
- DirectoryPathPrefixChangeManager.ensureListening(this, pathPrefixSet);
- } catch (IOException e) {
- listeningFailed = true;
- logger.log(TreeLogger.WARN, "The attempt to start listening for file changes in " + dir
- + " failed. Will fall back on full directory scans.");
- }
- }
- }
-
- @VisibleForTesting
- File getDirectory() {
+ public File getDirectory() {
return dir;
}
- private synchronized Map<AbstractResource, ResourceResolution> scanChangedFiles(
- TreeLogger logger, PathPrefixSet pathPrefixSet) throws ExecutionException {
- // Get cached results.
- Map<AbstractResource, ResourceResolution> resolutionsByResource =
- resolutionsByResourcePerPathPrefixSet.get(pathPrefixSet);
-
- // Update cached results.
- Collection<File> changedFiles =
- DirectoryPathPrefixChangeManager.getAndClearChangedFiles(this, pathPrefixSet);
- for (File changedFile : changedFiles) {
- String changedRelativePath = Util.makeRelativePath(dir, changedFile);
- FileResource resource = FileResource.of(changedRelativePath, changedFile);
-
- if (!changedFile.exists()) {
- if (resolutionsByResource.containsKey(resource)) {
- resolutionsByResource.remove(resource);
- }
- continue;
- }
-
- ResourceResolution resourceResolution = pathPrefixSet.includesResource(changedRelativePath);
- if (resourceResolution != null) {
- Messages.INCLUDING_FILE.log(logger, changedRelativePath, null);
- resolutionsByResource.put(resource, resourceResolution);
- } else {
- Messages.EXCLUDING_FILE.log(logger, changedRelativePath, null);
- }
- }
- return resolutionsByResource;
- }
-
- private synchronized Map<AbstractResource, ResourceResolution> scanRecursiveDirectory(
- TreeLogger logger, PathPrefixSet pathPrefixSet) {
- Map<AbstractResource, ResourceResolution> resolutionsByResource = Maps.newHashMap();
- descendToFindResources(logger, Lists.create(pathPrefixSet), Lists.create(resolutionsByResource),
- dir, "");
-
- // Cache results.
- resolutionsByResourcePerPathPrefixSet.put(pathPrefixSet, resolutionsByResource);
-
- return resolutionsByResource;
- }
-
@Override
public String getLocation() {
return location;
}
-
- /**
- * @param logger logs progress
- * @param pathPrefixSets the sets of path prefixes to determine what resources
- * are included
- * @param results the accumulating sets of resources (each with the
- * corresponding pathPrefix) found
- * @param dir the file or directory to consider
- * @param dirPath the abstract path name associated with 'parent', which
- * explicitly does not include the classpath entry in its path
- */
- @VisibleForTesting
- void descendToFindResources(TreeLogger logger,
- List<PathPrefixSet> pathPrefixSets,
- List<Map<AbstractResource, ResourceResolution>> results, File dir, String dirPath) {
- assert (dir.isDirectory()) : dir + " is not a directory";
- int len = pathPrefixSets.size();
-
- // Assert: this directory is included in the path prefix set.
-
- File[] children = dir.listFiles();
- for (File child : children) {
- String childPath = dirPath + child.getName();
- if (child.isDirectory()) {
- if (child.isHidden()) {
- // Don't look inside of intentionally hidden directories. It's just a waste of time,
- // sometimes lots of time.
- continue;
- }
- String childDirPath = childPath + "/";
- for (int i = 0; i < len; ++i) {
- if (pathPrefixSets.get(i).includesDirectory(childDirPath)) {
- Messages.DESCENDING_INTO_DIR.log(logger, child.getPath(), null);
- descendToFindResources(logger, pathPrefixSets, results, child,
- childDirPath);
- break;
- }
- }
- } else if (child.isFile()) {
- for (int i = 0; i < len; ++i) {
- ResourceResolution resourceResolution = null;
- if ((resourceResolution = pathPrefixSets.get(i).includesResource(childPath)) != null) {
- Messages.INCLUDING_FILE.log(logger, childPath, null);
- FileResource r = FileResource.of(childPath, child);
- results.get(i).put(r, resourceResolution);
- } else {
- Messages.EXCLUDING_FILE.log(logger, childPath, null);
- }
- }
- }
- }
- }
}
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryPathPrefixChangeManager.java b/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryPathPrefixChangeManager.java
deleted file mode 100644
index 9bc05f5..0000000
--- a/dev/core/src/com/google/gwt/dev/resource/impl/DirectoryPathPrefixChangeManager.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright 2014 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.dev.resource.impl;
-
-import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
-import com.google.gwt.thirdparty.guava.common.collect.Maps;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Objects;
-import java.util.concurrent.ExecutionException;
-
-/**
- * Manages changed file accumulation for DirectoryClassPathEntry + PathPrefixSet pairs.
- * <p>
- * Changed file lists need to be collected and processed separately for each pair since the
- * processing depends on both the directory and the path prefix set.
- * <p>
- * ChangedFileAccumulators consume native resources and so require very strict lifecycle management
- * but ClassPathEntry and PathPrefixSet lifecycle management is very loose. This makes it difficult
- * to release ChangedFileAccumulators at the proper time. This manager class uses weak references to
- * ClassPathEntry and PathPrefixSet instances to lazily discover when ChangedFileAccumulators become
- * eligible for destruction.
- */
-class DirectoryPathPrefixChangeManager {
-
- /**
- * A hash key that is a combination of a DirectoryClassPathEntry and PathPrefixSet which also
- * takes special care not to block the garbage collection of either.
- */
- private static class DirectoryAndPathPrefix {
-
- private final WeakReference<DirectoryClassPathEntry> directoryClassPathEntryReference;
- private int hashCode;
- private final WeakReference<PathPrefixSet> pathPrefixSetReference;
-
- public DirectoryAndPathPrefix(DirectoryClassPathEntry directoryClassPathEntry,
- PathPrefixSet pathPrefixSet) {
- this.directoryClassPathEntryReference =
- new WeakReference<DirectoryClassPathEntry>(directoryClassPathEntry);
- this.pathPrefixSetReference = new WeakReference<PathPrefixSet>(pathPrefixSet);
-
- hashCode = Objects.hash(directoryClassPathEntry, pathPrefixSet);
- }
-
- @Override
- public boolean equals(Object object) {
- if (object instanceof DirectoryAndPathPrefix) {
- DirectoryAndPathPrefix directoryAndPathPrefix = (DirectoryAndPathPrefix) object;
- return Objects.equals(directoryClassPathEntryReference.get(),
- directoryAndPathPrefix.directoryClassPathEntryReference.get()) && Objects.equals(
- pathPrefixSetReference.get(), directoryAndPathPrefix.pathPrefixSetReference.get());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return hashCode;
- }
-
- /**
- * If either the instance has been destroyed then it is no longer possible for a caller to
- * request the accumulated changed files list for the combination. This means the combination is
- * old and tracking can be stopped.
- */
- public boolean isOld() {
- return directoryClassPathEntryReference.get() == null || pathPrefixSetReference.get() == null;
- }
- }
-
- private static Map<DirectoryAndPathPrefix, ChangedFileAccumulator>
- changedFileAccumulatorsByDirectoryAndPathPrefix = Maps.newHashMap();
-
- /**
- * Start a FileChangeAccumulator for the given DirectoryClassPathEntry + PathPrefixSet pair.
- */
- public static void ensureListening(DirectoryClassPathEntry directoryClassPathEntry,
- PathPrefixSet pathPrefixSet) throws IOException {
- clearOldListeners();
-
- DirectoryAndPathPrefix directoryAndPathPrefix =
- new DirectoryAndPathPrefix(directoryClassPathEntry, pathPrefixSet);
-
- synchronized (changedFileAccumulatorsByDirectoryAndPathPrefix) {
- if (changedFileAccumulatorsByDirectoryAndPathPrefix.containsKey(directoryAndPathPrefix)) {
- return;
- }
-
- changedFileAccumulatorsByDirectoryAndPathPrefix.put(directoryAndPathPrefix,
- new ChangedFileAccumulator(directoryClassPathEntry.getDirectory().toPath()));
- }
- }
-
- /**
- * Returns a sorted copy of the list of files that have changed since the last time changed files
- * were requested.
- */
- public static List<File> getAndClearChangedFiles(DirectoryClassPathEntry directoryClassPathEntry,
- PathPrefixSet pathPrefixSet) throws ExecutionException {
- ChangedFileAccumulator changedFileAccumulator = changedFileAccumulatorsByDirectoryAndPathPrefix
- .get(new DirectoryAndPathPrefix(directoryClassPathEntry, pathPrefixSet));
- assert changedFileAccumulator
- != null : "Listening must be started before changed files can be requested.";
- return changedFileAccumulator.getAndClearChangedFiles();
- }
-
- @VisibleForTesting
- static int getActiveListenerCount() {
- clearOldListeners();
-
- synchronized (changedFileAccumulatorsByDirectoryAndPathPrefix) {
- return changedFileAccumulatorsByDirectoryAndPathPrefix.size();
- }
- }
-
- @VisibleForTesting
- static boolean isListening(DirectoryClassPathEntry directoryClassPathEntry,
- PathPrefixSet pathPrefixSet) {
- synchronized (changedFileAccumulatorsByDirectoryAndPathPrefix) {
- return changedFileAccumulatorsByDirectoryAndPathPrefix.containsKey(
- new DirectoryAndPathPrefix(directoryClassPathEntry, pathPrefixSet));
- }
- }
-
- /**
- * Finds any DirectoryClassPathEntry and PathPrefixSet pairs where at least one instance has been
- * destroyed (and thus can no longer be queried about) and shuts down the associated changed file
- * accumulation.
- */
- private static void clearOldListeners() {
- synchronized (changedFileAccumulatorsByDirectoryAndPathPrefix) {
- Iterator<Entry<DirectoryAndPathPrefix, ChangedFileAccumulator>> entriesIterator =
- changedFileAccumulatorsByDirectoryAndPathPrefix.entrySet().iterator();
- while (entriesIterator.hasNext()) {
- Entry<DirectoryAndPathPrefix, ChangedFileAccumulator> entry = entriesIterator.next();
- DirectoryAndPathPrefix directoryAndPathPrefix = entry.getKey();
- ChangedFileAccumulator fileChangeAccumulator = entry.getValue();
- if (directoryAndPathPrefix.isOld()) {
- fileChangeAccumulator.shutdown();
- entriesIterator.remove();
- }
- }
- }
- }
-}
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java b/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java
index 3a16a16..f412f08 100644
--- a/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/PathPrefixSet.java
@@ -324,9 +324,7 @@
private void assertValidAbstractDirectoryPathName(String name) {
assert (name != null);
- // assert ("".equals(name) || (!name.startsWith("/") &&
- // name.endsWith("/")));
- assert (!name.startsWith("/") && name.endsWith("/"));
+ assert (!name.startsWith("/"));
}
private void assertValidAbstractResourcePathName(String name) {
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/ResourceAccumulator.java b/dev/core/src/com/google/gwt/dev/resource/impl/ResourceAccumulator.java
new file mode 100644
index 0000000..9e5dc90
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/ResourceAccumulator.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.resource.impl;
+
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
+
+import com.google.gwt.thirdparty.guava.common.collect.ArrayListMultimap;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
+import com.google.gwt.thirdparty.guava.common.collect.Multimap;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Map;
+
+/**
+ * Listens for and accumulates resources for a given root and PathPrefixSet.
+ */
+class ResourceAccumulator {
+
+ private static final boolean WATCH_FILE_CHANGES_DEFAULT = Boolean.parseBoolean(
+ System.getProperty("gwt.watchFileChanges", "true"));
+
+ private Map<AbstractResource, ResourceResolution> resolutionsByResource;
+ private Multimap<Path, Path> childPathsByParentPath;
+ private Path rootDirectory;
+ private WeakReference<PathPrefixSet> pathPrefixSetRef;
+ private WatchService watchService;
+ private boolean watchFileChanges = WATCH_FILE_CHANGES_DEFAULT;
+
+ public ResourceAccumulator(Path rootDirectory, PathPrefixSet pathPrefixSet) {
+ this.rootDirectory = rootDirectory;
+ this.pathPrefixSetRef = new WeakReference<PathPrefixSet>(pathPrefixSet);
+ }
+
+ public boolean isWatchServiceActive() {
+ return watchService != null;
+ }
+
+ /**
+ * Make sure the resources associated with this directory and pathPrefixSet are up-to-date.
+ */
+ public void refreshResources() throws IOException {
+ if (isWatchServiceActive()) {
+ refresh();
+ } else {
+ fullRefresh();
+ }
+ }
+
+ public Map<AbstractResource, ResourceResolution> getResources() {
+ return resolutionsByResource;
+ }
+
+ public void shutdown() throws IOException {
+ // watchService field is not cleared so any attempt to use this class after shutdown will fail.
+ stopWatchService();
+ }
+
+ /**
+ * Full refresh clears existing resources and watchers and does a clean refresh.
+ */
+ private void fullRefresh() throws IOException {
+ resolutionsByResource = Maps.newIdentityHashMap();
+ childPathsByParentPath = ArrayListMultimap.create();
+
+ maybeInitializeWatchService();
+
+ onNewDirectory(rootDirectory);
+ }
+
+ private void maybeInitializeWatchService() throws IOException {
+ if (watchFileChanges) {
+ stopWatchService();
+ try {
+ watchService = FileSystems.getDefault().newWatchService();
+ } catch (IOException e) {
+ watchFileChanges = false;
+ }
+ }
+ }
+
+ private void stopWatchService() throws IOException {
+ if (watchService != null) {
+ watchService.close();
+ }
+ }
+
+ private void refresh() throws IOException {
+ while (true) {
+ WatchKey watchKey = watchService.poll();
+ if (watchKey == null) {
+ return;
+ }
+
+ Path parentDir = (Path) watchKey.watchable();
+
+ for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
+ WatchEvent.Kind<?> eventKind = watchEvent.kind();
+ if (eventKind == OVERFLOW) {
+ fullRefresh();
+ return;
+ }
+
+ Path child = parentDir.resolve((Path) watchEvent.context());
+ if (eventKind == ENTRY_CREATE) {
+ onNewPath(child);
+ } else if (eventKind == ENTRY_DELETE) {
+ onRemovedPath(child);
+ }
+ }
+
+ watchKey.reset();
+ }
+ }
+
+ private void onNewPath(Path path) throws IOException {
+ if (Files.isHidden(path)) {
+ return;
+ }
+
+ if (Files.isRegularFile(path)) {
+ onNewFile(path);
+ } else {
+ onNewDirectory(path);
+ }
+ }
+
+ private void onNewDirectory(Path directory) throws IOException {
+ String relativePath = getRelativePath(directory);
+ if (!relativePath.isEmpty() && !getPathPrefixSet().includesDirectory(relativePath)) {
+ return;
+ }
+
+ if (watchService != null) {
+ // Start watching the directory.
+ directory.register(watchService, ENTRY_CREATE, ENTRY_DELETE);
+ }
+
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
+ for (Path child : stream) {
+ childPathsByParentPath.put(directory, child);
+ onNewPath(child);
+ }
+ }
+ }
+
+ private void onNewFile(Path file) {
+ FileResource resource = toFileResource(file);
+ ResourceResolution resourceResolution = getPathPrefixSet().includesResource(resource.getPath());
+ if (resourceResolution != null) {
+ resolutionsByResource.put(resource, resourceResolution);
+ }
+ }
+
+ private void onRemovedPath(Path path) {
+ resolutionsByResource.remove(toFileResource(path));
+ for (Path child : childPathsByParentPath.get(path)) {
+ onRemovedPath(child);
+ }
+ }
+
+ private FileResource toFileResource(Path path) {
+ String relativePath = getRelativePath(path);
+ return FileResource.of(relativePath, path.toFile());
+ }
+
+ private String getRelativePath(Path directory) {
+ return rootDirectory.relativize(directory).toString();
+ }
+
+ private PathPrefixSet getPathPrefixSet() {
+ PathPrefixSet pathPrefixSet = pathPrefixSetRef.get();
+ // pathPrefixSet can never be null as the life span of this class is bound by it.
+ assert pathPrefixSet != null;
+ return pathPrefixSet;
+ }
+}
diff --git a/dev/core/src/com/google/gwt/dev/resource/impl/ResourceAccumulatorManager.java b/dev/core/src/com/google/gwt/dev/resource/impl/ResourceAccumulatorManager.java
new file mode 100644
index 0000000..b4fa48c
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/resource/impl/ResourceAccumulatorManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.resource.impl;
+
+import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
+import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+
+/**
+ * Manages {@link ResourceAccumulator}s for DirectoryClassPathEntry + PathPrefixSet pairs.
+ * <p>
+ * ResourceAccumulators consume native resources and so require very strict lifecycle management but
+ * ClassPathEntry and PathPrefixSet lifecycle management is very loose. This makes it difficult to
+ * release ResourceAccumulator at the proper time. This manager class uses weak references to
+ * ClassPathEntry and PathPrefixSet instances to lazily discover when ResourceAccumulator instances
+ * become eligible for destruction.
+ */
+class ResourceAccumulatorManager {
+
+ /**
+ * A hash key that is a combination of a DirectoryClassPathEntry and PathPrefixSet which also
+ * takes special care not to block the garbage collection of either.
+ */
+ private static class DirectoryAndPathPrefix {
+
+ private final WeakReference<DirectoryClassPathEntry> directoryClassPathEntryRef;
+ private final WeakReference<PathPrefixSet> pathPrefixSetRef;
+ private int hashCode;
+
+ public DirectoryAndPathPrefix(DirectoryClassPathEntry directoryClassPathEntry,
+ PathPrefixSet pathPrefixSet) {
+ this.directoryClassPathEntryRef = new WeakReference<>(directoryClassPathEntry);
+ this.pathPrefixSetRef = new WeakReference<PathPrefixSet>(pathPrefixSet);
+ hashCode = Objects.hash(directoryClassPathEntry, pathPrefixSet);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof DirectoryAndPathPrefix) {
+ DirectoryAndPathPrefix other = (DirectoryAndPathPrefix) object;
+ return directoryClassPathEntryRef.get() == other.directoryClassPathEntryRef.get()
+ && pathPrefixSetRef.get() == other.pathPrefixSetRef.get();
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ /**
+ * If either the instance has been destroyed then it is no longer possible for a caller to
+ * request the accumulated sources for the combination. This means the combination is
+ * old and tracking can be stopped.
+ */
+ public boolean isOld() {
+ return directoryClassPathEntryRef.get() == null || pathPrefixSetRef.get() == null;
+ }
+ }
+
+ private static Map<DirectoryAndPathPrefix, ResourceAccumulator> resourceAccumulators = Maps
+ .newHashMap();
+
+ static {
+ // Keep the resources fresh
+ new Thread() {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ refreshResources();
+ Thread.sleep(10);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }.start();
+ }
+
+ public static synchronized Map<AbstractResource, ResourceResolution> getResources(
+ DirectoryClassPathEntry directoryClassPathEntry, PathPrefixSet pathPrefixSet)
+ throws IOException {
+ DirectoryAndPathPrefix directoryAndPathPrefix =
+ new DirectoryAndPathPrefix(directoryClassPathEntry, pathPrefixSet);
+
+ ResourceAccumulator resourceAccumulator = resourceAccumulators.get(directoryAndPathPrefix);
+ if (resourceAccumulator == null) {
+ Path path = directoryClassPathEntry.getDirectory().toPath();
+ resourceAccumulator = new ResourceAccumulator(path, pathPrefixSet);
+ resourceAccumulators.put(directoryAndPathPrefix, resourceAccumulator);
+ }
+ resourceAccumulator.refreshResources();
+ return ImmutableMap.copyOf(resourceAccumulator.getResources());
+ }
+
+ public static synchronized void refreshResources() throws IOException {
+ Iterator<Entry<DirectoryAndPathPrefix, ResourceAccumulator>> entriesIterator =
+ resourceAccumulators.entrySet().iterator();
+ while (entriesIterator.hasNext()) {
+ Entry<DirectoryAndPathPrefix, ResourceAccumulator> entry = entriesIterator.next();
+ DirectoryAndPathPrefix directoryAndPathPrefix = entry.getKey();
+ ResourceAccumulator resourceAccumulator = entry.getValue();
+ if (directoryAndPathPrefix.isOld()) {
+ resourceAccumulator.shutdown();
+ entriesIterator.remove();
+ } else if (resourceAccumulator.isWatchServiceActive()) {
+ resourceAccumulator.refreshResources();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static int getActiveListenerCount() throws IOException {
+ refreshResources();
+
+ return resourceAccumulators.size();
+ }
+
+ @VisibleForTesting
+ static boolean isListening(DirectoryClassPathEntry directoryClassPathEntry,
+ PathPrefixSet pathPrefixSet) {
+ return resourceAccumulators.containsKey(
+ new DirectoryAndPathPrefix(directoryClassPathEntry, pathPrefixSet));
+ }
+}
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/ChangedFileAccumulatorTest.java b/dev/core/test/com/google/gwt/dev/resource/impl/ChangedFileAccumulatorTest.java
deleted file mode 100644
index 2c395c6..0000000
--- a/dev/core/test/com/google/gwt/dev/resource/impl/ChangedFileAccumulatorTest.java
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
- * Copyright 2014 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.google.gwt.dev.resource.impl;
-
-import com.google.gwt.thirdparty.guava.common.io.Files;
-
-import junit.framework.TestCase;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Tests for ChangedFileAccumulator.
- */
-public class ChangedFileAccumulatorTest extends TestCase {
-
- public void testAddFile() throws Exception {
- File rootDirectory = Files.createTempDir();
- File subDirectory = createDirectoryIn("subdir", rootDirectory);
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- createFileIn("New.java", subDirectory);
- waitForFileEvents();
-
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(1, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("New.java"));
-
- changedFileAccumulator.shutdown();
- }
-
- public void testDeleteFile() throws Exception {
- File rootDirectory = Files.createTempDir();
- File subDirectory = createDirectoryIn("subdir", rootDirectory);
- File originalFile = createFileIn("SomeFile.java", subDirectory);
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- originalFile.delete();
- waitForFileEvents();
-
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(1, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("SomeFile.java"));
-
- changedFileAccumulator.shutdown();
- }
-
- public void testListensInNewDirectories() throws Exception {
- File rootDirectory = Files.createTempDir();
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- // Create a new directory and contained file AFTER the root directory has started being listened
- // to.
- File subDirectory = createDirectoryIn("subdir", rootDirectory);
- createFileIn("New.java", subDirectory);
- waitForFileEvents();
-
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(2, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("subdir"));
- assertTrue(modifiedFiles.get(1).getPath().endsWith("New.java"));
-
- changedFileAccumulator.shutdown();
- }
-
- public void testModifyRepeatedly() throws Exception {
- File rootDirectory = Files.createTempDir();
- File fooFile = createFileIn("Foo.java", rootDirectory);
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- for (int i = 0; i < 5; i++) {
- fooFile.setLastModified(i * 1000);
- waitForFileEvents();
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(1, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("Foo.java"));
- }
-
- changedFileAccumulator.shutdown();
- }
-
- public void testMultipleListeners() throws Exception {
- File rootDirectory = Files.createTempDir();
- File subDirectory = createDirectoryIn("subdir", rootDirectory);
-
- ChangedFileAccumulator changedFileAccumulator1 =
- new ChangedFileAccumulator(rootDirectory.toPath());
- ChangedFileAccumulator changedFileAccumulator2 =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator1.getAndClearChangedFiles().isEmpty());
- assertTrue(changedFileAccumulator2.getAndClearChangedFiles().isEmpty());
-
- createFileIn("New.java", subDirectory);
- waitForFileEvents();
-
- List<File> modifiedFiles1 = changedFileAccumulator1.getAndClearChangedFiles();
- assertEquals(1, modifiedFiles1.size());
- assertTrue(modifiedFiles1.get(0).getPath().endsWith("New.java"));
-
- List<File> modifiedFiles2 = changedFileAccumulator2.getAndClearChangedFiles();
- assertEquals(1, modifiedFiles2.size());
- assertTrue(modifiedFiles2.get(0).getPath().endsWith("New.java"));
-
- changedFileAccumulator1.shutdown();
- changedFileAccumulator2.shutdown();
- }
-
- public void testRenameFile() throws Exception {
- File rootDirectory = Files.createTempDir();
- File subDirectory = createDirectoryIn("subdir", rootDirectory);
- File originalFile = createFileIn("OriginalName.java", subDirectory);
- File renamedFile = new File(subDirectory, "Renamed.java");
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- originalFile.renameTo(renamedFile);
- waitForFileEvents();
-
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(2, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("OriginalName.java"));
- assertTrue(modifiedFiles.get(1).getPath().endsWith("Renamed.java"));
-
- changedFileAccumulator.shutdown();
- }
-
- public void testRenameDirectory() throws Exception {
- File rootDirectory = Files.createTempDir();
- File subDirectory = createDirectoryIn("original_dir", rootDirectory);
- createFileIn("Name1.java", subDirectory);
- createFileIn("Name2.java", subDirectory);
- File renamedSubDirectory = new File(rootDirectory, "new_dir");
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- subDirectory.renameTo(renamedSubDirectory);
- waitForFileEvents();
-
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(6, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("new_dir"));
- assertTrue(modifiedFiles.get(1).getPath().endsWith("new_dir/Name1.java"));
- assertTrue(modifiedFiles.get(2).getPath().endsWith("new_dir/Name2.java"));
- assertTrue(modifiedFiles.get(3).getPath().endsWith("original_dir"));
- assertTrue(modifiedFiles.get(4).getPath().endsWith("original_dir/Name1.java"));
- assertTrue(modifiedFiles.get(5).getPath().endsWith("original_dir/Name2.java"));
-
- changedFileAccumulator.shutdown();
- }
-
- public void testRenameParentDirectory() throws Exception {
- File rootDirectory = Files.createTempDir();
- File parentDirectory = createDirectoryIn("original_dir", rootDirectory);
- File subDirectory = createDirectoryIn("subdir", parentDirectory);
- createFileIn("Name1.java", subDirectory);
- createFileIn("Name2.java", subDirectory);
- File renamedParentDirectory = new File(rootDirectory, "new_dir");
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- parentDirectory.renameTo(renamedParentDirectory);
- waitForFileEvents();
-
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(8, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("new_dir"));
- assertTrue(modifiedFiles.get(1).getPath().endsWith("new_dir/subdir"));
- assertTrue(modifiedFiles.get(2).getPath().endsWith("new_dir/subdir/Name1.java"));
- assertTrue(modifiedFiles.get(3).getPath().endsWith("new_dir/subdir/Name2.java"));
- assertTrue(modifiedFiles.get(4).getPath().endsWith("original_dir"));
- assertTrue(modifiedFiles.get(5).getPath().endsWith("original_dir/subdir"));
- assertTrue(modifiedFiles.get(6).getPath().endsWith("original_dir/subdir/Name1.java"));
- assertTrue(modifiedFiles.get(7).getPath().endsWith("original_dir/subdir/Name2.java"));
-
- changedFileAccumulator.shutdown();
- }
-
- public void testSymlinkInfiniteLoop() throws Exception {
- File rootDirectory = Files.createTempDir();
- File subDirectory = Files.createTempDir();
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- // Symlink in a loop
- java.nio.file.Files.createSymbolicLink(new File(rootDirectory, "sublink").toPath(),
- subDirectory.toPath()).toFile();
- java.nio.file.Files.createSymbolicLink(new File(subDirectory, "sublink").toPath(),
- rootDirectory.toPath()).toFile();
- createFileIn("New.java", subDirectory);
- waitForFileEvents();
-
- // Will throw an error if ChangedFileAccumulator got stuck in an infinite directory scan loop.
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
- assertEquals(3, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("sublink"));
- assertEquals(modifiedFiles.get(1).getPath(), subDirectory.getPath());
- assertTrue(modifiedFiles.get(2).getPath().endsWith("New.java"));
-
- changedFileAccumulator.shutdown();
- }
-
- public void testSymlinks() throws Exception {
- File scratchDirectory = Files.createTempDir();
- File newFile = createFileIn("New.java", scratchDirectory);
- File rootDirectory = Files.createTempDir();
- File subDirectory = Files.createTempDir();
-
- ChangedFileAccumulator changedFileAccumulator =
- new ChangedFileAccumulator(rootDirectory.toPath());
-
- assertTrue(changedFileAccumulator.getAndClearChangedFiles().isEmpty());
-
- // Symlink in a subdirectory and then symlink in a contained file.
- java.nio.file.Files.createSymbolicLink(new File(rootDirectory, "sublink").toPath(),
- subDirectory.toPath()).toFile();
- java.nio.file.Files.createSymbolicLink(new File(subDirectory, "New.java").toPath(),
- newFile.toPath()).toFile();
- waitForFileEvents();
-
- List<File> modifiedFiles = changedFileAccumulator.getAndClearChangedFiles();
-
- assertEquals(3, modifiedFiles.size());
- assertTrue(modifiedFiles.get(0).getPath().endsWith("New.java"));
- assertTrue(modifiedFiles.get(1).getPath().endsWith("sublink"));
- assertEquals(modifiedFiles.get(2).getPath(), subDirectory.getPath());
-
- changedFileAccumulator.shutdown();
- }
-
- private static File createDirectoryIn(String fileName, File inDirectory) {
- File newDirectory = new File(inDirectory, fileName);
- newDirectory.mkdir();
- return newDirectory;
- }
-
- private static File createFileIn(String fileName, File inDirectory) throws IOException {
- File newFile = new File(inDirectory, fileName);
- newFile.createNewFile();
- return newFile;
- }
-
- private void waitForFileEvents() throws InterruptedException {
- Thread.sleep(100);
- }
-}
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java b/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java
index eef4de3..3843f23 100644
--- a/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/ClassPathEntryTest.java
@@ -17,7 +17,6 @@
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
-import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.io.Files;
import java.io.File;
@@ -25,7 +24,6 @@
import java.lang.ref.WeakReference;
import java.net.URISyntaxException;
import java.util.Collection;
-import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -47,19 +45,14 @@
File javaFile = new File(nestedHiddenDir, "ShouldNotBeFound.java");
javaFile.createNewFile();
- // Prepare a place to record findings.
- List<Map<AbstractResource, ResourceResolution>> foundFiles =
- Lists.<Map<AbstractResource, ResourceResolution>> newArrayList();
- foundFiles.add(Maps.<AbstractResource, ResourceResolution> newHashMap());
-
// Perform a class path directory inspection.
DirectoryClassPathEntry cpe = new DirectoryClassPathEntry(tempDir);
- cpe.descendToFindResources(TreeLogger.NULL, Lists.newArrayList(createInclusivePathPrefixSet()),
- foundFiles, tempDir, "");
+ Map<AbstractResource, ResourceResolution> resources =
+ cpe.findApplicableResources(TreeLogger.NULL, createInclusivePathPrefixSet());
// Verify that even though we're using an ALL filter, we still didn't find any files inside the
// .svn dir, because we never even enumerate its contents.
- assertTrue(foundFiles.get(0).isEmpty());
+ assertTrue(resources.isEmpty());
}
public void testResourceCreated() throws IOException, InterruptedException {
@@ -70,30 +63,60 @@
createInclusivePathPrefixSet(), createInclusivePathPrefixSet()));
}
- public void testForResourceListenerLeaks() throws IOException, InterruptedException {
+ public void testForResourceListenerLeaks_pathPrefixSetIsCollected() throws Exception {
// Create a folder an initially empty folder.
PathPrefixSet pathPrefixSet = createInclusivePathPrefixSet();
DirectoryClassPathEntry classPathEntry = new DirectoryClassPathEntry(Files.createTempDir());
- // Show that the WeakDirectoryNotifier is not listening for any updates.
+ // Show that we are not listening.
awaitFullGc();
- assertEquals(0, DirectoryPathPrefixChangeManager.getActiveListenerCount());
+ assertEquals(0, ResourceAccumulatorManager.getActiveListenerCount());
// Start listening for updates.
- DirectoryPathPrefixChangeManager.ensureListening(classPathEntry, pathPrefixSet);
+ ResourceAccumulatorManager.getResources(classPathEntry, pathPrefixSet);
- // Show that the WeakDirectoryNotifier is now listening for updates.
+ // Show that we are now listening for updates.
awaitFullGc();
- assertEquals(1, DirectoryPathPrefixChangeManager.getActiveListenerCount());
+ assertEquals(1, ResourceAccumulatorManager.getActiveListenerCount());
- // Dereference the classpath entry and pathprefixset and give the garbage collector an
- // opportunity to clear any weak references.
+ // Dereference the pathPrefixSet to give garbage collector an opportunity to clear any weak
+ // references.
pathPrefixSet = null;
+
+ // Show that we are no longer listening for updates.
+ awaitFullGc();
+ assertEquals(0, ResourceAccumulatorManager.getActiveListenerCount());
+
+ // Make sure classPathEntry is not GC'd until this point.
+ assertNotNull(classPathEntry);
+ }
+
+ public void testForResourceListenerLeaks_classPathEntryIsCollected() throws Exception {
+ // Create a folder an initially empty folder.
+ PathPrefixSet pathPrefixSet = createInclusivePathPrefixSet();
+ DirectoryClassPathEntry classPathEntry = new DirectoryClassPathEntry(Files.createTempDir());
+
+ // Show that we are not listening.
+ awaitFullGc();
+ assertEquals(0, ResourceAccumulatorManager.getActiveListenerCount());
+
+ // Start listening for updates.
+ ResourceAccumulatorManager.getResources(classPathEntry, pathPrefixSet);
+
+ // Show that we are now listening for updates.
+ awaitFullGc();
+ assertEquals(1, ResourceAccumulatorManager.getActiveListenerCount());
+
+ // Dereference the classPathEntry to give the garbage collector an opportunity to clear any weak
+ // references.
classPathEntry = null;
- // Show that the WeakDirectoryNotifier is no longer listening for updates.
+ // Show that we are no longer listening for updates.
awaitFullGc();
- assertEquals(0, DirectoryPathPrefixChangeManager.getActiveListenerCount());
+ assertEquals(0, ResourceAccumulatorManager.getActiveListenerCount());
+
+ // Make sure pathPrefixSet is not GC'd until this point.
+ assertNotNull(pathPrefixSet);
}
public void testResourceCreated(Collection<PathPrefixSet> pathPrefixSets) throws IOException,
diff --git a/dev/core/test/com/google/gwt/dev/resource/impl/ResourceAccumulatorTest.java b/dev/core/test/com/google/gwt/dev/resource/impl/ResourceAccumulatorTest.java
new file mode 100644
index 0000000..545f513
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/resource/impl/ResourceAccumulatorTest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.dev.resource.impl;
+
+import com.google.gwt.thirdparty.guava.common.collect.Lists;
+import com.google.gwt.thirdparty.guava.common.io.Files;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystemException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Tests for ResourceAccumulator.
+ */
+public class ResourceAccumulatorTest extends TestCase {
+
+ public void testAddFile() throws Exception {
+ File rootDirectory = Files.createTempDir();
+ File subDirectory = createDirectoryIn("subdir", rootDirectory);
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ assertTrue(getResources(resourceAccumulator).isEmpty());
+
+ createFileIn("New.java", subDirectory);
+ waitForFileEvents();
+
+ List<AbstractResource> resources = getResources(resourceAccumulator);
+ assertEquals(1, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("New.java"));
+
+ resourceAccumulator.shutdown();
+ }
+
+ public void testDeleteFile() throws Exception {
+ File rootDirectory = Files.createTempDir();
+ File subDirectory = createDirectoryIn("subdir", rootDirectory);
+ File originalFile = createFileIn("SomeFile.java", subDirectory);
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ List<AbstractResource> resources = getResources(resourceAccumulator);
+ assertEquals(1, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("SomeFile.java"));
+
+ originalFile.delete();
+ waitForFileEvents();
+
+ assertTrue(getResources(resourceAccumulator).isEmpty());
+
+ resourceAccumulator.shutdown();
+ }
+
+ public void testListensInNewDirectories() throws Exception {
+ File rootDirectory = Files.createTempDir();
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ assertTrue(getResources(resourceAccumulator).isEmpty());
+
+ // Create a new directory and contained file AFTER the root directory has started being listened
+ // to.
+ File subDirectory = createDirectoryIn("subdir", rootDirectory);
+ createFileIn("New.java", subDirectory);
+ waitForFileEvents();
+
+ List<AbstractResource> resources = getResources(resourceAccumulator);
+ assertEquals(1, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("New.java"));
+
+ resourceAccumulator.shutdown();
+ }
+
+ public void testMultipleListeners() throws Exception {
+ File rootDirectory = Files.createTempDir();
+ File subDirectory = createDirectoryIn("subdir", rootDirectory);
+
+ ResourceAccumulator resourceAccumulator1 =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+ ResourceAccumulator resourceAccumulator2 =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ assertTrue(getResources(resourceAccumulator1).isEmpty());
+ assertTrue(getResources(resourceAccumulator2).isEmpty());
+
+ createFileIn("New.java", subDirectory);
+ waitForFileEvents();
+
+ List<AbstractResource> resources1 = getResources(resourceAccumulator1);
+ assertEquals(1, resources1.size());
+ assertTrue(resources1.get(0).getPath().endsWith("New.java"));
+
+ List<AbstractResource> resources2 = getResources(resourceAccumulator2);
+ assertEquals(1, resources2.size());
+ assertTrue(resources2.get(0).getPath().endsWith("New.java"));
+
+ resourceAccumulator1.shutdown();
+ resourceAccumulator2.shutdown();
+ }
+
+ public void testRenameFile() throws Exception {
+ File rootDirectory = Files.createTempDir();
+ File subDirectory = createDirectoryIn("subdir", rootDirectory);
+ File originalFile = createFileIn("OriginalName.java", subDirectory);
+ File renamedFile = new File(subDirectory, "Renamed.java");
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ List<AbstractResource> resources = getResources(resourceAccumulator);
+ assertEquals(1, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("OriginalName.java"));
+
+ originalFile.renameTo(renamedFile);
+ waitForFileEvents();
+
+ resources = getResources(resourceAccumulator);
+ assertEquals(1, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("Renamed.java"));
+
+ resourceAccumulator.shutdown();
+ }
+
+ public void testRenameDirectory() throws Exception {
+ File rootDirectory = Files.createTempDir();
+ File subDirectory = createDirectoryIn("original_dir", rootDirectory);
+ createFileIn("Name1.java", subDirectory);
+ createFileIn("Name2.java", subDirectory);
+ File renamedSubDirectory = new File(rootDirectory, "new_dir");
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ List<AbstractResource> resources = getResources(resourceAccumulator);
+ assertEquals(2, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("original_dir/Name1.java"));
+ assertTrue(resources.get(1).getPath().endsWith("original_dir/Name2.java"));
+
+ subDirectory.renameTo(renamedSubDirectory);
+ waitForFileEvents();
+
+ resources = getResources(resourceAccumulator);
+ assertEquals(2, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("new_dir/Name1.java"));
+ assertTrue(resources.get(1).getPath().endsWith("new_dir/Name2.java"));
+
+ resourceAccumulator.shutdown();
+ }
+
+ public void testRenameParentDirectory() throws Exception {
+ File rootDirectory = Files.createTempDir();
+ File parentDirectory = createDirectoryIn("original_dir", rootDirectory);
+ File subDirectory = createDirectoryIn("subdir", parentDirectory);
+ createFileIn("Name1.java", subDirectory);
+ createFileIn("Name2.java", subDirectory);
+ File renamedParentDirectory = new File(rootDirectory, "new_dir");
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ List<AbstractResource> resources = getResources(resourceAccumulator);
+ assertEquals(2, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("original_dir/subdir/Name1.java"));
+ assertTrue(resources.get(1).getPath().endsWith("original_dir/subdir/Name2.java"));
+
+ parentDirectory.renameTo(renamedParentDirectory);
+ waitForFileEvents();
+
+ resources = getResources(resourceAccumulator);
+ assertEquals(2, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("new_dir/subdir/Name1.java"));
+ assertTrue(resources.get(1).getPath().endsWith("new_dir/subdir/Name2.java"));
+
+ resourceAccumulator.shutdown();
+ }
+
+ public void testSymlinkInfiniteLoop() throws Exception {
+ File rootDirectory = Files.createTempDir();
+ File subDirectory = Files.createTempDir();
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ assertTrue(getResources(resourceAccumulator).isEmpty());
+
+ // Symlink in a loop
+ java.nio.file.Files.createSymbolicLink(new File(rootDirectory, "sublink").toPath(),
+ subDirectory.toPath()).toFile();
+ java.nio.file.Files.createSymbolicLink(new File(subDirectory, "sublink").toPath(),
+ rootDirectory.toPath()).toFile();
+ createFileIn("New.java", subDirectory);
+ waitForFileEvents();
+
+ try {
+ // Should throw an error if resourceAccumulator got stuck in an infinite directory scan loop.
+ getResources(resourceAccumulator);
+ fail();
+ } catch (FileSystemException expected) {
+ // Expected
+ }
+
+ resourceAccumulator.shutdown();
+ }
+
+ public void testSymlinks() throws Exception {
+ File scratchDirectory = Files.createTempDir();
+ File newFile = createFileIn("New.java", scratchDirectory);
+ File rootDirectory = Files.createTempDir();
+ File subDirectory = Files.createTempDir();
+
+ ResourceAccumulator resourceAccumulator =
+ new ResourceAccumulator(rootDirectory.toPath(), createInclusivePathPrefixSet());
+
+ assertTrue(getResources(resourceAccumulator).isEmpty());
+
+ // Symlink in a subdirectory and then symlink in a contained file.
+ java.nio.file.Files.createSymbolicLink(new File(rootDirectory, "sublink").toPath(),
+ subDirectory.toPath()).toFile();
+ java.nio.file.Files.createSymbolicLink(new File(subDirectory, "New.java").toPath(),
+ newFile.toPath()).toFile();
+ waitForFileEvents();
+
+ List<AbstractResource> resources = getResources(resourceAccumulator);
+ assertEquals(1, resources.size());
+ assertTrue(resources.get(0).getPath().endsWith("sublink/New.java"));
+
+ resourceAccumulator.shutdown();
+ }
+
+ private static File createDirectoryIn(String fileName, File inDirectory) {
+ File newDirectory = new File(inDirectory, fileName);
+ newDirectory.mkdir();
+ return newDirectory;
+ }
+
+ private static File createFileIn(String fileName, File inDirectory) throws IOException {
+ File newFile = new File(inDirectory, fileName);
+ newFile.createNewFile();
+ return newFile;
+ }
+
+ private List<AbstractResource> getResources(ResourceAccumulator resourceAccumulator)
+ throws IOException {
+ resourceAccumulator.refreshResources();
+ List<AbstractResource> list = Lists.newArrayList(resourceAccumulator.getResources().keySet());
+ Collections.sort(list, new Comparator<AbstractResource>() {
+ @Override
+ public int compare(AbstractResource a, AbstractResource b) {
+ return a.getLocation().compareTo(b.getLocation());
+ }
+ });
+ return list;
+ }
+
+ private static PathPrefixSet createInclusivePathPrefixSet() {
+ PathPrefixSet pathPrefixes = new PathPrefixSet();
+ pathPrefixes.add(new PathPrefix("", null));
+ return pathPrefixes;
+ }
+
+ private void waitForFileEvents() throws InterruptedException {
+ Thread.sleep(100);
+ }
+}