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 e11872c5a048..62b8a1a7ddf5 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 @@ -20,34 +20,32 @@ import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.notification.Notifications; +import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.SystemInfo; -import com.intellij.openapi.util.io.FileSystemUtil; import com.intellij.openapi.util.io.FileUtil; 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.PairFunction; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; import javax.swing.event.HyperlinkEvent; import java.io.*; import java.util.*; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * @author max */ public class FileWatcher { - @NonNls public static final String PROPERTY_WATCHER_DISABLED = "filewatcher.disabled"; - @NonNls private static final String PROPERTY_WATCHER_EXECUTABLE_PATH = "idea.filewatcher.executable.path"; + @NonNls public static final String PROPERTY_WATCHER_DISABLED = "idea.filewatcher.disabled"; + @NonNls public static final String PROPERTY_WATCHER_EXECUTABLE_PATH = "idea.filewatcher.executable.path"; private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.local.FileWatcher"); @@ -59,15 +57,7 @@ public class FileWatcher { @NonNls private static final String EXIT_COMMAND = "EXIT"; @NonNls private static final String MESSAGE_COMMAND = "MESSAGE"; - private static final PairFunction PATH_COMPARATOR = new PairFunction() { - @Override - public Boolean fun(final String s1, final String s2) { - return SystemInfo.isFileSystemCaseSensitive ? s1.equals(s2) : s1.equalsIgnoreCase(s2); - } - }; - private final Object LOCK = new Object(); - private final Lock SET_ROOTS_LOCK = new ReentrantLock(true); private List myDirtyPaths = new ArrayList(); private List myDirtyRecursivePaths = new ArrayList(); @@ -75,21 +65,23 @@ public class FileWatcher { private List myManualWatchRoots = new ArrayList(); private final List> myMapping = new ArrayList>(); - private List> myCanonicalMapping = new ArrayList>(); - private List myRecursiveWatchRoots = new ArrayList(); private List myFlatWatchRoots = new ArrayList(); + private final Collection myAllPaths = new ArrayList(2); + private final Collection myWatchedPaths = new ArrayList(2); + + private File executable; private volatile Process notifierProcess; private volatile BufferedReader notifierReader; - private volatile BufferedWriter notifierWriter; + private volatile BufferedWriter notifierWriter; private boolean myFailureShownToTheUser = false; private int attemptCount = 0; private static final int MAX_PROCESS_LAUNCH_ATTEMPT_COUNT = 10; private boolean isShuttingDown = false; - private final ManagingFS myManagingFS; + private final ManagingFS myManagingFS; private static final FileWatcher ourInstance = new FileWatcher(); public static FileWatcher getInstance() { @@ -140,7 +132,6 @@ public class FileWatcher { myDirtyRecursivePaths = new ArrayList(); return result; } - } public List getDirtyDirs() { @@ -158,26 +149,16 @@ public class FileWatcher { } public void setWatchRoots(final List recursive, final List flat) { - SET_ROOTS_LOCK.lock(); - try { - synchronized (LOCK) { - if (myRecursiveWatchRoots.equals(recursive) && myFlatWatchRoots.equals(flat)) return; - } - - final List> mapping = new ArrayList>(); - long t = System.nanoTime(); - final List checkedRecursive = checkPaths(recursive, mapping); - final List checkedFlat = checkPaths(flat, mapping); - t = (System.nanoTime() - t) / 1000; - LOG.info((recursive.size() + flat.size()) + " paths checked, " + mapping.size() + " mapped, " + t + " mks"); + synchronized (LOCK) { + if (myRecursiveWatchRoots.equals(recursive) && myFlatWatchRoots.equals(flat)) return; if (isAlive()) { try { writeLine(ROOTS_COMMAND); - for (String path : checkedRecursive) { + for (String path : recursive) { writeLine(path); } - for (String path : checkedFlat) { + for (String path : flat) { writeLine("|" + path); } writeLine("#"); @@ -187,37 +168,10 @@ public class FileWatcher { } } - synchronized (LOCK) { - myRecursiveWatchRoots = recursive; - myFlatWatchRoots = flat; - myMapping.clear(); - myCanonicalMapping = mapping; - } + myRecursiveWatchRoots = recursive; + myFlatWatchRoots = flat; + myMapping.clear(); } - finally { - SET_ROOTS_LOCK.unlock(); - } - } - - private static List checkPaths(final List paths, final List> mapping) { - if (!SystemInfo.areSymLinksSupported) return paths; - - final List checkedPaths = new ArrayList(paths.size()); - for (String path : paths) { - String watched = path; - final String canonical = getCanonicalPath(path); - //noinspection ConstantConditions - if (!PATH_COMPARATOR.fun(path, canonical)) { - mapping.add(Pair.create((watched = canonical), path)); - } - checkedPaths.add(watched); - } - return checkedPaths; - } - - private static String getCanonicalPath(final String path) { - final String realPath = FileSystemUtil.resolveSymLink(path); - return realPath != null ? realPath : path; } private boolean isAlive() { @@ -247,43 +201,34 @@ public class FileWatcher { shutdownProcess(); - String execPath = null; + if (executable == null) { + executable = getExecutable(); - final String altExecPath = System.getProperty(PROPERTY_WATCHER_EXECUTABLE_PATH); - if (altExecPath != null && new File(altExecPath).isFile()) { - execPath = FileUtil.toSystemDependentName(altExecPath); - } - - if (execPath == null) { - final String execName; - execName = getExecutableName(); - if (execName == null) { + if (executable == null) { myFailureShownToTheUser = true; // ignore unsupported platforms return; } - execPath = PathManager.getBinPath() + File.separatorChar + execName; + + 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; + } } - final File exec = new File(execPath); - if (!exec.exists()) { - notifyOnFailure("File watcher is not found at path: " + execPath, null); - return; - } - - if (!exec.canExecute()) { - notifyOnFailure("File watcher is not executable: " + execPath +"", new NotificationListener() { - @Override - public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { - ShowFilePathAction.openFile(exec); - } - }); - return; - } - - LOG.info("Starting file watcher: " + execPath); - - notifierProcess = Runtime.getRuntime().exec(new String[]{execPath}); - + LOG.info("Starting file watcher: " + executable); + notifierProcess = Runtime.getRuntime().exec(executable.getAbsolutePath()); notifierReader = new BufferedReader(new InputStreamReader(notifierProcess.getInputStream())); notifierWriter = new BufferedWriter(new OutputStreamWriter(notifierProcess.getOutputStream())); @@ -299,15 +244,43 @@ public class FileWatcher { } @Nullable - private static String getExecutableName() { + private static File getExecutable() { + String execPath = null; + + final String altExecPath = System.getProperty(PROPERTY_WATCHER_EXECUTABLE_PATH); + if (altExecPath != null && new File(altExecPath).isFile()) { + execPath = FileUtil.toSystemDependentName(altExecPath); + } + + if (execPath == null) { + final String execName = getExecutableName(false); + if (execName == null) { + return null; + } + execPath = FileUtil.join(PathManager.getBinPath(), execName); + } + + File exec = new File(execPath); + if (!exec.exists()) { + String homePath = PathManager.getHomePath(); + if (new File(homePath, "community").exists()) { + homePath += File.separator + "community"; + } + exec = new File(FileUtil.join(homePath, "bin", getExecutableName(true))); + } + return exec; + } + + @Nullable + private static String getExecutableName(final boolean withSubDir) { if (SystemInfo.isWindows) { - return "fsnotifier.exe"; + return (withSubDir ? "win" + File.separator : "") + "fsnotifier.exe"; } else if (SystemInfo.isMac) { - return "fsnotifier"; + return (withSubDir ? "mac" + File.separator : "") + "fsnotifier"; } else if (SystemInfo.isLinux) { - return SystemInfo.isAMD64 ? "fsnotifier64" : "fsnotifier"; + return (withSubDir ? "linux" + File.separator : "") + (SystemInfo.isAMD64 ? "fsnotifier64" : "fsnotifier"); } return null; @@ -316,7 +289,8 @@ public class FileWatcher { private void notifyOnFailure(String cause, @Nullable NotificationListener listener) { if (!myFailureShownToTheUser) { myFailureShownToTheUser = true; - Notifications.Bus.notify(new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, "External file sync may be slow", cause, NotificationType.WARNING, listener)); + Notifications.Bus.notify(new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID, "External file sync may be slow", + cause, NotificationType.WARNING, listener)); } } @@ -339,6 +313,32 @@ public class FileWatcher { return notifierProcess != null; } + @TestOnly + public void startup() throws IOException { + final Application app = ApplicationManager.getApplication(); + assert app != null && app.isUnitTestMode() : app; + + myFailureShownToTheUser = true; + attemptCount = 0; + startupProcess(false); + attemptCount = 2 * MAX_PROCESS_LAUNCH_ATTEMPT_COUNT; + if (notifierProcess != null) { + new WatchForChangesThread().start(); + } + } + + @TestOnly + public void shutdown() throws InterruptedException { + final Application app = ApplicationManager.getApplication(); + assert app != null && app.isUnitTestMode() : app; + + final Process process = notifierProcess; + if (process != null) { + shutdownProcess(); + process.waitFor(); + } + } + private class WatchForChangesThread extends Thread { public WatchForChangesThread() { //noinspection HardCodedStringLiteral @@ -396,7 +396,7 @@ public class FileWatcher { final String pathB = readLine(); if (pathB == null || "#".equals(pathB)) break; - pairs.add(new Pair(ensureEndsWithSlash(pathA), ensureEndsWithSlash(pathB))); + pairs.add(Pair.create(preparePathForMapping(pathA), preparePathForMapping(pathB))); } while (true); @@ -414,10 +414,10 @@ public class FileWatcher { } synchronized (LOCK) { - final String watchedPath = checkWatchable(path); - if (watchedPath != null) { + final Collection watchedPaths = checkWatchable(path); + if (!watchedPaths.isEmpty()) { try { - onPathChange(ChangeKind.valueOf(command), watchedPath); + onPathChange(ChangeKind.valueOf(command), watchedPaths); } catch (IllegalArgumentException e) { LOG.error("Illegal watcher command: " + command); @@ -438,9 +438,9 @@ public class FileWatcher { } } - private static String ensureEndsWithSlash(String path) { - if (path.endsWith("/") || path.endsWith(File.separator)) return path; - return path + '/'; + 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 { @@ -486,64 +486,76 @@ public class FileWatcher { return line; } - public boolean isWatched(VirtualFile file) { - return isOperational() && checkWatchable(file.getPresentableUrl()) != null; + public boolean isWatched(@NotNull final VirtualFile file) { + if (isOperational()) { + synchronized (LOCK) { + return !checkWatchable(file.getPresentableUrl()).isEmpty(); + } + } + return false; } - @Nullable - private String checkWatchable(String path) { - if (path == null) return null; + @NotNull + private Collection checkWatchable(final String reportedPath) { + if (reportedPath == null) return Collections.emptyList(); - for (Pair mapping : myCanonicalMapping) { - if (path.startsWith(mapping.first)) { - path = mapping.second + path.substring(mapping.first.length()); - break; + myAllPaths.clear(); + myAllPaths.add(reportedPath); + for (Pair map : myMapping) { + if (FileUtil.startsWith(reportedPath, map.first)) { + myAllPaths.add(map.second + reportedPath.substring(map.first.length())); + } + else if (FileUtil.startsWith(reportedPath, map.second)) { + myAllPaths.add(map.first + reportedPath.substring(map.second.length())); } } - for (String root : myRecursiveWatchRoots) { - if (FileUtil.startsWith(path, root)) { - return path; + myWatchedPaths.clear(); + ext: + for (String path : myAllPaths) { + for (String root : myRecursiveWatchRoots) { + if (FileUtil.startsWith(path, root)) { + myWatchedPaths.add(path); + continue ext; + } + } + + for (String root : myFlatWatchRoots) { + if (FileUtil.pathsEqual(path, root)) { + myWatchedPaths.add(path); + continue ext; + } + final File parentFile = new File(path).getParentFile(); + if (parentFile != null && FileUtil.pathsEqual(parentFile.getPath(), root)) { + myWatchedPaths.add(path); + continue ext; + } } } - - for (String root : myFlatWatchRoots) { - if (FileUtil.pathsEqual(path, root)) { - return path; - } - final File parentFile = new File(path).getParentFile(); - if (parentFile != null && FileUtil.pathsEqual(parentFile.getPath(), root)) { - return path; - } - } - - return null; + return myWatchedPaths; } - private void onPathChange(final ChangeKind changeKind, final String path) { + private void onPathChange(final ChangeKind changeKind, final Collection paths) { switch (changeKind) { case STATS: case CHANGE: - addPath(path, myDirtyPaths); + myDirtyPaths.addAll(paths); break; case CREATE: case DELETE: - final File parentFile = new File(path).getParentFile(); - if (parentFile != null) { - addPath(parentFile.getPath(), myDirtyPaths); - } - else { - addPath(path, myDirtyPaths); + for (String path : paths) { + final File parent = new File(path).getParentFile(); + myDirtyPaths.add(parent != null ? parent.getPath() : path); } break; case DIRTY: - addPath(path, myDirtyDirs); + myDirtyDirs.addAll(paths); break; case RECDIRTY: - addPath(path, myDirtyRecursivePaths); + myDirtyRecursivePaths.addAll(paths); break; case RESET: @@ -552,19 +564,6 @@ public class FileWatcher { } } - private void addPath(String path, List list) { - list.add(path); - - for (Pair map : myMapping) { - if (FileUtil.startsWith(path, map.getFirst())) { - list.add(map.getSecond() + path.substring(map.getFirst().length())); - } - else if (FileUtil.startsWith(path, map.getSecond())) { - list.add(map.getFirst() + path.substring(map.getSecond().length())); - } - } - } - private void reset() { synchronized (LOCK) { myDirtyPaths.clear(); 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 new file mode 100644 index 000000000000..074522040598 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/FileWatcherTest.java @@ -0,0 +1,438 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.vfs.local; + +import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.idea.Bombed; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.*; +import com.intellij.openapi.vfs.impl.local.FileWatcher; +import com.intellij.openapi.vfs.newvfs.BulkFileListener; +import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import com.intellij.testFramework.PlatformLangTestCase; +import com.intellij.util.Function; +import com.intellij.util.TimeoutUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.messages.MessageBusConnection; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.*; + +public class FileWatcherTest extends PlatformLangTestCase { + private static final int NATIVE_PROCESS_DELAY = 500; // time to event to be caught by native watcher and passed to watcher thread + + private FileWatcher myWatcher; + private LocalFileSystem myFileSystem; + private MessageBusConnection myConnection; + private final List myEvents = new ArrayList(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + myWatcher = FileWatcher.getInstance(); + assertNotNull(myWatcher); + assertFalse(myWatcher.isOperational()); + myWatcher.startup(); + assertTrue(myWatcher.isOperational()); + + myFileSystem = LocalFileSystem.getInstance(); + assertNotNull(myFileSystem); + + myConnection = ApplicationManager.getApplication().getMessageBus().connect(); + myConnection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() { + @Override + public void before(@NotNull List events) { + } + + @Override + public void after(@NotNull List events) { + synchronized (myEvents) { + myEvents.addAll(events); + myEvents.notifyAll(); + } + } + }); + } + + @Override + protected void tearDown() throws Exception { + try { + myConnection.disconnect(); + myWatcher.shutdown(); + } + finally { + myFileSystem = null; + myWatcher = null; + super.tearDown(); + } + } + + + public void testFileRoot() throws Exception { + final File file = FileUtil.createTempFile("test.", ".txt"); + refresh(file); + final LocalFileSystem.WatchRequest request = watch(file); + try { + FileUtil.writeToFile(file, "new content"); + assertEvent(VFileContentChangeEvent.class, file.getAbsolutePath()); + + FileUtil.delete(file); + assertEvent(VFileDeleteEvent.class, file.getAbsolutePath()); + + FileUtil.writeToFile(file, "re-creation"); + assertEvent(VFileCreateEvent.class, file.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoot(request); + FileUtil.delete(file); + } + } + + public void testNonCanonicallyNamedFileRoot() throws Exception { + if (SystemInfo.isFileSystemCaseSensitive) { + System.out.println("Ignored: case-insensitive FS required"); + return; + } + + final File file = FileUtil.createTempFile("test.", ".txt"); + refresh(file); + + final String watchRoot = file.getAbsolutePath().toUpperCase(Locale.US); + final LocalFileSystem.WatchRequest request = watch(new File(watchRoot)); + try { + FileUtil.writeToFile(file, "new content"); + assertEvent(VFileContentChangeEvent.class, file.getAbsolutePath()); + + FileUtil.delete(file); + assertEvent(VFileDeleteEvent.class, file.getAbsolutePath()); + + FileUtil.writeToFile(file, "re-creation"); + assertEvent(VFileCreateEvent.class, file.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoot(request); + FileUtil.delete(file); + } + } + + public void testDirectoryRecursive() throws Exception { + final File topDir = FileUtil.createTempDirectory("top.", null); + refresh(topDir); + + final LocalFileSystem.WatchRequest request = watch(topDir); + try { + final File subDir = FileUtil.createTempDirectory(topDir, "sub.", null); + assertEvent(VFileCreateEvent.class, subDir.getAbsolutePath()); + refresh(subDir); + + final File file = FileUtil.createTempFile(subDir, "test.", ".txt", true, false); + assertEvent(VFileCreateEvent.class, file.getAbsolutePath()); + + FileUtil.writeToFile(file, "new content"); + assertEvent(VFileContentChangeEvent.class, file.getAbsolutePath()); + + FileUtil.delete(file); + assertEvent(VFileDeleteEvent.class, file.getAbsolutePath()); + + FileUtil.writeToFile(file, "re-creation"); + assertEvent(VFileCreateEvent.class, file.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoot(request); + FileUtil.delete(topDir); + } + } + + public void testDirectoryFlat() throws Exception { + final File topDir = FileUtil.createTempDirectory("top.", null); + final File watchedFile = FileUtil.createTempFile(topDir, "test.", ".txt", true, false); + final File subDir = FileUtil.createTempDirectory(topDir, "sub.", null); + final File unwatchedFile = FileUtil.createTempFile(subDir, "test.", ".txt", true, false); + refresh(topDir); + + final LocalFileSystem.WatchRequest request = watch(topDir, false); + try { + FileUtil.writeToFile(watchedFile, "new content"); + assertEvent(VFileContentChangeEvent.class, watchedFile.getAbsolutePath()); + + FileUtil.writeToFile(unwatchedFile, "new content"); + assertEvent(VFileEvent.class); + } + finally { + myFileSystem.removeWatchedRoot(request); + FileUtil.delete(topDir); + } + } + + public void testDirectoryNonExisting() throws Exception { + final File topDir = FileUtil.createTempDirectory("top.", null); + final File subDir = new File(topDir, "subDir"); + final File file = new File(subDir, "file.txt"); + refresh(topDir); + + final LocalFileSystem.WatchRequest request = watch(subDir); + try { + assertTrue(subDir.toString(), subDir.mkdir()); + assertEvent(VFileCreateEvent.class, subDir.getAbsolutePath()); + refresh(subDir); + + FileUtil.writeToFile(file, "new content"); + assertEvent(VFileCreateEvent.class, file.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoot(request); + FileUtil.delete(topDir); + } + } + + public void testDirectoryOverlapping() throws Exception { + final File topDir = FileUtil.createTempDirectory("top.", null); + final File file1 = FileUtil.createTempFile(topDir, "file1.", ".txt", true, false); + final File subDir = FileUtil.createTempDirectory(topDir, "sub.", null); + final File file2 = FileUtil.createTempFile(subDir, "file2.", ".txt", true, false); + final File sideDir = FileUtil.createTempDirectory("side.", null); + final File file3 = FileUtil.createTempFile(sideDir, "file3.", ".txt", true, false); + refresh(topDir); + refresh(sideDir); + + final LocalFileSystem.WatchRequest request1 = watch(subDir); + final LocalFileSystem.WatchRequest request2 = watch(sideDir); + try { + FileUtil.writeToFile(file1, "new content"); + FileUtil.writeToFile(file2, "new content"); + FileUtil.writeToFile(file3, "new content"); + assertEvent(VFileContentChangeEvent.class, file2.getAbsolutePath(), file3.getAbsolutePath()); + + final LocalFileSystem.WatchRequest request3 = watch(topDir); + try { + FileUtil.writeToFile(file1, "newer content"); + FileUtil.writeToFile(file2, "newer content"); + FileUtil.writeToFile(file3, "newer content"); + assertEvent(VFileContentChangeEvent.class, file1.getAbsolutePath(), file2.getAbsolutePath(), file3.getAbsolutePath()); + } + finally { + unwatch(request3); + } + + FileUtil.writeToFile(file1, "newest content"); + FileUtil.writeToFile(file2, "newest content"); + FileUtil.writeToFile(file3, "newest content"); + assertEvent(VFileContentChangeEvent.class, file2.getAbsolutePath(), file3.getAbsolutePath()); + + FileUtil.delete(file1); + FileUtil.delete(file2); + FileUtil.delete(file3); + assertEvent(VFileDeleteEvent.class, file1.getAbsolutePath(), file2.getAbsolutePath(), file3.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoots(Arrays.asList(request1, request2)); + FileUtil.delete(topDir); + } + } + + @Bombed(user = "roman.shevchenko", year = 2012, month = Calendar.MAY, day = 1) + public void testSymlinkAboveWatchRoot() throws Exception { + final File topDir = FileUtil.createTempDirectory("top.", null); + final File topLink = SymlinkHandlingTest.createTempLink(topDir.getAbsolutePath(), "link"); + final File subDir = FileUtil.createTempDirectory(topDir, "sub.", null); + final File file = FileUtil.createTempFile(subDir, "test.", ".txt", true, false); + final File fileLink = new File(new File(topLink, subDir.getName()), file.getName()); + refresh(topDir); + refresh(topLink); + + final LocalFileSystem.WatchRequest request = watch(topLink); + try { + FileUtil.writeToFile(file, "new content"); + assertEvent(VFileContentChangeEvent.class, fileLink.getAbsolutePath()); + + FileUtil.delete(file); + assertEvent(VFileDeleteEvent.class, fileLink.getAbsolutePath()); + + FileUtil.writeToFile(file, "re-creation"); + assertEvent(VFileCreateEvent.class, fileLink.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoot(request); + FileUtil.delete(topLink); + FileUtil.delete(topDir); + } + } + + @Bombed(user = "roman.shevchenko", year = 2012, month = Calendar.MAY, day = 1) + public void testSymlinkBelowWatchRoot() throws Exception { + final File targetDir = FileUtil.createTempDirectory("top.", null); + final File file = FileUtil.createTempFile(targetDir, "test.", ".txt", true, false); + final File linkDir = FileUtil.createTempDirectory("link.", null); + final File link = new File(linkDir, "link"); + SymlinkHandlingTest.createTempLink(targetDir.getAbsolutePath(), link.getAbsolutePath()); + final File fileLink = new File(link, file.getName()); + refresh(targetDir); + refresh(linkDir); + + final LocalFileSystem.WatchRequest request = watch(linkDir); + try { + FileUtil.writeToFile(file, "new content"); + assertEvent(VFileContentChangeEvent.class, fileLink.getAbsolutePath()); + + FileUtil.delete(file); + assertEvent(VFileDeleteEvent.class, fileLink.getAbsolutePath()); + + FileUtil.writeToFile(file, "re-creation"); + assertEvent(VFileCreateEvent.class, fileLink.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoot(request); + FileUtil.delete(linkDir); + FileUtil.delete(targetDir); + } + } + + public void testSubst() throws Exception { + if (!SystemInfo.isWindows) { + System.out.println("Ignored: Windows required"); + return; + } + + final Set roots = ContainerUtil.map2Set(File.listRoots(), new Function() { + @Override + public Character fun(File root) { + return root.getPath().toLowerCase(Locale.US).charAt(0); + } + }); + char subst = 0; + for (char c = 'e'; c <= 'z'; c++) { + if (!roots.contains(c)) { + subst = c; + break; + } + } + assertFalse("Occupied: " + roots.toString(), subst == 0); + + final File targetDir = FileUtil.createTempDirectory("top.", null); + final File subDir = FileUtil.createTempDirectory(targetDir, "sub.", null); + final File file = FileUtil.createTempFile(subDir, "test.", ".txt", true, false); + final int rv = new GeneralCommandLine("subst", subst + ":", targetDir.getAbsolutePath()).createProcess().waitFor(); + assertEquals(0, rv); + + final File substDir = new File((subst + ":\\").toUpperCase(Locale.US), subDir.getName()); + final File substFile = new File(substDir, file.getName()); + refresh(targetDir); + refresh(substDir); + + final LocalFileSystem.WatchRequest request = watch(substDir); + try { + FileUtil.writeToFile(file, "new content"); + assertEvent(VFileContentChangeEvent.class, substFile.getAbsolutePath()); + + final LocalFileSystem.WatchRequest request2 = watch(targetDir); + try { + FileUtil.delete(file); + assertEvent(VFileDeleteEvent.class, file.getAbsolutePath(), substFile.getAbsolutePath()); + } + finally { + unwatch(request2); + } + + FileUtil.writeToFile(file, "re-creation"); + assertEvent(VFileCreateEvent.class, substFile.getAbsolutePath()); + } + finally { + myFileSystem.removeWatchedRoot(request); + new GeneralCommandLine("subst", subst + ":", "/d").createProcess().waitFor(); + FileUtil.delete(targetDir); + } + } + + + private List getEvents() throws InterruptedException { + TimeoutUtil.sleep(NATIVE_PROCESS_DELAY); + myFileSystem.refresh(false); + + synchronized (myEvents) { + final ArrayList result = new ArrayList(myEvents); + myEvents.clear(); + return result; + } + } + + private void clearEvents() { + myFileSystem.refresh(false); + synchronized (myEvents) { + myEvents.clear(); + } + } + + @NotNull + private LocalFileSystem.WatchRequest watch(final File watchFile) throws InterruptedException { + return watch(watchFile, true); + } + + @NotNull + private LocalFileSystem.WatchRequest watch(final File watchFile, final boolean recursive) throws InterruptedException { + final LocalFileSystem.WatchRequest request = myFileSystem.addRootToWatch(watchFile.getAbsolutePath(), recursive); + assertNotNull(request); + TimeoutUtil.sleep(NATIVE_PROCESS_DELAY); + clearEvents(); + return request; + } + + private void unwatch(final LocalFileSystem.WatchRequest request) throws InterruptedException { + myFileSystem.removeWatchedRoot(request); + TimeoutUtil.sleep(NATIVE_PROCESS_DELAY); + clearEvents(); + } + + private VirtualFile refresh(final File file) { + final VirtualFile vFile = myFileSystem.refreshAndFindFileByIoFile(file); + assertNotNull(file.toString(), vFile); + VfsUtilCore.visitChildrenRecursively(vFile, new VirtualFileVisitor() { + @Override + public boolean visitFile(@NotNull VirtualFile file) { + file.getChildren(); + return true; + } + }); + return vFile; + } + + private void assertEvent(final Class type, final String... paths) throws InterruptedException { + final List events = getEvents(); + assertEquals(events.toString(), paths.length, events.size()); + + final Set pathSet = ContainerUtil.map2Set(paths, new Function() { + @Override + public String fun(final String path) { + return FileUtil.toSystemIndependentName(path); + } + }); + + for (final VFileEvent event : events) { + assertTrue(event.toString(), type.isInstance(event)); + + final VirtualFile eventFile = event.getFile(); + assertNotNull(event.toString(), eventFile); + + assertTrue(eventFile + " not in " + Arrays.toString(paths), pathSet.remove(eventFile.getPath())); + } + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/SymlinkHandlingTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/SymlinkHandlingTest.java index 025a888e1a20..2a8cb8d5d453 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/SymlinkHandlingTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/SymlinkHandlingTest.java @@ -266,7 +266,7 @@ public class SymlinkHandlingTest extends LightPlatformLangTestCase { } // todo[r.sh] use NIO2 API after migration to JDK 7 - private static File createTempLink(final String target, final String link) throws InterruptedException, ExecutionException { + public static File createTempLink(final String target, final String link) throws InterruptedException, ExecutionException { final boolean isAbsolute = SystemInfo.isUnix && StringUtil.startsWithChar(link, '/') || SystemInfo.isWindows && link.matches("^[c-zC-Z]:[/\\\\].*$"); final File linkFile = isAbsolute ? new File(link) : new File(FileUtil.getTempDirectory(), link);