/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* 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.intellij.openapi.vfs.impl.local;
import com.intellij.notification.*;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.local.FileWatcherNotificationSink;
import com.intellij.openapi.vfs.local.PluggableFileWatcher;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* @author max
*/
public class FileWatcher {
private static final Logger LOG = Logger.getInstance(FileWatcher.class);
public static final NotNullLazyValue<NotificationGroup> NOTIFICATION_GROUP = new NotNullLazyValue<NotificationGroup>() {
@NotNull
@Override
protected NotificationGroup compute() {
return new NotificationGroup("File Watcher Messages", NotificationDisplayType.STICKY_BALLOON, true);
}
};
public static class DirtyPaths {
public final Set<String> dirtyPaths = ContainerUtil.newTroveSet();
public final Set<String> dirtyPathsRecursive = ContainerUtil.newTroveSet();
public final Set<String> dirtyDirectories = ContainerUtil.newTroveSet();
public static final DirtyPaths EMPTY = new DirtyPaths();
public boolean isEmpty() {
return dirtyPaths.isEmpty() && dirtyPathsRecursive.isEmpty() && dirtyDirectories.isEmpty();
}
private void addDirtyPath(String path) {
if (!dirtyPathsRecursive.contains(path)) {
dirtyPaths.add(path);
}
}
private void addDirtyPathRecursive(String path) {
dirtyPaths.remove(path);
dirtyPathsRecursive.add(path);
}
}
private final ManagingFS myManagingFS;
private final MyFileWatcherNotificationSink myNotificationSink;
private final PluggableFileWatcher[] myWatchers;
private final AtomicBoolean myFailureShown = new AtomicBoolean(false);
private volatile CanonicalPathMap myPathMap = new CanonicalPathMap();
private volatile List<Collection<String>> myManualWatchRoots = Collections.emptyList();
FileWatcher(@NotNull ManagingFS managingFS) {
myManagingFS = managingFS;
myNotificationSink = new MyFileWatcherNotificationSink();
myWatchers = new PluggableFileWatcher[] {new NativeFileWatcherImpl()}; //FIXME [VISTALL] this is dirty hack, due we don't allow change file watcher
for (PluggableFileWatcher watcher : myWatchers) {
watcher.initialize(myManagingFS, myNotificationSink);
}
}
public void dispose() {
for (PluggableFileWatcher watcher : myWatchers) {
watcher.dispose();
}
}
public boolean isOperational() {
for (PluggableFileWatcher watcher : myWatchers) {
if (watcher.isOperational()) return true;
}
return false;
}
public boolean isSettingRoots() {
for (PluggableFileWatcher watcher : myWatchers) {
if (watcher.isSettingRoots()) return true;
}
return false;
}
@NotNull
public DirtyPaths getDirtyPaths() {
return myNotificationSink.getDirtyPaths();
}
@NotNull
public Collection<String> getManualWatchRoots() {
List<Collection<String>> manualWatchRoots = myManualWatchRoots;
Set<String> result = null;
for (Collection<String> roots : manualWatchRoots) {
if (result == null) {
result = ContainerUtil.newHashSet(roots);
}
else {
result.retainAll(roots);
}
}
return result != null ? result : Collections.emptyList();
}
/**
* Clients should take care of not calling this method in parallel.
*/
public void setWatchRoots(@NotNull List<String> recursive, @NotNull List<String> flat) {
CanonicalPathMap pathMap = new CanonicalPathMap(recursive, flat);
myPathMap = pathMap;
myManualWatchRoots = ContainerUtil.createLockFreeCopyOnWriteList();
for (PluggableFileWatcher watcher : myWatchers) {
watcher.setWatchRoots(pathMap.getCanonicalRecursiveWatchRoots(), pathMap.getCanonicalFlatWatchRoots());
}
}
public void notifyOnFailure(@NotNull String cause, @Nullable NotificationListener listener) {
LOG.warn(cause);
if (myFailureShown.compareAndSet(false, true)) {
String title = ApplicationBundle.message("watcher.slow.sync");
ApplicationManager.getApplication().invokeLater(() -> Notifications.Bus.notify(NOTIFICATION_GROUP.getValue().createNotification(title, cause, NotificationType.WARNING, listener)), ModalityState.NON_MODAL);
}
}
private class MyFileWatcherNotificationSink implements FileWatcherNotificationSink {
private final Object myLock = new Object();
private DirtyPaths myDirtyPaths = new DirtyPaths();
private DirtyPaths getDirtyPaths() {
DirtyPaths dirtyPaths = DirtyPaths.EMPTY;
synchronized (myLock) {
if (!myDirtyPaths.isEmpty()) {
dirtyPaths = myDirtyPaths;
myDirtyPaths = new DirtyPaths();
}
}
for (PluggableFileWatcher watcher : myWatchers) {
watcher.resetChangedPaths();
}
return dirtyPaths;
}
@Override
public void notifyManualWatchRoots(@NotNull Collection<String> roots) {
myManualWatchRoots.add(roots.isEmpty() ? Collections.emptySet() : ContainerUtil.newHashSet(roots));
notifyOnAnyEvent();
}
@Override
public void notifyMapping(@NotNull Collection<Pair<String, String>> mapping) {
if (!mapping.isEmpty()) {
myPathMap.addMapping(mapping);
}
notifyOnAnyEvent();
}
@Override
public void notifyDirtyPath(@NotNull String path) {
Collection<String> paths = myPathMap.getWatchedPaths(path, true);
if (!paths.isEmpty()) {
synchronized (myLock) {
for (String eachPath : paths) {
myDirtyPaths.addDirtyPath(eachPath);
}
}
}
notifyOnAnyEvent();
}
@Override
public void notifyPathCreatedOrDeleted(@NotNull String path) {
Collection<String> paths = myPathMap.getWatchedPaths(path, true);
if (!paths.isEmpty()) {
synchronized (myLock) {
for (String p : paths) {
myDirtyPaths.addDirtyPathRecursive(p);
String parentPath = new File(p).getParent();
if (parentPath != null) {
myDirtyPaths.addDirtyPath(parentPath);
}
}
}
}
notifyOnAnyEvent();
}
@Override
public void notifyDirtyDirectory(@NotNull String path) {
Collection<String> paths = myPathMap.getWatchedPaths(path, false);
if (!paths.isEmpty()) {
synchronized (myLock) {
myDirtyPaths.dirtyDirectories.addAll(paths);
}
}
notifyOnAnyEvent();
}
@Override
public void notifyDirtyPathRecursive(@NotNull String path) {
Collection<String> paths = myPathMap.getWatchedPaths(path, false);
if (!paths.isEmpty()) {
synchronized (myLock) {
for (String each : paths) {
myDirtyPaths.addDirtyPathRecursive(each);
}
}
}
notifyOnAnyEvent();
}
@Override
public void notifyReset(@Nullable String path) {
if (path != null) {
synchronized (myLock) {
myDirtyPaths.addDirtyPathRecursive(path);
}
}
else {
VirtualFile[] roots = myManagingFS.getLocalRoots();
synchronized (myLock) {
for (VirtualFile root : roots) {
myDirtyPaths.addDirtyPathRecursive(root.getPresentableUrl());
}
}
}
notifyOnReset();
}
@Override
public void notifyUserOnFailure(@NotNull String cause, @Nullable NotificationListener listener) {
notifyOnFailure(cause, listener);
}
}
/* test data and methods */
private volatile Consumer<Boolean> myTestNotifier = null;
private void notifyOnAnyEvent() {
Consumer<Boolean> notifier = myTestNotifier;
if (notifier != null) notifier.accept(Boolean.FALSE);
}
private void notifyOnReset() {
Consumer<Boolean> notifier = myTestNotifier;
if (notifier != null) notifier.accept(Boolean.TRUE);
}
@TestOnly
public void startup(@Nullable Consumer<Boolean> notifier) throws IOException {
myTestNotifier = notifier;
for (PluggableFileWatcher watcher : myWatchers) {
watcher.startup();
}
}
@TestOnly
public void shutdown() throws InterruptedException {
for (PluggableFileWatcher watcher : myWatchers) {
watcher.shutdown();
}
myTestNotifier = null;
}
}