/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.vfs.impl.file;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newLinkedHashSet;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.Files.getLastModifiedTime;
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;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.CREATED;
import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.DELETED;
import static org.eclipse.che.api.project.shared.dto.event.FileWatcherEventType.MODIFIED;
@Singleton
public class FileTreeWatcher {
private static final Logger LOG = LoggerFactory.getLogger(FileTreeWatcher.class);
private static final long EVENT_PROCESS_TIMEOUT_SEC = 2;
private final File watchRoot;
private final Path watchRootPath;
private final Map<Path, WatchedDirectory> watchedDirectories;
private final List<PathMatcher> excludePatterns;
private final FileWatcherNotificationHandler fileWatcherNotificationHandler;
private final ExecutorService executor;
private final AtomicBoolean running;
private WatchService watchService;
private WatchEvent.Modifier[] watchEventModifiers;
@Inject
public FileTreeWatcher(@Named("che.user.workspaces.storage") File watchRoot,
@Named("vfs.index_filter_matcher") Set<PathMatcher> excludePatterns,
FileWatcherNotificationHandler fileWatcherNotificationHandler) {
watchEventModifiers = new WatchEvent.Modifier[0];
this.watchRoot = toCanonicalFile(watchRoot);
this.watchRootPath = this.watchRoot.toPath();
this.excludePatterns = newArrayList(excludePatterns);
this.fileWatcherNotificationHandler = fileWatcherNotificationHandler;
ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(true)
.setUncaughtExceptionHandler(
LoggingUncaughtExceptionHandler.getInstance())
.setNameFormat("FileTreeWatcher-%d")
.build();
executor = Executors.newSingleThreadExecutor(threadFactory);
running = new AtomicBoolean();
watchedDirectories = newHashMap();
}
private static File toCanonicalFile(File file) {
try {
return file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
public void startup() throws IOException {
watchService = FileSystems.getDefault().newWatchService();
if (isPollingWatchService(watchService)) {
watchEventModifiers = new WatchEvent.Modifier[]{createSensitivityWatchEventModifier()};
}
running.set(true);
walkTreeAndSetupWatches(watchRootPath);
executor.execute(new WatchEventTask());
fileWatcherNotificationHandler.started(watchRoot);
}
private boolean isPollingWatchService(WatchService watchService) {
return "sun.nio.fs.PollingWatchService".equals(watchService.getClass().getName());
}
private WatchEvent.Modifier createSensitivityWatchEventModifier() {
try {
Class<?> aModifierEnum = Class.forName("com.sun.nio.file.SensitivityWatchEventModifier");
Object[] sensitivityEnumConstants = aModifierEnum.getEnumConstants();
return (WatchEvent.Modifier)sensitivityEnumConstants[0];
} catch (Exception e) {
LOG.warn("Can't create 'com.sun.nio.file.SensitivityWatchEventModifier'", e);
}
return null;
}
public void shutdown() {
boolean interrupted = false;
executor.shutdown();
try {
if (!executor.awaitTermination(3, SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(3, SECONDS)) {
LOG.warn("Unable terminate Executor");
}
}
} catch (InterruptedException e) {
interrupted = true;
executor.shutdownNow();
}
try {
walkTreeAndRemoveWatches(watchRootPath);
} catch (IOException e) {
LOG.warn(e.getMessage());
}
try {
watchService.close();
} catch (IOException e) {
LOG.warn(e.getMessage());
}
if (interrupted) {
Thread.currentThread().interrupt();
}
}
public void addExcludeMatcher(PathMatcher exclude) {
this.excludePatterns.add(exclude);
}
public void removeExcludeMatcher(PathMatcher exclude) {
this.excludePatterns.remove(exclude);
}
private void walkTreeAndSetupWatches(Path root) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path relativePath = watchRootPath.relativize(dir);
if (shouldNotify(relativePath)) {
setupDirectoryWatcher(dir);
}
return CONTINUE;
}
});
}
private boolean shouldNotify(Path subPath) {
for (PathMatcher excludePattern : excludePatterns) {
if (excludePattern.matches(subPath)) {
return false;
}
}
return true;
}
private void walkTreeAndRemoveWatches(Path root) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
cancelDirectoryWatcher(dir);
return CONTINUE;
}
});
}
private void walkTreeAndFireCreatedEvents(Path root) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (!dir.equals(root)) {
fireWatchEvent(CREATED, dir, true);
}
return CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
fireWatchEvent(CREATED, file, false);
return CONTINUE;
}
});
}
private void setupDirectoryWatcher(Path directory) throws IOException {
if (watchedDirectories.get(directory) == null) {
WatchKey watchKey = directory.register(watchService,
new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW},
watchEventModifiers);
WatchedDirectory watchedDirectory = new WatchedDirectory(directory, watchKey);
try (DirectoryStream<Path> entries = Files.newDirectoryStream(directory)) {
for (Path entry : entries) {
watchedDirectory
.addItem(new DirectoryItem(entry.getFileName(), Files.isDirectory(entry), getLastModifiedInMillis(entry)));
if (Files.isDirectory(entry)) {
setupDirectoryWatcher(entry);
}
}
}
watchedDirectories.put(directory, watchedDirectory);
}
}
private void cancelDirectoryWatcher(Path path) {
WatchedDirectory watchedDirectory = watchedDirectories.remove(path);
if (watchedDirectory != null) {
watchedDirectory.getWatchKey().cancel();
}
}
private class WatchEventTask implements Runnable {
final Set<PendingEvent> pendingEvents = newLinkedHashSet();
@Override
public void run() {
while (running.get()) {
try {
WatchKey watchKey;
if (pendingEvents.isEmpty()) {
watchKey = watchService.take();
} else {
watchKey = watchService.poll(EVENT_PROCESS_TIMEOUT_SEC, SECONDS);
if (watchKey == null) {
processPendingEvents(pendingEvents);
pendingEvents.clear();
}
}
if (watchKey != null) {
pendingEvents.add(new PendingEvent((Path)watchKey.watchable()));
watchKey.pollEvents();
watchKey.reset();
}
} catch (InterruptedException | ClosedWatchServiceException e) {
running.set(false);
} catch (Throwable e) {
running.set(false);
fileWatcherNotificationHandler.errorOccurred(watchRoot, e);
}
}
}
}
private void processPendingEvents(Collection<PendingEvent> pendingEvents) throws IOException {
for (PendingEvent pendingEvent : pendingEvents) {
Path eventDirectoryPath = pendingEvent.getPath();
WatchedDirectory watchedDirectory = watchedDirectories.get(eventDirectoryPath);
if (watchedDirectory == null){
continue;
}
if (Files.exists(eventDirectoryPath)) {
boolean isModifiedNotYetReported = true;
final int hitCounter = watchedDirectory.incrementHitCounter();
try (DirectoryStream<Path> entries = Files.newDirectoryStream(eventDirectoryPath)) {
for (Path fsItem : entries) {
DirectoryItem directoryItem = watchedDirectory.getItem(fsItem.getFileName());
if (directoryItem == null) {
try {
boolean directory = Files.isDirectory(fsItem);
directoryItem = new DirectoryItem(fsItem.getFileName(), directory, getLastModifiedInMillis(fsItem));
watchedDirectory.addItem(directoryItem);
if (isModifiedNotYetReported){
isModifiedNotYetReported = false;
fireWatchEvent(MODIFIED, eventDirectoryPath, true);
}
fireWatchEvent(CREATED, fsItem, directoryItem.isDirectory());
if (directory) {
walkTreeAndFireCreatedEvents(fsItem);
setupDirectoryWatcher(fsItem);
}
} catch (IOException ignored) {
}
} else {
long lastModified;
try {
lastModified = getLastModifiedInMillis(fsItem);
} catch (IOException ignored) {
continue;
}
if (lastModified != directoryItem.getLastModified() && Files.isRegularFile(fsItem)) {
fireWatchEvent(MODIFIED, fsItem, false);
}
directoryItem.touch(lastModified);
directoryItem.updateHitCounter(hitCounter);
}
}
}
for (Iterator<DirectoryItem> iterator = watchedDirectory.getItems().iterator(); iterator.hasNext(); ) {
DirectoryItem directoryItem = iterator.next();
if (hitCounter != directoryItem.getHitCount()) {
iterator.remove();
if (isModifiedNotYetReported){
isModifiedNotYetReported = false;
fireWatchEvent(MODIFIED, eventDirectoryPath, true);
}
fireWatchEvent(DELETED, eventDirectoryPath.resolve(directoryItem.getName()), directoryItem.isDirectory());
}
}
} else {
for (DirectoryItem directoryItem : watchedDirectory.getItems()) {
fireWatchEvent(DELETED, eventDirectoryPath.resolve(directoryItem.getName()), directoryItem.isDirectory());
}
watchedDirectories.remove(eventDirectoryPath);
}
}
}
private void fireWatchEvent(FileWatcherEventType eventType, Path eventPath, boolean isDirectory) {
Path relativePath = watchRootPath.relativize(eventPath);
if (shouldNotify(relativePath)) {
fileWatcherNotificationHandler.handleFileWatcherEvent(eventType, watchRoot, relativePath.toString(), isDirectory);
}
}
private long getLastModifiedInMillis(Path path) throws IOException {
return getLastModifiedTime(path, NOFOLLOW_LINKS).toMillis();
}
static class PendingEvent {
final Path path;
PendingEvent(Path path) {
this.path = path;
}
Path getPath() {
return path;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof PendingEvent) {
PendingEvent other = (PendingEvent)o;
return Objects.equals(path, other.path);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(path);
}
}
static class WatchedDirectory {
final Path path;
final WatchKey watchKey;
final List<DirectoryItem> items;
int hitCounter;
WatchedDirectory(Path path, WatchKey watchKey) {
this.path = path;
this.watchKey = watchKey;
items = newArrayList();
}
WatchKey getWatchKey() {
return watchKey;
}
Path getPath() {
return path;
}
DirectoryItem getItem(Path name) {
for (DirectoryItem item : items) {
if (item.getName().equals(name)) {
return item;
}
}
return null;
}
void addItem(DirectoryItem item) {
item.updateHitCounter(this.hitCounter);
items.add(item);
}
List<DirectoryItem> getItems() {
return items;
}
int incrementHitCounter() {
return ++hitCounter;
}
}
static class DirectoryItem {
final Path name;
final boolean directory;
long lastModified;
int hitCounter;
DirectoryItem(Path name, boolean directory, long lastModified) {
this.name = name;
this.directory = directory;
this.lastModified = lastModified;
}
long getLastModified() {
return lastModified;
}
Path getName() {
return name;
}
boolean isDirectory() {
return directory;
}
void touch(long lastModified) {
this.lastModified = lastModified;
}
int getHitCount() {
return hitCounter;
}
void updateHitCounter(int hitCounter) {
this.hitCounter = hitCounter;
}
}
}