Merge pull request #378 (https://github.com/JetBrains/intellij-community/pull/378)
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / impl / local / FileWatcher.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.vfs.impl.local;
17
18 import com.intellij.notification.*;
19 import com.intellij.openapi.application.ApplicationBundle;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.application.ModalityState;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.util.NotNullLazyValue;
24 import com.intellij.openapi.util.Pair;
25 import com.intellij.openapi.vfs.VirtualFile;
26 import com.intellij.openapi.vfs.local.FileWatcherNotificationSink;
27 import com.intellij.openapi.vfs.local.PluggableFileWatcher;
28 import com.intellij.openapi.vfs.newvfs.ManagingFS;
29 import com.intellij.util.containers.ContainerUtil;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32 import org.jetbrains.annotations.TestOnly;
33
34 import java.io.File;
35 import java.io.IOException;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.List;
39 import java.util.Set;
40 import java.util.concurrent.atomic.AtomicBoolean;
41 import java.util.function.Consumer;
42
43 /**
44  * @author max
45  */
46 public class FileWatcher {
47   private static final Logger LOG = Logger.getInstance(FileWatcher.class);
48
49   public static final NotNullLazyValue<NotificationGroup> NOTIFICATION_GROUP = new NotNullLazyValue<NotificationGroup>() {
50     @NotNull
51     @Override
52     protected NotificationGroup compute() {
53       return new NotificationGroup("File Watcher Messages", NotificationDisplayType.STICKY_BALLOON, true);
54     }
55   };
56
57   public static class DirtyPaths {
58     public final Set<String> dirtyPaths = ContainerUtil.newTroveSet();
59     public final Set<String> dirtyPathsRecursive = ContainerUtil.newTroveSet();
60     public final Set<String> dirtyDirectories = ContainerUtil.newTroveSet();
61
62     public static final DirtyPaths EMPTY = new DirtyPaths();
63
64     public boolean isEmpty() {
65       return dirtyPaths.isEmpty() && dirtyPathsRecursive.isEmpty() && dirtyDirectories.isEmpty();
66     }
67
68     private void addDirtyPath(String path) {
69       if (!dirtyPathsRecursive.contains(path)) {
70         dirtyPaths.add(path);
71       }
72     }
73
74     private void addDirtyPathRecursive(String path) {
75       dirtyPaths.remove(path);
76       dirtyPathsRecursive.add(path);
77     }
78   }
79
80   private final ManagingFS myManagingFS;
81   private final MyFileWatcherNotificationSink myNotificationSink;
82   private final PluggableFileWatcher[] myWatchers;
83   private final AtomicBoolean myFailureShown = new AtomicBoolean(false);
84
85   private volatile CanonicalPathMap myPathMap = new CanonicalPathMap();
86   private volatile List<Collection<String>> myManualWatchRoots = Collections.emptyList();
87
88   FileWatcher(@NotNull ManagingFS managingFS) {
89     myManagingFS = managingFS;
90     myNotificationSink = new MyFileWatcherNotificationSink();
91     myWatchers = PluggableFileWatcher.EP_NAME.getExtensions();
92     for (PluggableFileWatcher watcher : myWatchers) {
93       watcher.initialize(myManagingFS, myNotificationSink);
94     }
95   }
96
97   public void dispose() {
98     for (PluggableFileWatcher watcher : myWatchers) {
99       watcher.dispose();
100     }
101   }
102
103   public boolean isOperational() {
104     for (PluggableFileWatcher watcher : myWatchers) {
105       if (watcher.isOperational()) return true;
106     }
107     return false;
108   }
109
110   public boolean isSettingRoots() {
111     for (PluggableFileWatcher watcher : myWatchers) {
112       if (watcher.isSettingRoots()) return true;
113     }
114     return false;
115   }
116
117   @NotNull
118   public DirtyPaths getDirtyPaths() {
119     return myNotificationSink.getDirtyPaths();
120   }
121
122   @NotNull
123   public Collection<String> getManualWatchRoots() {
124     List<Collection<String>> manualWatchRoots = myManualWatchRoots;
125
126     Set<String> result = null;
127     for (Collection<String> roots : manualWatchRoots) {
128       if (result == null) {
129         result = ContainerUtil.newHashSet(roots);
130       }
131       else {
132         result.retainAll(roots);
133       }
134     }
135
136     return result != null ? result : Collections.emptyList();
137   }
138
139   /**
140    * Clients should take care of not calling this method in parallel.
141    */
142   public void setWatchRoots(@NotNull List<String> recursive, @NotNull List<String> flat) {
143     CanonicalPathMap pathMap = new CanonicalPathMap(recursive, flat);
144
145     myPathMap = pathMap;
146     myManualWatchRoots = ContainerUtil.createLockFreeCopyOnWriteList();
147
148     for (PluggableFileWatcher watcher : myWatchers) {
149       watcher.setWatchRoots(pathMap.getCanonicalRecursiveWatchRoots(), pathMap.getCanonicalFlatWatchRoots());
150     }
151   }
152
153   public void notifyOnFailure(@NotNull String cause, @Nullable NotificationListener listener) {
154     LOG.warn(cause);
155
156     if (myFailureShown.compareAndSet(false, true)) {
157       String title = ApplicationBundle.message("watcher.slow.sync");
158       ApplicationManager.getApplication().invokeLater(() -> {
159         Notifications.Bus.notify(NOTIFICATION_GROUP.getValue().createNotification(title, cause, NotificationType.WARNING, listener));
160       }, ModalityState.NON_MODAL);
161     }
162   }
163
164   private class MyFileWatcherNotificationSink implements FileWatcherNotificationSink {
165     private final Object myLock = new Object();
166     private DirtyPaths myDirtyPaths = new DirtyPaths();
167
168     private DirtyPaths getDirtyPaths() {
169       DirtyPaths dirtyPaths = DirtyPaths.EMPTY;
170
171       synchronized (myLock) {
172         if (!myDirtyPaths.isEmpty()) {
173           dirtyPaths = myDirtyPaths;
174           myDirtyPaths = new DirtyPaths();
175         }
176       }
177
178       for (PluggableFileWatcher watcher : myWatchers) {
179         watcher.resetChangedPaths();
180       }
181
182       return dirtyPaths;
183     }
184
185     @Override
186     public void notifyManualWatchRoots(@NotNull Collection<String> roots) {
187       myManualWatchRoots.add(ContainerUtil.newHashSet(roots));
188       notifyOnAnyEvent();
189     }
190
191     @Override
192     public void notifyMapping(@NotNull Collection<Pair<String, String>> mapping) {
193       if (!mapping.isEmpty()) {
194         myPathMap.addMapping(mapping);
195       }
196       notifyOnAnyEvent();
197     }
198
199     @Override
200     public void notifyDirtyPath(@NotNull String path) {
201       Collection<String> paths = myPathMap.getWatchedPaths(path, true);
202       if (!paths.isEmpty()) {
203         synchronized (myLock) {
204           for (String eachPath : paths) {
205             myDirtyPaths.addDirtyPath(eachPath);
206           }
207         }
208       }
209       notifyOnAnyEvent();
210     }
211
212     @Override
213     public void notifyPathCreatedOrDeleted(@NotNull String path) {
214       Collection<String> paths = myPathMap.getWatchedPaths(path, true);
215       if (!paths.isEmpty()) {
216         synchronized (myLock) {
217           for (String p : paths) {
218             myDirtyPaths.addDirtyPathRecursive(p);
219             String parentPath = new File(p).getParent();
220             if (parentPath != null) {
221               myDirtyPaths.addDirtyPath(parentPath);
222             }
223           }
224         }
225       }
226       notifyOnAnyEvent();
227     }
228
229     @Override
230     public void notifyDirtyDirectory(@NotNull String path) {
231       Collection<String> paths = myPathMap.getWatchedPaths(path, false);
232       if (!paths.isEmpty()) {
233         synchronized (myLock) {
234           myDirtyPaths.dirtyDirectories.addAll(paths);
235         }
236       }
237       notifyOnAnyEvent();
238     }
239
240     @Override
241     public void notifyDirtyPathRecursive(@NotNull String path) {
242       Collection<String> paths = myPathMap.getWatchedPaths(path, false);
243       if (!paths.isEmpty()) {
244         synchronized (myLock) {
245           for (String each : paths) {
246             myDirtyPaths.addDirtyPathRecursive(each);
247           }
248         }
249       }
250       notifyOnAnyEvent();
251     }
252
253     @Override
254     public void notifyReset(@Nullable String path) {
255       if (path != null) {
256         synchronized (myLock) {
257           myDirtyPaths.addDirtyPathRecursive(path);
258         }
259       }
260       else {
261         VirtualFile[] roots = myManagingFS.getLocalRoots();
262         synchronized (myLock) {
263           for (VirtualFile root : roots) {
264             myDirtyPaths.addDirtyPathRecursive(root.getPresentableUrl());
265           }
266         }
267       }
268       notifyOnReset();
269     }
270
271     @Override
272     public void notifyUserOnFailure(@NotNull String cause, @Nullable NotificationListener listener) {
273       notifyOnFailure(cause, listener);
274     }
275   }
276
277   /* test data and methods */
278
279   private volatile Consumer<Boolean> myTestNotifier = null;
280
281   private void notifyOnAnyEvent() {
282     Consumer<Boolean> notifier = myTestNotifier;
283     if (notifier != null) notifier.accept(Boolean.FALSE);
284   }
285
286   private void notifyOnReset() {
287     Consumer<Boolean> notifier = myTestNotifier;
288     if (notifier != null) notifier.accept(Boolean.TRUE);
289   }
290
291   @TestOnly
292   public void startup(@Nullable Consumer<Boolean> notifier) throws IOException {
293     myTestNotifier = notifier;
294     for (PluggableFileWatcher watcher : myWatchers) {
295       watcher.startup();
296     }
297   }
298
299   @TestOnly
300   public void shutdown() throws InterruptedException {
301     for (PluggableFileWatcher watcher : myWatchers) {
302       watcher.shutdown();
303     }
304     myTestNotifier = null;
305   }
306 }