fix deadlock in ManagingFS initialization (PY-1215)
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / impl / local / FileWatcher.java
1 /*
2  * Copyright 2000-2009 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
17 /*
18  * @author max
19  */
20 package com.intellij.openapi.vfs.impl.local;
21
22 import com.intellij.ide.BrowserUtil;
23 import com.intellij.notification.Notification;
24 import com.intellij.notification.NotificationListener;
25 import com.intellij.notification.NotificationType;
26 import com.intellij.notification.Notifications;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.application.PathManager;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.util.Pair;
31 import com.intellij.openapi.util.SystemInfo;
32 import com.intellij.openapi.util.io.FileUtil;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.openapi.vfs.newvfs.ManagingFS;
35 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
36 import com.intellij.openapi.vfs.watcher.ChangeKind;
37 import org.jetbrains.annotations.NonNls;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import javax.swing.event.HyperlinkEvent;
42 import java.io.*;
43 import java.util.*;
44
45 public class FileWatcher {
46   @NonNls public static final String PROPERTY_WATCHER_DISABLED = "filewatcher.disabled";
47   @NonNls private static final String PROPERTY_WATCHER_EXECUTABLE_PATH = "idea.filewatcher.executable.path";
48
49   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.local.FileWatcher");
50
51   @NonNls private static final String GIVEUP_COMMAND = "GIVEUP";
52   @NonNls private static final String RESET_COMMAND = "RESET";
53   @NonNls private static final String UNWATCHEABLE_COMMAND = "UNWATCHEABLE";
54   @NonNls private static final String ROOTS_COMMAND = "ROOTS";
55   @NonNls private static final String REMAP_COMMAND = "REMAP";
56   @NonNls private static final String EXIT_COMMAND = "EXIT";
57   @NonNls private static final String MESSAGE_COMMAND = "MESSAGE";
58
59   private final Object LOCK = new Object();
60   private List<String> myDirtyPaths = new ArrayList<String>();
61   private List<String> myDirtyRecursivePaths = new ArrayList<String>();
62   private List<String> myDirtyDirs = new ArrayList<String>();
63   private List<String> myManualWatchRoots = new ArrayList<String>();
64   private final List<Pair<String, String>> myMapping = new ArrayList<Pair<String, String>>();
65
66   private List<String> myRecursiveWatchRoots = new ArrayList<String>();
67   private List<String> myFlatWatchRoots = new ArrayList<String>();
68
69   private Process notifierProcess;
70   private BufferedReader notifierReader;
71   private BufferedWriter notifierWriter;
72
73   private static final FileWatcher ourInstance = new FileWatcher();
74   private int attemptCount = 0;
75   private static final int MAX_PROCESS_LAUNCH_ATTEMPT_COUNT = 10;
76   private boolean isShuttingDown = false;
77   private final ManagingFS myManagingFS;
78
79   public static FileWatcher getInstance() {
80     return ourInstance;
81   }
82
83   private FileWatcher() {
84     // to avoid deadlock (PY-1215), initialize ManagingFS reference in main thread, not in filewatcher thread
85     myManagingFS = ManagingFS.getInstance();
86     try {
87       if (!"true".equals(System.getProperty(PROPERTY_WATCHER_DISABLED))) {
88         startupProcess();
89       }
90     }
91     catch (IOException e) {
92       // Ignore
93     }
94
95     if (notifierProcess != null) {
96       LOG.info("Native file watcher is operational.");
97       new WatchForChangesThread().start();
98
99       Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
100         public void run() {
101           isShuttingDown = true;
102           shutdownProcess();
103         }
104       }, "FileWatcher shutdown hook"));
105     }
106     else {
107       LOG.info("Native file watcher failed to startup.");
108     }
109   }
110
111   public List<String> getDirtyPaths() {
112     synchronized (LOCK) {
113       final List<String> result = myDirtyPaths;
114       myDirtyPaths = new ArrayList<String>();
115       return result;
116     }
117   }
118
119   public List<String> getDirtyRecursivePaths() {
120     synchronized (LOCK) {
121       final List<String> result = myDirtyRecursivePaths;
122       myDirtyRecursivePaths = new ArrayList<String>();
123       return result;
124     }
125
126   }
127
128   public List<String> getDirtyDirs() {
129     synchronized (LOCK) {
130       final List<String> result = myDirtyDirs;
131       myDirtyDirs = new ArrayList<String>();
132       return result;
133     }
134   }
135
136   public List<String> getManualWatchRoots() {
137     synchronized (LOCK) {
138       return Collections.unmodifiableList(myManualWatchRoots);
139     }
140   }
141
142   public void setWatchRoots(List<String> recursive, List<String> flat) {
143     synchronized (LOCK) {
144       try {
145         if (myRecursiveWatchRoots.equals(recursive) && myFlatWatchRoots.equals(flat)) return;
146
147         myRecursiveWatchRoots = recursive;
148         myFlatWatchRoots = flat;
149
150         if (isAlive()) {
151           writeLine(ROOTS_COMMAND);
152           myMapping.clear();
153
154           for (String path : recursive) {
155             writeLine(path);
156           }
157           for (String path : flat) {
158             writeLine("|" + path);
159           }
160           writeLine("#");
161         }
162       }
163       catch (IOException e) {
164         LOG.error(e);
165       }
166     }
167   }
168
169   private boolean isAlive() {
170     if (!isOperational()) return false;
171
172     try {
173       notifierProcess.exitValue();
174     }
175     catch (IllegalThreadStateException e) {
176       return true;
177     }
178
179     return false;
180   }
181
182   private void setManualWatchRoots(List<String> roots) {
183     synchronized (LOCK) {
184       myManualWatchRoots = roots;
185     }
186   }
187
188   private void startupProcess() throws IOException {
189     if (isShuttingDown) return;
190
191     if (attemptCount++ > MAX_PROCESS_LAUNCH_ATTEMPT_COUNT) {
192       throw new IOException("Can't launch process anymore");
193     }
194
195     shutdownProcess();
196
197     @NonNls final String executableName = SystemInfo.isWindows ? "fsnotifier.exe" : "fsnotifier";
198
199     String alternatePathToFilewatcherExecutable = System.getProperty(PROPERTY_WATCHER_EXECUTABLE_PATH);
200     if (alternatePathToFilewatcherExecutable != null) {
201       if (!new File(alternatePathToFilewatcherExecutable).exists()) {
202         alternatePathToFilewatcherExecutable = null;
203       }
204     }
205     final String pathToExecutable = alternatePathToFilewatcherExecutable != null? FileUtil.toSystemDependentName(alternatePathToFilewatcherExecutable) : PathManager.getBinPath() + File.separatorChar + executableName;
206     notifierProcess = Runtime.getRuntime().exec(new String[]{pathToExecutable});
207     
208     notifierReader = new BufferedReader(new InputStreamReader(notifierProcess.getInputStream()));
209     notifierWriter = new BufferedWriter(new OutputStreamWriter(notifierProcess.getOutputStream()));
210   }
211
212   private void shutdownProcess() {
213     if (notifierProcess != null) {
214       if (isAlive()) {
215         try {
216           writeLine(EXIT_COMMAND);
217         }
218         catch (IOException e) {
219           // Do nothing
220         }
221       }
222
223       notifierProcess = null;
224       notifierReader = null;
225       notifierWriter = null;
226     }
227   }
228
229   public boolean isOperational() {
230     return notifierProcess != null;
231   }
232
233   private class WatchForChangesThread extends Thread {
234
235     public WatchForChangesThread() {
236       //noinspection HardCodedStringLiteral
237       super("WatchForChangesThread");
238     }
239
240     public void run() {
241       try {
242         while (true) {
243           if (ApplicationManager.getApplication().isDisposeInProgress() || notifierProcess == null || isShuttingDown) return;
244
245           final String command = readLine();
246           if (command == null) {
247             // Unexpected process exit, relaunch attempt
248             startupProcess();
249             continue;
250           }
251
252           if (GIVEUP_COMMAND.equals(command)) {
253             LOG.info("Filewatcher gives up to operate on this platform");
254             shutdownProcess();
255             return;
256           }
257
258           if (RESET_COMMAND.equals(command)) {
259             reset();
260           }
261           else if (UNWATCHEABLE_COMMAND.equals(command)) {
262             List<String> roots = new ArrayList<String>();
263             do {
264               final String path = readLine();
265               if (path == null || "#".equals(path)) break;
266               roots.add(path);
267             }
268             while (true);
269
270             setManualWatchRoots(roots);
271           }
272           else if (MESSAGE_COMMAND.equals(command)) {
273             final String message = readLine();
274             if (message == null) break;
275
276             Notifications.Bus.notify(
277               new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, "File Watcher", message, NotificationType.WARNING,
278                                new NotificationListener() {
279                                  public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
280                                    if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
281                                      BrowserUtil.launchBrowser(event.getURL().toExternalForm());
282                                    }
283                                  }
284                                }));
285           }
286           else if (REMAP_COMMAND.equals(command)) {
287             Set<Pair<String, String>> pairs = new HashSet<Pair<String, String>>();
288             do {
289               final String pathA = readLine();
290               if (pathA == null || "#".equals(pathA)) break;
291               final String pathB = readLine();
292               if (pathB == null || "#".equals(pathB)) break;
293
294               pairs.add(new Pair<String, String>(ensureEndsWithSlash(pathA), ensureEndsWithSlash(pathB)));
295             }
296             while (true);
297
298             myMapping.clear();
299             myMapping.addAll(pairs);
300           }
301           else {
302             String path = readLine();
303             if (path == null) {
304               // Unexpected process exit, relaunch attempt
305               startupProcess();
306               continue;
307             }
308
309             if (isWatcheable(path)) {
310               try {
311                 onPathChange(ChangeKind.valueOf(command), path);
312               }
313               catch (IllegalArgumentException e) {
314                 LOG.error("Illegal watcher command: " + command);
315               }
316             }
317             else if (LOG.isDebugEnabled()) {
318               LOG.debug("not watcheable, filtered: " + path);
319             }
320           }
321         }
322       }
323       catch (IOException e) {
324         reset();
325         shutdownProcess();
326         LOG.info("Watcher terminated and attempt to restart has failed. Exiting watching thread.", e);
327       }
328     }
329   }
330
331   private String ensureEndsWithSlash(String path) {
332     if (path.endsWith("/") || path.endsWith(File.separator)) return path;
333     return path + '/';
334   }
335
336   private void writeLine(String line) throws IOException {
337     try {
338       if (LOG.isDebugEnabled()) {
339         LOG.debug("to fsnotifier: " + line);
340       }
341       notifierWriter.write(line);
342       notifierWriter.newLine();
343       notifierWriter.flush();
344     }
345     catch (IOException e) {
346       try {
347         notifierProcess.exitValue();
348       }
349       catch (IllegalThreadStateException e1) {
350         throw e;
351       }
352
353       notifierProcess = null;
354       notifierWriter = null;
355       notifierReader = null;
356     }
357   }
358
359   @Nullable
360   private String readLine() throws IOException {
361     if (notifierReader == null) return null;
362
363     final String line = notifierReader.readLine();
364     if (LOG.isDebugEnabled()) {
365       LOG.debug("fsnotifier says: " + line);
366     }
367     return line;
368   }
369
370   private boolean isWatcheable(final String path) {
371     if (path == null) return false;
372
373     synchronized (LOCK) {
374       for (String root : myRecursiveWatchRoots) {
375         if (FileUtil.startsWith(path, root)) return true;
376       }
377
378       for (String root : myFlatWatchRoots) {
379         if (FileUtil.pathsEqual(path, root)) return true;
380         final File parentFile = new File(path).getParentFile();
381         if (parentFile != null && FileUtil.pathsEqual(parentFile.getPath(), root)) return true;
382       }
383     }
384
385     return false;
386   }
387
388   private void onPathChange(final ChangeKind changeKind, final String path) {
389     synchronized (LOCK) {
390       switch (changeKind) {
391         case STATS:
392         case CHANGE:
393           addPath(path, myDirtyPaths);
394           break;
395
396         case CREATE:
397         case DELETE:
398           final File parentFile = new File(path).getParentFile();
399           if (parentFile != null) {
400             addPath(parentFile.getPath(), myDirtyPaths);
401           }
402           else {
403             addPath(path, myDirtyPaths);
404           }
405           break;
406
407         case DIRTY:
408           addPath(path, myDirtyDirs);
409           break;
410
411         case RECDIRTY:
412           addPath(path, myDirtyRecursivePaths);
413           break;
414
415         case RESET:
416           reset();
417           break;
418       }
419     }
420   }
421
422   private void addPath(String path, List<String> list) {
423     list.add(path);
424
425     for (Pair<String, String> map : myMapping) {
426       if (FileUtil.startsWith(path, map.getFirst())) {
427         list.add(map.getSecond() + path.substring(map.getFirst().length()));
428       }
429       else if (FileUtil.startsWith(path, map.getSecond())) {
430         list.add(map.getFirst() + path.substring(map.getSecond().length()));
431       }
432     }
433   }
434
435   private void reset() {
436     synchronized (LOCK) {
437       myDirtyPaths.clear();
438       myDirtyDirs.clear();
439       myDirtyRecursivePaths.clear();
440
441       for (VirtualFile root : myManagingFS.getLocalRoots()) {
442         ((NewVirtualFile)root).markDirtyRecursively();
443       }
444     }
445   }
446 }