From bef2b36170590eaaccc214f977e7e875a2567f67 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Thu, 18 Oct 2012 13:41:04 +0200 Subject: [PATCH] FileWatcher operation fixes --- .../openapi/vfs/watcher/ChangeKind.java | 8 +- .../vfs/watcher/NotAvailableException.java | 8 +- .../ide/diff/VirtualFileDiffElement.java | 6 +- .../openapi/vfs/impl/local/FileWatcher.java | 696 +++++++++--------- .../vfs/impl/local/LocalFileSystemImpl.java | 25 +- .../vfs/newvfs/RefreshSessionImpl.java | 5 +- .../openapi/vfs/local/FileWatcherTest.java | 10 +- 7 files changed, 371 insertions(+), 387 deletions(-) diff --git a/platform/platform-api/src/com/intellij/openapi/vfs/watcher/ChangeKind.java b/platform/platform-api/src/com/intellij/openapi/vfs/watcher/ChangeKind.java index 8ce2a5e45739..5c4472447977 100644 --- a/platform/platform-api/src/com/intellij/openapi/vfs/watcher/ChangeKind.java +++ b/platform/platform-api/src/com/intellij/openapi/vfs/watcher/ChangeKind.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -/* - * @author max - */ package com.intellij.openapi.vfs.watcher; +/** @deprecated not an API (to remove in IDEA 13) */ +@SuppressWarnings("UnusedDeclaration") public enum ChangeKind { CREATE, DELETE, diff --git a/platform/platform-api/src/com/intellij/openapi/vfs/watcher/NotAvailableException.java b/platform/platform-api/src/com/intellij/openapi/vfs/watcher/NotAvailableException.java index 630d6f037525..dccecf9c7a11 100644 --- a/platform/platform-api/src/com/intellij/openapi/vfs/watcher/NotAvailableException.java +++ b/platform/platform-api/src/com/intellij/openapi/vfs/watcher/NotAvailableException.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 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. @@ -13,11 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.intellij.openapi.vfs.watcher; -/** - * @author ven - */ +/** @deprecated not an API (to remove in IDEA 13) */ +@SuppressWarnings("UnusedDeclaration") public class NotAvailableException extends RuntimeException { } diff --git a/platform/platform-impl/src/com/intellij/ide/diff/VirtualFileDiffElement.java b/platform/platform-impl/src/com/intellij/ide/diff/VirtualFileDiffElement.java index 6097f86c0353..d1247fb21d7d 100644 --- a/platform/platform-impl/src/com/intellij/ide/diff/VirtualFileDiffElement.java +++ b/platform/platform-impl/src/com/intellij/ide/diff/VirtualFileDiffElement.java @@ -33,8 +33,6 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.impl.local.FileWatcher; -import com.intellij.openapi.vfs.newvfs.NewVirtualFile; import com.intellij.util.PlatformIcons; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -289,9 +287,7 @@ public class VirtualFileDiffElement extends DiffElement { } }); } - if (!FileWatcher.getInstance().isWatched(virtualFile)) { - ((NewVirtualFile)virtualFile).markDirtyRecursively(); - } + virtualFile.refresh(true, true); } } diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java index 56376b28f9ba..402ca7e2a92d 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/FileWatcher.java @@ -31,7 +31,7 @@ import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.newvfs.ManagingFS; import com.intellij.openapi.vfs.newvfs.NewVirtualFile; -import com.intellij.openapi.vfs.watcher.ChangeKind; +import com.intellij.util.TimeoutUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -61,10 +61,13 @@ public class FileWatcher { @NonNls private static final String EXIT_COMMAND = "EXIT"; @NonNls private static final String MESSAGE_COMMAND = "MESSAGE"; - private static final int MAX_PROCESS_LAUNCH_ATTEMPT_COUNT = 10; - private static final int MAGIC_PROCESS_LAUNCH_ATTEMPT_COUNT = 88 * MAX_PROCESS_LAUNCH_ATTEMPT_COUNT; + private enum ChangeKind { + CREATE, DELETE, STATS, CHANGE, DIRTY, RECDIRTY + } - private final Object LOCK = new Object(); + private static final int MAX_PROCESS_LAUNCH_ATTEMPT_COUNT = 10; + + private final Object myLock = new Object(); private List myDirtyPaths = newArrayList(); private List myDirtyRecursivePaths = newArrayList(); @@ -78,180 +81,70 @@ public class FileWatcher { private final Collection myAllPaths = newArrayListWithExpectedSize(2); private final Collection myWatchedPaths = newArrayListWithExpectedSize(2); - private File executable; - private volatile Process notifierProcess; - private volatile BufferedReader notifierReader; - - private volatile BufferedWriter notifierWriter; - private boolean myFailureShownToTheUser = false; - private int attemptCount = 0; - private boolean isShuttingDown = false; - private final ManagingFS myManagingFS; - private static final FileWatcher ourInstance = new FileWatcher(); + private final File myExecutable; + private volatile Process myNotifierProcess; + private volatile BufferedReader myNotifierReader; + private volatile BufferedWriter myNotifierWriter; + + private volatile int myStartAttemptCount = 0; + private volatile boolean myIsShuttingDown = false; + private volatile boolean myFailureShownToTheUser = false; + + /** @deprecated use {@linkplain com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl#getFileWatcher()} (to remove in IDEA 13) */ public static FileWatcher getInstance() { - return ourInstance; + return ((LocalFileSystemImpl)LocalFileSystem.getInstance()).getFileWatcher(); } - private FileWatcher() { - // to avoid deadlock (PY-1215), initialize ManagingFS reference in main thread, not in FileWatcher thread - myManagingFS = ManagingFS.getInstance(); + FileWatcher(@NotNull final ManagingFS managingFS) { + myManagingFS = managingFS; - final boolean explicitlyDisabled = Boolean.parseBoolean(System.getProperty(PROPERTY_WATCHER_DISABLED)); - try { - if (!explicitlyDisabled) { - startupProcess(false); - } - } - catch (IOException e) { - LOG.warn(e.getMessage()); - } + final boolean disabled = Boolean.parseBoolean(System.getProperty(PROPERTY_WATCHER_DISABLED)); + myExecutable = getExecutable(); - if (notifierProcess != null) { - LOG.info("Native file watcher is operational."); - //noinspection CallToThreadStartDuringObjectConstruction - new WatchForChangesThread().start(); - - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { - @Override - public void run() { - isShuttingDown = true; - shutdownProcess(); - } - }, "FileWatcher shutdown hook")); + if (disabled) { + LOG.info("Native file watcher is disabled"); } - else { - String message = explicitlyDisabled ? String.format("File watcher is disabled ('%s' property is set)", PROPERTY_WATCHER_DISABLED) - : "File watcher failed to startup"; - LOG.info(message); + else if (myExecutable == null) { + LOG.info("Native file watcher is not supported on this platform"); + } + else if (!myExecutable.exists()) { + final String message = "Native file watcher executable not found"; notifyOnFailure(message, null); } - } - - public List getDirtyPaths() { - synchronized (LOCK) { - final List result = myDirtyPaths; - myDirtyPaths = new ArrayList(); - return result; - } - } - - public List getDirtyRecursivePaths() { - synchronized (LOCK) { - final List result = myDirtyRecursivePaths; - myDirtyRecursivePaths = new ArrayList(); - return result; - } - } - - public List getDirtyDirs() { - synchronized (LOCK) { - final List result = myDirtyDirs; - myDirtyDirs = new ArrayList(); - return result; - } - } - - public List getManualWatchRoots() { - synchronized (LOCK) { - return Collections.unmodifiableList(myManualWatchRoots); - } - } - - public void setWatchRoots(final List recursive, final List flat) { - synchronized (LOCK) { - if (myRecursiveWatchRoots.equals(recursive) && myFlatWatchRoots.equals(flat)) return; - - if (isAlive()) { - try { - writeLine(ROOTS_COMMAND); - for (String path : recursive) { - writeLine(path); - } - for (String path : flat) { - writeLine("|" + path); - } - writeLine("#"); - } - catch (IOException e) { - LOG.error(e); + else if (!myExecutable.canExecute()) { + final String message = "Native file watcher is not executable: " + myExecutable + ""; + notifyOnFailure(message, new NotificationListener() { + @Override + public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { + ShowFilePathAction.openFile(myExecutable); } + }); + } + else { + try { + startupProcess(false); + } + catch (IOException e) { + LOG.warn(e.getMessage()); } - myRecursiveWatchRoots = recursive; - myFlatWatchRoots = flat; - myMapping.clear(); - } - } - - private boolean isAlive() { - if (!isOperational()) return false; - - try { - final Process process = notifierProcess; - if (process != null) { - process.exitValue(); + if (myNotifierProcess != null) { + LOG.info("Native file watcher is operational."); + //noinspection CallToThreadStartDuringObjectConstruction + new WatchForChangesThread().start(); + } + else { + final String message = "File watcher failed to startup"; + notifyOnFailure(message, null); } } - catch (IllegalThreadStateException e) { - return true; - } - - return false; } - @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) - private void startupProcess(final boolean restart) throws IOException { - if (isShuttingDown) return; - - if (attemptCount++ > MAX_PROCESS_LAUNCH_ATTEMPT_COUNT) { - notifyOnFailure("File watcher cannot be started", null); - throw new IOException("Can't launch process anymore"); - } - + public void dispose() { + myIsShuttingDown = true; shutdownProcess(); - - if (executable == null) { - executable = getExecutable(); - - if (executable == null) { - myFailureShownToTheUser = true; // ignore unsupported platforms - return; - } - - if (!executable.exists()) { - notifyOnFailure("File watcher is not found at path: " + executable, null); - return; - } - - if (!executable.canExecute()) { - final String message = "File watcher is not executable: " + executable + ""; - final File exec = executable; - notifyOnFailure(message, new NotificationListener() { - @Override - public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { - ShowFilePathAction.openFile(exec); - } - }); - return; - } - } - - LOG.info("Starting file watcher: " + executable); - notifierProcess = Runtime.getRuntime().exec(new String[]{executable.getAbsolutePath()}); - notifierReader = new BufferedReader(new InputStreamReader(notifierProcess.getInputStream())); - notifierWriter = new BufferedWriter(new OutputStreamWriter(notifierProcess.getOutputStream())); - - synchronized (LOCK) { - if (restart && myRecursiveWatchRoots.size() + myFlatWatchRoots.size() > 0) { - final List recursiveWatchRoots = new ArrayList(myRecursiveWatchRoots); - final List flatWatchRoots = new ArrayList(myFlatWatchRoots); - myRecursiveWatchRoots.clear(); - myFlatWatchRoots.clear(); - setWatchRoots(recursiveWatchRoots, flatWatchRoots); - } - } } @Nullable @@ -284,92 +177,177 @@ public class FileWatcher { @Nullable private static String getExecutableName(final boolean withSubDir) { - if (SystemInfo.isWindows) { - return (withSubDir ? "win" + File.separator : "") + "fsnotifier.exe"; - } - else if (SystemInfo.isMac) { - return (withSubDir ? "mac" + File.separator : "") + "fsnotifier"; - } - else if (SystemInfo.isLinux) { - return (withSubDir ? "linux" + File.separator : "") + (SystemInfo.isAMD64 ? "fsnotifier64" : "fsnotifier"); - } - + if (SystemInfo.isWindows) return (withSubDir ? "win" + File.separator : "") + "fsnotifier.exe"; + else if (SystemInfo.isMac) return (withSubDir ? "mac" + File.separator : "") + "fsnotifier"; + else if (SystemInfo.isLinux) return (withSubDir ? "linux" + File.separator : "") + (SystemInfo.isAMD64 ? "fsnotifier64" : "fsnotifier"); return null; } private void notifyOnFailure(String cause, @Nullable NotificationListener listener) { + LOG.warn(cause); + if (!myFailureShownToTheUser) { myFailureShownToTheUser = true; - Notifications.Bus.notify(new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, "External file sync may be slow", - cause, NotificationType.WARNING, listener)); + final Notification notification = new Notification( + Notifications.SYSTEM_MESSAGES_GROUP_ID, "External file sync may be slow", cause, NotificationType.WARNING, listener); + Notifications.Bus.notify(notification); + } + } + + @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) + private void startupProcess(final boolean restart) throws IOException { + if (myIsShuttingDown) return; + + if (myStartAttemptCount++ > MAX_PROCESS_LAUNCH_ATTEMPT_COUNT) { + notifyOnFailure("File watcher cannot be started", null); + throw new IOException("Can't launch process anymore"); + } + + if (restart) { + shutdownProcess(); + } + + LOG.info("Starting file watcher: " + myExecutable); + myNotifierProcess = Runtime.getRuntime().exec(new String[]{myExecutable.getAbsolutePath()}); // use array to allow spaces in path + myNotifierReader = new BufferedReader(new InputStreamReader(myNotifierProcess.getInputStream())); + myNotifierWriter = new BufferedWriter(new OutputStreamWriter(myNotifierProcess.getOutputStream())); + + if (restart) { + synchronized (myLock) { + if (myRecursiveWatchRoots.size() + myFlatWatchRoots.size() > 0) { + setWatchRoots(myRecursiveWatchRoots, myFlatWatchRoots, true); + } + } } } private void shutdownProcess() { - if (notifierProcess != null) { + if (myNotifierProcess != null) { if (isAlive()) { try { writeLine(EXIT_COMMAND); } catch (IOException ignore) { } } - - notifierProcess = null; - notifierReader = null; - notifierWriter = null; } + + myNotifierProcess = null; + myNotifierReader = null; + myNotifierWriter = null; + } + + private boolean isAlive() { + try { + final Process process = myNotifierProcess; + if (process != null) { + process.exitValue(); + } + } + catch (IllegalThreadStateException e) { + return true; + } + + return false; } public boolean isOperational() { - return notifierProcess != null; + return myNotifierProcess != null; } - @TestOnly - public static Logger getLog() { return LOG; } + public static class DirtyPaths { + public final List dirtyPaths; + public final List dirtyPathsRecursive; + public final List dirtyDirectories; - @TestOnly - public void startup(@Nullable final Runnable notifier) throws IOException { - final Application app = ApplicationManager.getApplication(); - assert app != null && app.isUnitTestMode() : app; + private DirtyPaths(List dirtyPaths, List dirtyPathsRecursive, List dirtyDirectories) { + this.dirtyPaths = dirtyPaths; + this.dirtyPathsRecursive = dirtyPathsRecursive; + this.dirtyDirectories = dirtyDirectories; + } + } - myFailureShownToTheUser = true; - attemptCount = 0; - startupProcess(false); - attemptCount = MAGIC_PROCESS_LAUNCH_ATTEMPT_COUNT; - if (notifierProcess != null) { - (myThread = new WatchForChangesThread()).start(); + public DirtyPaths getDirtyPaths() { + synchronized (myLock) { + final DirtyPaths dirtyPaths = new DirtyPaths(myDirtyPaths, myDirtyRecursivePaths, myDirtyDirs); + myDirtyPaths = new ArrayList(); + myDirtyRecursivePaths = new ArrayList(); + myDirtyDirs = new ArrayList(); + return dirtyPaths; + } + } + + public List getManualWatchRoots() { + synchronized (myLock) { + return Collections.unmodifiableList(myManualWatchRoots); + } + } + + public void setWatchRoots(final List recursive, final List flat) { + setWatchRoots(recursive, flat, false); + } + + private void setWatchRoots(List recursive, List flat, final boolean restart) { + if (!isAlive()) return; + + if (ApplicationManager.getApplication().isDisposeInProgress()) { + recursive = flat = Collections.emptyList(); } - myNotifier = notifier; - } - - @TestOnly - public void shutdown() throws InterruptedException { - final Application app = ApplicationManager.getApplication(); - assert app != null && app.isUnitTestMode() : app; - - myNotifier = null; - - final Process process = notifierProcess; - if (process != null) { - shutdownProcess(); - process.waitFor(); - if (myThread != null && myThread.isAlive()) { - myThread.join(10000); - assert !myThread.isAlive() : myThread; + synchronized (myLock) { + if (!restart && myRecursiveWatchRoots.equals(recursive) && myFlatWatchRoots.equals(flat)) { + return; } - myThread = null; + + myMapping.clear(); + + try { + writeLine(ROOTS_COMMAND); + for (String path : recursive) { + writeLine(path); + } + for (String path : flat) { + writeLine("|" + path); + } + writeLine("#"); + } + catch (IOException e) { + LOG.error(e); + shutdownProcess(); + } + + myRecursiveWatchRoots = recursive; + myFlatWatchRoots = flat; } } - private FileWatcher.WatchForChangesThread myThread = null; - private volatile Runnable myNotifier = null; - - private void notifyOnEvent() { - final Runnable notifier = myNotifier; - if (notifier != null) { - notifier.run(); + private void writeLine(final String line) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("<< " + line); } + + final BufferedWriter writer = myNotifierWriter; + if (writer != null) { + writer.write(line); + writer.newLine(); + writer.flush(); + } + } + + @NotNull + private String readLine() throws IOException { + final BufferedReader reader = myNotifierReader; + if (reader == null) { + throw new EOFException("Process terminated"); + } + + final String line = reader.readLine(); + if (LOG.isDebugEnabled()) { + LOG.debug(">> " + line); + } + if (line == null) { + throw new EOFException(); + } + return line; } private class WatchForChangesThread extends Thread { @@ -381,171 +359,127 @@ public class FileWatcher { public void run() { try { while (true) { - if (ApplicationManager.getApplication().isDisposeInProgress() || notifierProcess == null || isShuttingDown) return; - - final String command = readLine(); - if (command == null) { - if (attemptCount == MAGIC_PROCESS_LAUNCH_ATTEMPT_COUNT) { - LOG.debug("Leaving watcher thread"); - return; - } - - // Unexpected process exit, relaunch attempt - startupProcess(true); - continue; - } - - if (GIVE_UP_COMMAND.equals(command)) { - LOG.info("FileWatcher gives up to operate on this platform"); - shutdownProcess(); + if (myIsShuttingDown) { + LOG.info("Shutting down - leaving watcher thread"); return; } - if (RESET_COMMAND.equals(command)) { - reset(); + if (myNotifierProcess == null) { + TimeoutUtil.sleep(1000); + continue; } - else if (UNWATCHABLE_COMMAND.equals(command)) { - List roots = new ArrayList(); - do { + + try { + final String command = readLine(); + + if (GIVE_UP_COMMAND.equals(command)) { + LOG.info("Native file watcher gives up to operate on this platform"); + shutdownProcess(); + return; + } + else if (RESET_COMMAND.equals(command)) { + reset(); + } + else if (UNWATCHABLE_COMMAND.equals(command)) { + List roots = new ArrayList(); + do { + final String path = readLine(); + if (path == null || "#".equals(path)) break; + roots.add(path); + } + while (true); + + synchronized (myLock) { + myManualWatchRoots = roots; + } + + notifyOnEvent(); + } + else if (MESSAGE_COMMAND.equals(command)) { + final String message = readLine(); + Notifications.Bus.notify( + new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, "File Watcher", message, NotificationType.WARNING, + NotificationListener.URL_OPENING_LISTENER)); + } + else if (REMAP_COMMAND.equals(command)) { + Set> pairs = new HashSet>(); + do { + final String pathA = readLine(); + if (pathA == null || "#".equals(pathA)) break; + final String pathB = readLine(); + if (pathB == null || "#".equals(pathB)) break; + + pairs.add(Pair.create(preparePathForMapping(pathA), preparePathForMapping(pathB))); + } + while (true); + + synchronized (myLock) { + myMapping.clear(); + myMapping.addAll(pairs); + } + + notifyOnEvent(); + } + else { final String path = readLine(); - if (path == null || "#".equals(path)) break; - roots.add(path); - } - while (true); - synchronized (LOCK) { - myManualWatchRoots = roots; - } - - notifyOnEvent(); - } - else if (MESSAGE_COMMAND.equals(command)) { - final String message = readLine(); - if (message == null) break; - - Notifications.Bus.notify( - new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, "File Watcher", message, NotificationType.WARNING, - NotificationListener.URL_OPENING_LISTENER)); - } - else if (REMAP_COMMAND.equals(command)) { - Set> pairs = new HashSet>(); - do { - final String pathA = readLine(); - if (pathA == null || "#".equals(pathA)) break; - final String pathB = readLine(); - if (pathB == null || "#".equals(pathB)) break; - - pairs.add(Pair.create(preparePathForMapping(pathA), preparePathForMapping(pathB))); - } - while (true); - - synchronized (LOCK) { - myMapping.clear(); - myMapping.addAll(pairs); - } - - notifyOnEvent(); - } - else { - final String path = readLine(); - if (path == null) { - // Unexpected process exit, relaunch attempt - startupProcess(true); - continue; - } - - final ChangeKind kind; - try { - kind = ChangeKind.valueOf(command); - } - catch (IllegalArgumentException e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Illegal watcher command: " + command); + final ChangeKind kind; + try { + kind = ChangeKind.valueOf(command); } - else { - LOG.error("Illegal watcher command: " + command); - } - continue; - } - - synchronized (LOCK) { - if (isWindowsOverflow(path, kind)) { - resetRoot(path); + catch (IllegalArgumentException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Illegal watcher command: " + command); + } + else { + LOG.error("Illegal watcher command: " + command); + } continue; } - final Collection watchedPaths = checkWatchable(path, !(kind == ChangeKind.DIRTY || kind == ChangeKind.RECDIRTY)); - if (!watchedPaths.isEmpty()) { - onPathChange(kind, watchedPaths); - } - else if (LOG.isDebugEnabled()) { - LOG.debug("Not watchable, filtered: " + path); + + synchronized (myLock) { + if (isWindowsOverflow(path, kind)) { + resetRoot(path); + continue; + } + final Collection watchedPaths = checkWatchable(path, !(kind == ChangeKind.DIRTY || kind == ChangeKind.RECDIRTY)); + if (!watchedPaths.isEmpty()) { + onPathChange(kind, watchedPaths); + } + else if (LOG.isDebugEnabled()) { + LOG.debug("Not watchable, filtered: " + path); + } } } } + catch (IOException e) { + LOG.warn("Watcher terminated", e); + startupProcess(true); + } } } catch (IOException e) { - reset(); shutdownProcess(); - LOG.info("Watcher terminated and attempt to restart has failed. Exiting watching thread.", e); + LOG.warn("Watcher terminated and attempt to restart has failed. Exiting watching thread.", e); + } + catch (Throwable t) { + shutdownProcess(); + LOG.error("Watcher thread stopped unexpectedly", t); } finally { LOG.debug("Watcher thread finished"); } } - } - private static String preparePathForMapping(final String path) { - final String localPath = FileUtil.toSystemDependentName(path); - return localPath.endsWith(File.separator) ? localPath : localPath + File.separator; - } - - private void writeLine(String line) throws IOException { - if (LOG.isDebugEnabled()) { - LOG.debug("<< " + line); + private String preparePathForMapping(final String path) { + final String localPath = FileUtil.toSystemDependentName(path); + return localPath.endsWith(File.separator) ? localPath : localPath + File.separator; } - - final Process process = notifierProcess; - final BufferedWriter writer = notifierWriter; - try { - if (writer != null) { - writer.write(line); - writer.newLine(); - writer.flush(); - } - } - catch (IOException e) { - try { - if (process != null) { - process.exitValue(); - } - } - catch (IllegalThreadStateException e1) { - throw e; - } - finally { - notifierProcess = null; - notifierWriter = null; - notifierReader = null; - } - } - } - - @Nullable - private String readLine() throws IOException { - final BufferedReader reader = notifierReader; - if (reader == null) return null; - - final String line = reader.readLine(); - if (LOG.isDebugEnabled()) { - LOG.debug(">> " + line); - } - return line; } public boolean isWatched(@NotNull final VirtualFile file) { if (isOperational()) { - synchronized (LOCK) { + synchronized (myLock) { return !checkWatchable(file.getPresentableUrl(), true).isEmpty(); } } @@ -616,17 +550,13 @@ public class FileWatcher { case RECDIRTY: myDirtyRecursivePaths.addAll(paths); break; - - case RESET: - reset(); - break; } notifyOnEvent(); } private void reset() { - synchronized (LOCK) { + synchronized (myLock) { myDirtyPaths.clear(); myDirtyDirs.clear(); myDirtyRecursivePaths.clear(); @@ -651,4 +581,54 @@ public class FileWatcher { notifyOnEvent(); } + + /* test data and methods */ + + private FileWatcher.WatchForChangesThread myThread = null; + private volatile Runnable myNotifier = null; + + @TestOnly + public static Logger getLog() { return LOG; } + + @TestOnly + public void startup(@Nullable final Runnable notifier) throws IOException { + final Application app = ApplicationManager.getApplication(); + assert app != null && app.isUnitTestMode() : app; + + myIsShuttingDown = false; + myStartAttemptCount = 0; + startupProcess(false); + if (myNotifierProcess != null) { + (myThread = new WatchForChangesThread()).start(); + } + + myNotifier = notifier; + } + + @TestOnly + public void shutdown() throws InterruptedException { + final Application app = ApplicationManager.getApplication(); + assert app != null && app.isUnitTestMode() : app; + + myNotifier = null; + + final Process process = myNotifierProcess; + if (process != null) { + myIsShuttingDown = true; + shutdownProcess(); + process.waitFor(); + if (myThread != null && myThread.isAlive()) { + myThread.join(10000); + assert !myThread.isAlive() : myThread; + } + myThread = null; + } + } + + private void notifyOnEvent() { + final Runnable notifier = myNotifier; + if (notifier != null) { + notifier.run(); + } + } } diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java index 740006be1d55..4a38c34f6124 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/impl/local/LocalFileSystemImpl.java @@ -47,6 +47,7 @@ public final class LocalFileSystemImpl extends LocalFileSystemBase implements Ap private final Object myLock = new Object(); private final List myRootsToWatch = new ArrayList(); private TreeNode myNormalizedTree = null; + private final ManagingFS myManagingFS; private final FileWatcher myWatcher; private static class WatchRequestImpl implements WatchRequest { @@ -109,19 +110,26 @@ public final class LocalFileSystemImpl extends LocalFileSystemBase implements Ap private Map nodes = new HashMap(); } - public LocalFileSystemImpl() { - myWatcher = FileWatcher.getInstance(); + public LocalFileSystemImpl(@NotNull ManagingFS managingFS) { + myManagingFS = managingFS; + myWatcher = new FileWatcher(myManagingFS); if (myWatcher.isOperational()) { new StoreRefreshStatusThread().start(); } } + @NotNull + public FileWatcher getFileWatcher() { + return myWatcher; + } + @Override public void initComponent() { } @Override public void disposeComponent() { + myWatcher.dispose(); } @Override @@ -140,7 +148,7 @@ public final class LocalFileSystemImpl extends LocalFileSystemBase implements Ap }); PersistentFS.getInstance().clearIdCache(); - for (VirtualFile root : ManagingFS.getInstance().getRoots(this)) { + for (VirtualFile root : myManagingFS.getRoots(this)) { if (root instanceof VirtualDirectoryImpl) { ((VirtualDirectoryImpl)root).cleanupCachedChildren(survivors); } @@ -263,9 +271,10 @@ public final class LocalFileSystemImpl extends LocalFileSystemBase implements Ap private void storeRefreshStatusToFiles() { if (myWatcher.isOperational()) { // TODO: different ways to mark dirty for all these cases - markPathsDirty(myWatcher.getDirtyPaths()); - markFlatDirsDirty(myWatcher.getDirtyDirs()); - markRecursiveDirsDirty(myWatcher.getDirtyRecursivePaths()); + final FileWatcher.DirtyPaths dirtyPaths = myWatcher.getDirtyPaths(); + markPathsDirty(dirtyPaths.dirtyPaths); + markFlatDirsDirty(dirtyPaths.dirtyDirectories); + markRecursiveDirsDirty(dirtyPaths.dirtyPathsRecursive); } } @@ -515,7 +524,7 @@ public final class LocalFileSystemImpl extends LocalFileSystemBase implements Ap Runnable heavyRefresh = new Runnable() { @Override public void run() { - for (VirtualFile root : ManagingFS.getInstance().getRoots(LocalFileSystemImpl.this)) { + for (VirtualFile root : myManagingFS.getRoots(LocalFileSystemImpl.this)) { ((NewVirtualFile)root).markDirtyRecursively(); } @@ -524,7 +533,7 @@ public final class LocalFileSystemImpl extends LocalFileSystemBase implements Ap }; if (asynchronous && myWatcher.isOperational()) { - RefreshQueue.getInstance().refresh(true, true, heavyRefresh, ManagingFS.getInstance().getRoots(this)); + RefreshQueue.getInstance().refresh(true, true, heavyRefresh, myManagingFS.getRoots(this)); } else { heavyRefresh.run(); diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java index fe12a1f3f21d..a5f332e81721 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/RefreshSessionImpl.java @@ -94,9 +94,10 @@ public class RefreshSessionImpl extends RefreshSession { boolean hasEventsToFire = myFinishRunnable != null || !myEvents.isEmpty(); if (!workQueue.isEmpty()) { - ((LocalFileSystemImpl)LocalFileSystem.getInstance()).markSuspiciousFilesDirty(workQueue); + final LocalFileSystemImpl fs = (LocalFileSystemImpl)LocalFileSystem.getInstance(); + fs.markSuspiciousFilesDirty(workQueue); + final FileWatcher watcher = fs.getFileWatcher(); - final FileWatcher watcher = FileWatcher.getInstance(); for (VirtualFile file : workQueue) { final NewVirtualFile nvf = (NewVirtualFile)file; if (!myIsRecursive && (!myIsAsync || !watcher.isWatched(nvf))) { // We're unable to definitely refresh synchronously by means of file watcher. diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/FileWatcherTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/FileWatcherTest.java index 3ae619cf11f7..fd0a2b176ab6 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/FileWatcherTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/FileWatcherTest.java @@ -24,6 +24,7 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.IoTestUtil; import com.intellij.openapi.vfs.*; import com.intellij.openapi.vfs.impl.local.FileWatcher; +import com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl; import com.intellij.openapi.vfs.newvfs.BulkFileListener; import com.intellij.openapi.vfs.newvfs.NewVirtualFile; import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; @@ -83,15 +84,16 @@ public class FileWatcherTest extends PlatformLangTestCase { super.setUp(); - myAlarm = new Alarm(Alarm.ThreadToUse.OWN_THREAD, getProject()); - myWatcher = FileWatcher.getInstance(); + myFileSystem = LocalFileSystem.getInstance(); + assertNotNull(myFileSystem); + + myWatcher = ((LocalFileSystemImpl)myFileSystem).getFileWatcher(); assertNotNull(myWatcher); assertFalse(myWatcher.isOperational()); myWatcher.startup(myNotifier); assertTrue(myWatcher.isOperational()); - myFileSystem = LocalFileSystem.getInstance(); - assertNotNull(myFileSystem); + myAlarm = new Alarm(Alarm.ThreadToUse.OWN_THREAD, getProject()); myConnection = ApplicationManager.getApplication().getMessageBus().connect(); myConnection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener.Adapter() {