File watcher: fix subst mapping; remove symlink mapping; clean locks; test

This commit is contained in:
Roman Shevchenko
2012-04-10 19:54:39 +02:00
parent 8487c658e4
commit a1e4dc645e
3 changed files with 591 additions and 154 deletions
@@ -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<String,String,Boolean> PATH_COMPARATOR = new PairFunction<String, String, Boolean>() {
@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<String> myDirtyPaths = new ArrayList<String>();
private List<String> myDirtyRecursivePaths = new ArrayList<String>();
@@ -75,21 +65,23 @@ public class FileWatcher {
private List<String> myManualWatchRoots = new ArrayList<String>();
private final List<Pair<String, String>> myMapping = new ArrayList<Pair<String, String>>();
private List<Pair<String, String>> myCanonicalMapping = new ArrayList<Pair<String, String>>();
private List<String> myRecursiveWatchRoots = new ArrayList<String>();
private List<String> myFlatWatchRoots = new ArrayList<String>();
private final Collection<String> myAllPaths = new ArrayList<String>(2);
private final Collection<String> myWatchedPaths = new ArrayList<String>(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<String>();
return result;
}
}
public List<String> getDirtyDirs() {
@@ -158,26 +149,16 @@ public class FileWatcher {
}
public void setWatchRoots(final List<String> recursive, final List<String> flat) {
SET_ROOTS_LOCK.lock();
try {
synchronized (LOCK) {
if (myRecursiveWatchRoots.equals(recursive) && myFlatWatchRoots.equals(flat)) return;
}
final List<Pair<String, String>> mapping = new ArrayList<Pair<String, String>>();
long t = System.nanoTime();
final List<String> checkedRecursive = checkPaths(recursive, mapping);
final List<String> 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<String> checkPaths(final List<String> paths, final List<Pair<String, String>> mapping) {
if (!SystemInfo.areSymLinksSupported) return paths;
final List<String> checkedPaths = new ArrayList<String>(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: <a href=\"" + executable + "\">" + executable + "</a>";
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: <a href=\"" + execPath + "\">" + execPath +"</a>", 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<String, String>(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<String> 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<String> checkWatchable(final String reportedPath) {
if (reportedPath == null) return Collections.emptyList();
for (Pair<String, String> mapping : myCanonicalMapping) {
if (path.startsWith(mapping.first)) {
path = mapping.second + path.substring(mapping.first.length());
break;
myAllPaths.clear();
myAllPaths.add(reportedPath);
for (Pair<String, String> 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<String> 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<String> list) {
list.add(path);
for (Pair<String, String> 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();
@@ -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<VFileEvent> myEvents = new ArrayList<VFileEvent>();
@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<? extends VFileEvent> events) {
}
@Override
public void after(@NotNull List<? extends VFileEvent> 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<Character> roots = ContainerUtil.map2Set(File.listRoots(), new Function<File, Character>() {
@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<VFileEvent> getEvents() throws InterruptedException {
TimeoutUtil.sleep(NATIVE_PROCESS_DELAY);
myFileSystem.refresh(false);
synchronized (myEvents) {
final ArrayList<VFileEvent> result = new ArrayList<VFileEvent>(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<? extends VFileEvent> type, final String... paths) throws InterruptedException {
final List<VFileEvent> events = getEvents();
assertEquals(events.toString(), paths.length, events.size());
final Set<String> pathSet = ContainerUtil.map2Set(paths, new Function<String, String>() {
@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()));
}
}
}
@@ -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);