[FELIX-2436] Reduce the number files created by the fileinstall Scanner
Use NIO2 Path watcher, even if we still use a polling mechanism, file watching are delegated to the JVM/OS
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1583362 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/DirectoryWatcher.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/DirectoryWatcher.java
index 4b3a2b4..5000904 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/DirectoryWatcher.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/DirectoryWatcher.java
@@ -21,7 +21,6 @@
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
@@ -42,7 +41,6 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
-import java.util.regex.Pattern;
import org.apache.felix.fileinstall.ArtifactInstaller;
import org.apache.felix.fileinstall.ArtifactListener;
@@ -106,6 +104,7 @@
public final static String UPDATE_WITH_LISTENERS = "felix.fileinstall.bundles.updateWithListeners";
public final static String OPTIONAL_SCOPE = "felix.fileinstall.optionalImportRefreshScope";
public final static String FRAGMENT_SCOPE = "felix.fileinstall.fragmentRefreshScope";
+ public final static String DISABLE_NIO2 = "felix.fileinstall.disableNio2";
public final static String SCOPE_NONE = "none";
public final static String SCOPE_MANAGED = "managed";
@@ -132,6 +131,7 @@
boolean updateWithListeners;
String fragmentScope;
String optionalScope;
+ boolean disableNio2;
// Map of all installed artifacts
final Map<File, Artifact> currentManagedArtifacts = new HashMap<File, Artifact>();
@@ -174,25 +174,18 @@
updateWithListeners = getBoolean(properties, UPDATE_WITH_LISTENERS, false); // Do not update bundles when listeners are updated
fragmentScope = properties.get(FRAGMENT_SCOPE);
optionalScope = properties.get(OPTIONAL_SCOPE);
+ disableNio2 = getBoolean(properties, DISABLE_NIO2, false);
this.context.addBundleListener(this);
- FilenameFilter flt;
- if (filter != null && filter.length() > 0)
- {
- flt = new FilenameFilter()
- {
- Pattern pattern = Pattern.compile(filter);
- public boolean accept(File dir, String name)
- {
- return pattern.matcher(name).matches();
- }
- };
+ if (disableNio2) {
+ scanner = new Scanner(watchedDirectory, filter);
+ } else {
+ try {
+ scanner = new WatcherScanner(context, watchedDirectory, filter);
+ } catch (Throwable t) {
+ scanner = new Scanner(watchedDirectory, filter);
+ }
}
- else
- {
- flt = null;
- }
- scanner = new Scanner(watchedDirectory, flt);
}
private void verifyWatchedDir()
@@ -769,6 +762,14 @@
}
try
{
+ scanner.close();
+ }
+ catch (IOException e)
+ {
+ // Ignore
+ }
+ try
+ {
join(10000);
}
catch (InterruptedException ie)
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Scanner.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Scanner.java
index d6fa652..985c2ba 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Scanner.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Scanner.java
@@ -18,6 +18,7 @@
*/
package org.apache.felix.fileinstall.internal;
+import java.io.Closeable;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
@@ -25,6 +26,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
import java.util.zip.CRC32;
/**
@@ -40,7 +42,7 @@
* the change on this file. This allows to not report the change until
* a big copy if complete for example.
*/
-public class Scanner {
+public class Scanner implements Closeable {
final File directory;
final FilenameFilter filter;
@@ -63,12 +65,26 @@
* Create a scanner for the specified directory and file filter
*
* @param directory the directory to scan
- * @param filter a filter for file names
+ * @param filterString a filter for file names
*/
- public Scanner(File directory, FilenameFilter filter)
+ public Scanner(File directory, final String filterString)
{
this.directory = canon(directory);
- this.filter = filter;
+ if (filterString != null && filterString.length() > 0)
+ {
+ this.filter = new FilenameFilter()
+ {
+ Pattern pattern = Pattern.compile(filterString);
+ public boolean accept(File dir, String name)
+ {
+ return pattern.matcher(name).matches();
+ }
+ };
+ }
+ else
+ {
+ this.filter = null;
+ }
}
/**
@@ -127,6 +143,9 @@
return files;
}
+ public void close() throws IOException {
+ }
+
private static File canon(File file)
{
try
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Watcher.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Watcher.java
new file mode 100644
index 0000000..acd4a53
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Watcher.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.felix.fileinstall.internal;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
+
+/**
+ * A File watching service
+ */
+public abstract class Watcher implements Closeable {
+
+ private Path root;
+ private boolean watch = true;
+ private WatchService watcher;
+ private PathMatcher dirMatcher;
+ private PathMatcher fileMatcher;
+ private final Map<WatchKey, Path> keys = new ConcurrentHashMap<WatchKey, Path>();
+ private volatile long lastModified;
+ private final Map<Path, Boolean> processedMap = new ConcurrentHashMap<Path, Boolean>();
+
+ public void init() throws IOException {
+ if (root == null) {
+ Iterable<Path> rootDirectories = getFileSystem().getRootDirectories();
+ for (Path rootDirectory : rootDirectories) {
+ if (rootDirectory != null) {
+ root = rootDirectory;
+ break;
+ }
+ }
+ }
+ if (!Files.exists(root)) {
+ fail("Root path does not exist: " + root);
+ } else if (!Files.isDirectory(root)) {
+ fail("Root path is not a directory: " + root);
+ }
+ if (watcher == null) {
+ watcher = watch ? getFileSystem().newWatchService() : null;
+ }
+ }
+
+ public void close() throws IOException {
+ if (watcher != null) {
+ watcher.close();
+ }
+ }
+
+ public long getLastModified() {
+ return lastModified;
+ }
+
+ // Properties
+ //-------------------------------------------------------------------------
+
+
+ public void setRootPath(String rootPath) {
+ Path path = new File(rootPath).getAbsoluteFile().toPath();
+ setRoot(path);
+ }
+
+ public void setRootDirectory(File directory) {
+ setRoot(directory.toPath());
+ }
+
+ public Path getRoot() {
+ return root;
+ }
+
+ public void setRoot(Path root) {
+ this.root = root;
+ }
+
+ public boolean isWatch() {
+ return watch;
+ }
+
+ public void setWatch(boolean watch) {
+ this.watch = watch;
+ }
+
+ public WatchService getWatcher() {
+ return watcher;
+ }
+
+ public void setWatcher(WatchService watcher) {
+ this.watcher = watcher;
+ }
+
+ public PathMatcher getDirMatcher() {
+ return dirMatcher;
+ }
+
+ public void setDirMatcher(PathMatcher dirMatcher) {
+ this.dirMatcher = dirMatcher;
+ }
+
+ public PathMatcher getFileMatcher() {
+ return fileMatcher;
+ }
+
+ public void setFileMatcher(PathMatcher fileMatcher) {
+ this.fileMatcher = fileMatcher;
+ }
+
+
+ // Implementation methods
+ //-------------------------------------------------------------------------
+
+ public void rescan() throws IOException {
+ for (WatchKey key : keys.keySet()) {
+ key.cancel();
+ }
+ keys.clear();
+ Files.walkFileTree(root, new FilteringFileVisitor());
+ }
+
+ public void processEvents() {
+ while (true) {
+ WatchKey key = watcher.poll();
+ if (key == null) {
+ break;
+ }
+ Path dir = keys.get(key);
+ if (dir == null) {
+ warn("Could not find key for " + key);
+ continue;
+ }
+
+ for (WatchEvent<?> event : key.pollEvents()) {
+ WatchEvent.Kind kind = event.kind();
+ WatchEvent<Path> ev = (WatchEvent<Path>)event;
+
+ // Context for directory entry event is the file name of entry
+ Path name = ev.context();
+ Path child = dir.resolve(name);
+
+ debug("Processing event {} on path {}", kind, child);
+
+ if (kind == OVERFLOW) {
+// rescan();
+ continue;
+ }
+
+ try {
+ if (kind == ENTRY_CREATE) {
+ if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
+
+ // if directory is created, and watching recursively, then
+ // register it and its sub-directories
+ Files.walkFileTree(child, new FilteringFileVisitor());
+ } else if (Files.isRegularFile(child, NOFOLLOW_LINKS)) {
+ scan(child);
+ }
+ } else if (kind == ENTRY_MODIFY) {
+ if (Files.isRegularFile(child, NOFOLLOW_LINKS)) {
+ scan(child);
+ }
+ } else if (kind == ENTRY_DELETE) {
+ unscan(child);
+ }
+ } catch (IOException x) {
+ // ignore to keep sample readbale
+ x.printStackTrace();
+ }
+ }
+
+ // reset key and remove from set if directory no longer accessible
+ boolean valid = key.reset();
+ if (!valid) {
+ debug("Removing key " + key + " and dir " + dir + " from keys");
+ keys.remove(key);
+
+ // all directories are inaccessible
+ if (keys.isEmpty()) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void scan(final Path file) throws IOException {
+ if (isMatchesFile(file)) {
+ process(file);
+ processedMap.put(file, Boolean.TRUE);
+ }
+ }
+
+ protected boolean isMatchesFile(Path file) {
+ boolean matches = true;
+ if (fileMatcher != null) {
+ Path rel = root.relativize(file);
+ matches = fileMatcher.matches(rel);
+ }
+ return matches;
+ }
+
+ private void unscan(final Path file) throws IOException {
+ if (isMatchesFile(file)) {
+ onRemove(file);
+ lastModified = System.currentTimeMillis();
+ } else {
+ // lets find all the files that now no longer exist
+ List<Path> files = new ArrayList<Path>(processedMap.keySet());
+ for (Path path : files) {
+ if (!Files.exists(path)) {
+ debug("File has been deleted: " + path);
+ processedMap.remove(path);
+ if (isMatchesFile(path)) {
+ onRemove(file);
+ lastModified = System.currentTimeMillis();
+ }
+ }
+ }
+ }
+ }
+
+ private void watch(final Path path) throws IOException {
+ if (watcher != null) {
+ WatchKey key = path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
+ keys.put(key, path);
+ debug("Watched path " + path + " key " + key);
+ } else {
+ warn("No watcher yet for path " + path);
+ }
+ }
+
+ protected FileSystem getFileSystem() {
+ return FileSystems.getDefault();
+ }
+
+ public class FilteringFileVisitor implements FileVisitor<Path> {
+
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+ if (Thread.interrupted()) {
+ throw new InterruptedIOException();
+ }
+ if (dirMatcher != null) {
+ Path rel = root.relativize(dir);
+ if (!"".equals(rel.toString()) && !dirMatcher.matches(rel)) {
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ }
+ watch(dir);
+ return FileVisitResult.CONTINUE;
+ }
+
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (Thread.interrupted()) {
+ throw new InterruptedIOException();
+ }
+ scan(file);
+ return FileVisitResult.CONTINUE;
+ }
+
+ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+ }
+
+
+ /**
+ * Throws an invalid argument exception after logging a warning
+ * just in case the stack trace gets gobbled up by application containers
+ * like spring or blueprint, at least the error message will be clearly shown in the log
+ *
+ */
+ public void fail(String message) {
+ warn(message);
+ throw new IllegalArgumentException(message);
+ }
+
+ protected abstract void debug(String message, Object... args);
+ protected abstract void warn(String message, Object... args);
+ protected abstract void process(Path path);
+ protected abstract void onRemove(Path path);
+}
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/WatcherScanner.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/WatcherScanner.java
new file mode 100644
index 0000000..b73c044
--- /dev/null
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/WatcherScanner.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.felix.fileinstall.internal;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.osgi.framework.BundleContext;
+
+public class WatcherScanner extends Scanner {
+
+ BundleContext bundleContext;
+ PathMatcher fileMatcher;
+ Watcher watcher;
+
+ Set<File> changed = new HashSet<File>();
+
+ /**
+ * Create a scanner for the specified directory and file filter
+ *
+ * @param directory the directory to scan
+ * @param filterString a filter for file names
+ */
+ public WatcherScanner(BundleContext bundleContext, File directory, String filterString) throws IOException {
+ super(directory, filterString);
+ this.bundleContext = bundleContext;
+ if (filterString != null) {
+ this.fileMatcher = FileSystems.getDefault().getPathMatcher("regex:" + filterString);
+ } else {
+ this.fileMatcher = null;
+ }
+ this.watcher = new ScannerWatcher();
+ this.watcher.setFileMatcher(fileMatcher);
+ this.watcher.setRootDirectory(this.directory);
+ this.watcher.init();
+ this.watcher.rescan();
+ }
+
+ public Set<File> scan(boolean reportImmediately) {
+ watcher.processEvents();
+ if (changed.isEmpty()) {
+ return new HashSet<File>();
+ }
+ Set<File> files = new HashSet<File>();
+ Set<File> removed = new HashSet<File>();
+ if (reportImmediately) {
+ removed.addAll(storedChecksums.keySet());
+ }
+ for (File file : changed)
+ {
+ long lastChecksum = lastChecksums.get(file) != null ? (Long) lastChecksums.get(file) : 0;
+ long storedChecksum = storedChecksums.get(file) != null ? (Long) storedChecksums.get(file) : 0;
+ long newChecksum = checksum(file);
+ lastChecksums.put(file, newChecksum);
+ if (file.exists()) {
+ // Only handle file when it does not change anymore and it has changed since last reported
+ if ((newChecksum == lastChecksum || reportImmediately)) {
+ if (newChecksum != storedChecksum) {
+ storedChecksums.put(file, newChecksum);
+ files.add(file);
+ } else {
+ changed.remove(file);
+ }
+ if (reportImmediately) {
+ removed.remove(file);
+ }
+ }
+ } else {
+ if (!reportImmediately) {
+ removed.add(file);
+ }
+ }
+ }
+ for (File file : removed)
+ {
+ // Make sure we'll handle a file that has been deleted
+ files.addAll(removed);
+ // Remove no longer used checksums
+ lastChecksums.remove(file);
+ storedChecksums.remove(file);
+ }
+ for (File file : files)
+ {
+ changed.remove(file);
+ }
+
+ return files;
+ }
+
+ public void close() throws IOException {
+ watcher.close();
+ }
+
+ class ScannerWatcher extends Watcher {
+
+ @Override
+ protected void process(Path path) {
+ File file = path.toFile();
+ while (!file.getParentFile().equals(directory)) {
+ file = file.getParentFile();
+ if (file == null) {
+ return;
+ }
+ }
+ changed.add(file);
+ }
+
+ @Override
+ protected void onRemove(Path path) {
+ File file = path.toFile();
+ while (!file.getParentFile().equals(directory)) {
+ file = file.getParentFile();
+ if (file == null) {
+ return;
+ }
+ }
+ changed.add(file);
+ }
+
+ @Override
+ protected void debug(String message, Object... args) {
+ log(Util.Logger.LOG_DEBUG, message, args);
+ }
+
+ @Override
+ protected void warn(String message, Object... args) {
+ log(Util.Logger.LOG_WARNING, message, args);
+ }
+
+ protected void log(int level, String message, Object... args) {
+ String msg = String.format(message, args);
+ Util.log(bundleContext, Util.getGlobalLogLevel(bundleContext),
+ level, msg, null);
+ }
+
+ }
+
+}