/******************************************************************************* * 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; } } }