lst: disable markers in .gitignored files
[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       if (!roots.isEmpty()) {
188         myManualWatchRoots.add(ContainerUtil.newHashSet(roots));
189       }
190       notifyOnAnyEvent();
191     }
192
193     @Override
194     public void notifyMapping(@NotNull Collection<Pair<String, String>> mapping) {
195       if (!mapping.isEmpty()) {
196         myPathMap.addMapping(mapping);
197       }
198       notifyOnAnyEvent();
199     }
200
201     @Override
202     public void notifyDirtyPath(@NotNull String path) {
203       Collection<String> paths = myPathMap.getWatchedPaths(path, true);
204       if (!paths.isEmpty()) {
205         synchronized (myLock) {
206           for (String eachPath : paths) {
207             myDirtyPaths.addDirtyPath(eachPath);
208           }
209         }
210       }
211       notifyOnAnyEvent();
212     }
213
214     @Override
215     public void notifyPathCreatedOrDeleted(@NotNull String path) {
216       Collection<String> paths = myPathMap.getWatchedPaths(path, true);
217       if (!paths.isEmpty()) {
218         synchronized (myLock) {
219           for (String p : paths) {
220             myDirtyPaths.addDirtyPathRecursive(p);
221             String parentPath = new File(p).getParent();
222             if (parentPath != null) {
223               myDirtyPaths.addDirtyPath(parentPath);
224             }
225           }
226         }
227       }
228       notifyOnAnyEvent();
229     }
230
231     @Override
232     public void notifyDirtyDirectory(@NotNull String path) {
233       Collection<String> paths = myPathMap.getWatchedPaths(path, false);
234       if (!paths.isEmpty()) {
235         synchronized (myLock) {
236           myDirtyPaths.dirtyDirectories.addAll(paths);
237         }
238       }
239       notifyOnAnyEvent();
240     }
241
242     @Override
243     public void notifyDirtyPathRecursive(@NotNull String path) {
244       Collection<String> paths = myPathMap.getWatchedPaths(path, false);
245       if (!paths.isEmpty()) {
246         synchronized (myLock) {
247           for (String each : paths) {
248             myDirtyPaths.addDirtyPathRecursive(each);
249           }
250         }
251       }
252       notifyOnAnyEvent();
253     }
254
255     @Override
256     public void notifyReset(@Nullable String path) {
257       if (path != null) {
258         synchronized (myLock) {
259           myDirtyPaths.addDirtyPathRecursive(path);
260         }
261       }
262       else {
263         VirtualFile[] roots = myManagingFS.getLocalRoots();
264         synchronized (myLock) {
265           for (VirtualFile root : roots) {
266             myDirtyPaths.addDirtyPathRecursive(root.getPresentableUrl());
267           }
268         }
269       }
270       notifyOnReset();
271     }
272
273     @Override
274     public void notifyUserOnFailure(@NotNull String cause, @Nullable NotificationListener listener) {
275       notifyOnFailure(cause, listener);
276     }
277   }
278
279   /* test data and methods */
280
281   private volatile Consumer<Boolean> myTestNotifier = null;
282
283   private void notifyOnAnyEvent() {
284     Consumer<Boolean> notifier = myTestNotifier;
285     if (notifier != null) notifier.accept(Boolean.FALSE);
286   }
287
288   private void notifyOnReset() {
289     Consumer<Boolean> notifier = myTestNotifier;
290     if (notifier != null) notifier.accept(Boolean.TRUE);
291   }
292
293   @TestOnly
294   public void startup(@Nullable Consumer<Boolean> notifier) throws IOException {
295     myTestNotifier = notifier;
296     for (PluggableFileWatcher watcher : myWatchers) {
297       watcher.startup();
298     }
299   }
300
301   @TestOnly
302   public void shutdown() throws InterruptedException {
303     for (PluggableFileWatcher watcher : myWatchers) {
304       watcher.shutdown();
305     }
306     myTestNotifier = null;
307   }
308 }