/******************************************************************************* * 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.watcher; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.ClosedWatchServiceException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchEvent.Modifier; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import static java.lang.Thread.currentThread; import static java.nio.file.Files.exists; 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.Executors.newSingleThreadExecutor; import static java.util.concurrent.TimeUnit.SECONDS; import static org.eclipse.che.api.vfs.watcher.FileWatcherUtils.isExcluded; /** * Watches directories for interactions with their entries. Based on * {@link WatchService} that uses underlying filesystem implementations. Does * not perform any data modification (including filesystem items) except for * tracking and notification the upper layers. Service operates with ordinary * java file system paths in counter to che virtual file system which may have * custom root element and structure. Transforming one we of path representation * into another and backwards is the responsibility of upper services. */ @Singleton public class FileWatcherService { private static final Logger LOG = LoggerFactory.getLogger(FileWatcherService.class); private final AtomicBoolean suspended = new AtomicBoolean(true); private final AtomicBoolean running = new AtomicBoolean(); private final Map<WatchKey, Path> keys = new ConcurrentHashMap<>(); private final Map<Path, Integer> registrations = new ConcurrentHashMap<>(); private final Set<PathMatcher> excludes; private final FileWatcherEventHandler handler; private final WatchService service; private final Modifier[] eventModifiers; private final Kind<?>[] eventKinds; private ExecutorService executor; @Inject public FileWatcherService(@Named("che.user.workspaces.storage.excludes") Set<PathMatcher> excludes, FileWatcherEventHandler handler, WatchService service) { this.excludes = excludes; this.handler = handler; this.service = service; this.eventModifiers = getWatchEventModifiers(); this.eventKinds = getWatchEventKinds(); } @SuppressWarnings("unchecked") private static <T> WatchEvent<T> cast(WatchEvent<?> event) { return (WatchEvent<T>)event; } @SuppressWarnings("unchecked") private static <T> T cast(Object event) { return (T)event; } private static Kind<?>[] getWatchEventKinds() { return new Kind<?>[]{ENTRY_DELETE, ENTRY_MODIFY, ENTRY_CREATE}; } /** * This is required to speed up mac based file watcher implementations * * @return sensitivity watch event modifier */ private static Modifier[] getWatchEventModifiers() { String className = "com.sun.nio.file.SensitivityWatchEventModifier"; try { Class<?> c = Class.forName(className); Field f = c.getField("HIGH"); Modifier modifier = cast(f.get(c)); LOG.debug("Class '{}' is found in classpath setting corresponding watch modifier", className); return new Modifier[]{modifier}; } catch (Exception e) { LOG.debug("Class '{}' is not found in classpath, falling to default mode", className, e); return new Modifier[]{}; } } @PostConstruct void start() throws IOException { ThreadFactoryBuilder builder = new ThreadFactoryBuilder(); ThreadFactory factory = builder.setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance()) .setNameFormat(FileWatcherService.class.getSimpleName()) .setDaemon(true) .build(); executor = newSingleThreadExecutor(factory); executor.execute(this::run); } @PreDestroy void stop() { running.compareAndSet(true, false); try { LOG.debug("Cancelling watch keys"); keys.keySet().forEach(WatchKey::cancel); LOG.debug("Closing java watch service"); service.close(); } catch (IOException e) { LOG.error("Closing of java watch service failed: ", e.getMessage()); } try { LOG.debug("Executor task shutdown started"); executor.shutdown(); executor.awaitTermination(5, SECONDS); } catch (InterruptedException e) { currentThread().interrupt(); LOG.debug("Executor task is interrupted"); } finally { if (!executor.isTerminated()) { LOG.debug("Executor task is not shutdown yet"); } executor.shutdownNow(); LOG.debug("Executor tasks have been shutdown"); } } boolean isStopped() { return executor.isShutdown(); } /** * Registers a directory for tracking of corresponding entry creation, * modification or deletion events. Each call of this method increase * by one registration counter that corresponds to each folder being * watched. Any event related to such directory entry is passed further to * the specific handler only if registration counter related to the * directory is above zero, otherwise registration watch key is canceled * and no further directory watching is being performed. * * @param dir * directory */ public void register(Path dir) { if (!Files.exists(dir)) { LOG.debug("Trying to register directory '{}' but it does not exist", dir); return; } LOG.debug("Registering directory '{}'", dir); if (keys.values().contains(dir)) { int previous = registrations.get(dir); LOG.debug("Directory is already being watched, increasing watch counter, previous value: {}", previous); registrations.put(dir, previous + 1); } else { try { LOG.debug("Starting watching directory '{}'", dir); WatchKey watchKey = dir.register(service, eventKinds, eventModifiers); keys.put(watchKey, dir); registrations.put(dir, 1); } catch (IOException e) { LOG.error("Can't register dir {} in file watch service", dir, e); } } } /** * Cancels registration of a directory for being watched. Each call of this * method decreases by one registration counter that corresponds to * directory specified by the argument. If registration counter comes to * zero directory watching is totally cancelled. * <p> * If this method is called for not existing directory nothing happens. * <p> * If this method is called for not registered directory nothing happens. * * @param dir * directory */ void unRegister(Path dir) { LOG.debug("Canceling directory '{}' registration", dir); Predicate<Entry<WatchKey, Path>> equalsDir = it -> it.getValue().equals(dir); if (!exists(dir)) { LOG.debug("Trying to unregister directory '{}' while it does not exist", dir); registrations.remove(dir); keys.entrySet().stream().filter(equalsDir).map(Entry::getKey).forEach(WatchKey::cancel); keys.entrySet().removeIf(equalsDir); return; } if (!registrations.containsKey(dir)) { LOG.debug("Trying to unregister directory '{}' while it is not registered", dir); return; } int previous = registrations.get(dir); if (previous == 1) { LOG.debug("Stopping watching directory '{}'", dir); registrations.remove(dir); keys.entrySet().stream().filter(equalsDir).map(Entry::getKey).forEach(WatchKey::cancel); keys.entrySet().removeIf(equalsDir); } else { LOG.debug("Directory is being watched by someone else, decreasing watch counter, previous value: {}", previous); registrations.put(dir, previous - 1); } } /** * Resumes service after it was in suspended state. If method is called * when the service is already not in a suspended state nothing happens. */ void resume() { if (suspended.compareAndSet(true, false)) { LOG.debug("Resuming service."); } } /** * Temporary suspends service of generating any events. Events received by * service in suspended state are totally skipped. If method is called when * the service is already in a suspended state nothing happens. */ void suspend() { if (suspended.compareAndSet(false, true)) { LOG.debug("Suspending service."); } } private void run() { suspended.compareAndSet(true, false); running.compareAndSet(false, true); while (running.get()) { try { WatchKey watchKey = service.take(); Path dir = keys.get(watchKey); List<WatchEvent<?>> watchEvents = watchKey.pollEvents(); if (suspended.get()) { resetAndRemove(watchKey, dir); LOG.debug("File watchers are running in suspended mode - skipping."); continue; } for (WatchEvent<?> event : watchEvents) { Kind<?> kind = event.kind(); if (kind == OVERFLOW) { LOG.warn("Detected file system events overflowing"); continue; } WatchEvent<Path> ev = cast(event); Path item = ev.context(); Path path = dir.resolve(item).toAbsolutePath(); if (isExcluded(excludes, path)) { LOG.debug("Path is within exclude list, skipping..."); continue; } handler.handle(path, kind); } resetAndRemove(watchKey, dir); } catch (InterruptedException e) { running.compareAndSet(true, false); LOG.debug("Interruption error when running file watcher, most likely caused by stopping it", e); } catch (ClosedWatchServiceException e) { running.compareAndSet(true, false); LOG.debug("Closing watch service while some of keys may be processing", e); } } } private void resetAndRemove(WatchKey watchKey, Path dir) { if (!watchKey.reset()) { if (dir != null) { registrations.remove(dir); } keys.remove(watchKey); } } }