blob: 1f1773510ce8bb772f9cbfa2e2c64e98bf4a28e1 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, 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.facebook.buck.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.eventbus.EventBus;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.LinkOption;
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.Map;
/**
* Watches a ProjectFilesystem for file changes using a given WatchService.
* Change events are posted to a given EventBus when postEvents are called unless the affected files are
* contained within the given excludeDirectories.
*/
public class ProjectFilesystemWatcher implements Closeable {
private final WatchService watchService; // TODO(user): use intellij file watching?
private final Map<WatchKey,Path> keys;
private final EventBus eventBus;
private final ProjectFilesystem filesystem;
private final ImmutableSet<String> ignoredPrefixes;
public ProjectFilesystemWatcher(ProjectFilesystem filesystem,
EventBus fileChangeEventBus,
ImmutableSet<String> excludeDirectories,
WatchService watchService) throws IOException {
this.filesystem = Preconditions.checkNotNull(filesystem);
this.ignoredPrefixes = Preconditions.checkNotNull(excludeDirectories);
this.eventBus = Preconditions.checkNotNull(fileChangeEventBus);
this.watchService = Preconditions.checkNotNull(watchService);
this.keys = Maps.newHashMap();
registerAll(filesystem.getRootPath());
}
/** Post filesystem events to eventBus */
public void postEvents() throws IOException {
WatchKey key;
while((key = watchService.poll()) != null) {
Path dir = keys.get(key);
if (dir == null) {
continue; // Ignored or unknown directory.
}
for (final WatchEvent<?> event : key.pollEvents()) {
if (filesystem.isPathChangeEvent(event)) {
// Check against ignored directories.
Path name = (Path) event.context();
final Path child = dir.resolve(name);
if (shouldIgnore(child)) {
continue;
}
// If directory is created, watch its children.
if (filesystem.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
registerAll(child);
}
continue; // TODO(user): post events about directories?
}
// Path returned by event.context() is relative to key directory, so return resolved
// child Path instead to allow clients to access the full, absolute Path correctly.
eventBus.post(new WatchEvent<Path>(){
@Override
@SuppressWarnings("unchecked") // Needed for conversion from Kind<?> to Kind<Path>
public Kind<Path> kind() {
return (Kind<Path>) event.kind();
}
@Override
public int count() {
return event.count();
}
@Override
public Path context() {
return child;
}
});
} else {
eventBus.post(event);
}
}
// Reset key and remove from set if directory no longer accessible
if (!key.reset()) {
keys.remove(key);
}
}
}
private boolean shouldIgnore(Path path) {
Path normalizedPath = path.normalize();
for (String prefix : ignoredPrefixes) {
if (normalizedPath.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* Register the given directory with the WatchService.
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService, unless it's an ignored sub-tree.
*/
private void registerAll(final Path start) throws IOException {
filesystem.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attributes)
throws IOException {
if (shouldIgnore(dir)) {
return FileVisitResult.SKIP_SUBTREE;
}
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
@Override
public void close() throws IOException {
watchService.close();
}
}