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