diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/VfsImplUtil.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/VfsImplUtil.java index a4a98d667c94..e2730da5cbf1 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/VfsImplUtil.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/VfsImplUtil.java @@ -167,7 +167,7 @@ public class VfsImplUtil { * * FileDocumentManager.getInstance().saveDocument(document); * runExternalToolToChangeFile(virtualFile.getPath()) // changes file externally in milliseconds, probably without changing file's length - * VfsUtil.markDirtyAndRefresh(true, true, true, virtualFile); // might be replace with {@link #forceSyncRefresh(VirtualFile)} + * VfsUtil.markDirtyAndRefresh(true, true, true, virtualFile); // might be replaced with {@link #forceSyncRefresh(VirtualFile)} * */ public static void forceSyncRefresh(@NotNull VirtualFile file) { @@ -236,20 +236,9 @@ public class VfsImplUtil { for (VFileEvent event : events) { if (!(event.getFileSystem() instanceof LocalFileSystem)) continue; - if (event instanceof VFileCreateEvent) continue; // new files don't affect existing handlers (and getFile() is costly) - - if (event instanceof VFilePropertyChangeEvent && - !VirtualFile.PROP_NAME.equals(((VFilePropertyChangeEvent)event).getPropertyName())) { - continue; - } + if (!(event instanceof VFileContentChangeEvent)) continue; String path = event.getPath(); - if (event instanceof VFilePropertyChangeEvent) { - path = ((VFilePropertyChangeEvent)event).getOldPath(); - } - else if (event instanceof VFileMoveEvent) { - path = ((VFileMoveEvent)event).getOldPath(); - } VirtualFile file = event.getFile(); if (file == null || !file.isDirectory()) { @@ -330,4 +319,63 @@ public class VfsImplUtil { if (state == null) throw new IllegalArgumentException(localPath + " not in " + ourHandlerCache.keySet()); } } + + /** + * check whether {@code event} (in LocalFileSystem) affects some jars and if so, generate appropriate additional JarFileSystem-events and corresponding after-event-actions. + * For example, "delete/change/move '/tmp/x.jar'" event should generate "delete jar:///tmp/x.jar!/" events. + */ + @NotNull + public static List getJarInvalidationEvents(@NotNull VFileEvent event, @NotNull List outApplyActions) { + if (!(event instanceof VFileDeleteEvent || + event instanceof VFileMoveEvent || + event instanceof VFilePropertyChangeEvent && VirtualFile.PROP_NAME.equals(((VFilePropertyChangeEvent)event).getPropertyName()))) { + return Collections.emptyList(); + } + String path; + if (event instanceof VFilePropertyChangeEvent) { + path = ((VFilePropertyChangeEvent)event).getOldPath(); + } + else if (event instanceof VFileMoveEvent) { + path = ((VFileMoveEvent)event).getOldPath(); + } + else { + path = event.getPath(); + } + + VirtualFile file = event.getFile(); + if (file == null) { + return Collections.emptyList(); + } + Collection jarPaths = ourDominatorsMap.get(path); + if (jarPaths == null) { + jarPaths = Collections.singletonList(path); + } + List events = new ArrayList<>(jarPaths.size()); + for (String jarPath : jarPaths) { + Pair handlerPair = ourHandlerCache.get(jarPath); + if (handlerPair == null) { + continue; + } + ArchiveFileSystem fileSystem = handlerPair.first; + NewVirtualFile root = ManagingFS.getInstance().findRoot(fileSystem.composeRootPath(jarPath), fileSystem); + if (root != null) { + VFileDeleteEvent jarDeleteEvent = new VFileDeleteEvent(event.getRequestor(), root, event.isFromRefresh()); + Runnable runnable = () -> { + Pair pair = ourHandlerCache.remove(jarPath); + if (pair != null) { + pair.second.dispose(); + forEachDirectoryComponent(jarPath, containingDirectoryPath -> { + Set handlers = ourDominatorsMap.get(containingDirectoryPath); + if (handlers != null && handlers.remove(jarPath) && handlers.isEmpty()) { + ourDominatorsMap.remove(containingDirectoryPath); + } + }); + } + }; + events.add(jarDeleteEvent); + outApplyActions.add(runnable); + } + } + return events; + } } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSImpl.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSImpl.java index f3aefcd48420..c72f552ae87e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSImpl.java @@ -722,14 +722,31 @@ public final class PersistentFSImpl extends PersistentFS implements Disposable { private void processEvent(@NotNull VFileEvent event) { ApplicationManager.getApplication().assertWriteAccessAllowed(); - // optimisation: skip all groupings - if (event.isValid()) { - List events = Collections.singletonList(event); - getPublisher().before(events); + if (!event.isValid()) { + return; + } + List outValidatedEvents = new ArrayList<>(); + outValidatedEvents.add(event); + List outApplyActions = new ArrayList<>(); + List jarDeleteEvents = VfsImplUtil.getJarInvalidationEvents(event, outApplyActions); + BulkFileListener publisher = getPublisher(); + if (jarDeleteEvents.isEmpty() && outApplyActions.isEmpty()) { + // optimisation: skip all groupings + publisher.before(outValidatedEvents); applyEvent(event); - getPublisher().after(events); + publisher.after(outValidatedEvents); + } + else { + outApplyActions.add(() -> applyEvent(event)); + // there are a number of additional jar events generated + for (VFileDeleteEvent jarDeleteEvent : jarDeleteEvents) { + outApplyActions.add(() -> applyEvent(jarDeleteEvent)); + outValidatedEvents.add(jarDeleteEvent); + } + + applyMultipleEvents(publisher, outApplyActions, outValidatedEvents); } } @@ -874,6 +891,15 @@ public final class PersistentFSImpl extends PersistentFS implements Disposable { groupDeletions(events, startIndex, endIndex, outValidatedEvents, outApplyActions, toIgnore); groupOthers(events, startIndex, endIndex, outValidatedEvents, outApplyActions); + for (int i = startIndex; i < endIndex; i++) { + VFileEvent event = events.get(i); + List jarDeleteEvents = VfsImplUtil.getJarInvalidationEvents(event, outApplyActions); + for (VFileDeleteEvent jarDeleteEvent : jarDeleteEvents) { + outApplyActions.add((Runnable)() -> applyEvent(jarDeleteEvent)); + outValidatedEvents.add(jarDeleteEvent); + } + } + return endIndex; } @@ -989,20 +1015,26 @@ public final class PersistentFSImpl extends PersistentFS implements Disposable { startIndex = groupAndValidate(events, startIndex, applyActions, validated, files, middleDirs); if (!validated.isEmpty()) { - PingProgress.interactWithEdtProgress(); - // do defensive copy to cope with ill-written listeners that save passed list for later processing - List toSend = ContainerUtil.immutableList(validated.toArray(new VFileEvent[0])); - publisher.before(toSend); - - PingProgress.interactWithEdtProgress(); - applyActions.forEach(Runnable::run); - - PingProgress.interactWithEdtProgress(); - publisher.after(toSend); + applyMultipleEvents(publisher, applyActions, validated); } } } + private static void applyMultipleEvents(@NotNull BulkFileListener publisher, + @NotNull List applyActions, + @NotNull List applyEvents) { + PingProgress.interactWithEdtProgress(); + // do defensive copy to cope with ill-written listeners that save passed list for later processing + List toSend = ContainerUtil.immutableList(applyEvents.toArray(new VFileEvent[0])); + publisher.before(toSend); + + PingProgress.interactWithEdtProgress(); + applyActions.forEach(Runnable::run); + + PingProgress.interactWithEdtProgress(); + publisher.after(toSend); + } + // remove children from specified directories using VirtualDirectoryImpl.removeChildren() optimised for bulk removals private void applyDeletions(@NotNull MultiMap deletions) { for (Map.Entry> entry : deletions.entrySet()) { diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/JarFileSystemTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/JarFileSystemTest.java index 097b61451062..fcec3456e5f4 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/JarFileSystemTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/local/JarFileSystemTest.java @@ -18,9 +18,11 @@ import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; import com.intellij.openapi.vfs.newvfs.events.VFileEvent; import com.intellij.testFramework.PlatformTestUtil; import com.intellij.testFramework.UsefulTestCase; +import com.intellij.testFramework.VfsTestUtil; import com.intellij.testFramework.fixtures.BareTestFixtureTestCase; import com.intellij.testFramework.rules.TempDirectory; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.messages.MessageBusConnection; import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Rule; @@ -39,8 +41,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.jar.JarFile; import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; import static com.intellij.openapi.util.io.IoTestUtil.assertTimestampsEqual; import static com.intellij.testFramework.PlatformTestUtil.assertPathsEqual; @@ -225,22 +225,20 @@ public class JarFileSystemTest extends BareTestFixtureTestCase { } @Test - public void testInvalidZip() throws IOException { - File testZip = tempDir.newFile("test.zip"); - try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(testZip))) { - writeEntry(zip, "a"); - writeEntry(zip, "a/b"); - writeEntry(zip, "a/b/c.txt"); - writeEntry(zip, "x\\y\\z.txt"); - writeEntry(zip, "/x/f.txt"); - writeEntry(zip, "d1/aB"); - writeEntry(zip, "d1/ab"); - writeEntry(zip, "D2/f1"); - writeEntry(zip, "d2/f2"); - } + public void testInvalidZip() { + VirtualFile vf = createJar( + "a" + , "a/b" + , "a/b/c.txt" + , "x\\y\\z.txt" + , "/x/f.txt" + , "d1/aB" + , "d1/ab" + , "D2/f1" + , "d2/f2"); - String rootPath = FileUtil.toSystemIndependentName(testZip.getPath()) + JarFileSystem.JAR_SEPARATOR; - VirtualFile jarRoot = JarFileSystem.getInstance().findFileByPath(rootPath); + String rootPath = vf.getPath() + JarFileSystem.JAR_SEPARATOR; + VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(vf); assertNotNull(jarRoot); List entries = new ArrayList<>(); VfsUtilCore.visitChildrenRecursively(jarRoot, new VirtualFileVisitor() { @@ -257,12 +255,6 @@ public class JarFileSystemTest extends BareTestFixtureTestCase { "a/", "a/b/", "a/b/c.txt", "x/", "x/y/", "x/f.txt", "x/y/z.txt", "d1/", "d1/aB", "d1/ab", "D2/", "D2/f1", "d2/", "d2/f2"); } - private static void writeEntry(ZipOutputStream zip, String name) throws IOException { - ZipEntry entry = new ZipEntry(name); - zip.putNextEntry(entry); - zip.closeEntry(); - } - @Test public void testJarRootForLocalFile() { String jarPath = PathManager.getJarPathForClass(Test.class); @@ -391,4 +383,34 @@ public class JarFileSystemTest extends BareTestFixtureTestCase { assertNotNull(jarRoot); assertNotNull(jarRoot.findFileByRelativePath(JarFile.MANIFEST_NAME)); } + + @Test + public void testJarFileMustInvalidateOnDeleteLocalEntryFile() { + VirtualFile vf = createJar("a", "a/b"); + + VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(vf); + assertNotNull(jarRoot); + VirtualFile a = jarRoot.findChild("a"); + assertNotNull(a); + assertTrue(a.isValid()); + assertTrue(jarRoot.isValid()); + + VirtualFile local = JarFileSystem.getInstance().getLocalVirtualFileFor(jarRoot); + assertEquals(LocalFileSystem.getInstance(), local.getFileSystem()); + JarFileSystemImpl.cleanupForNextTest(); // WTF, won't let delete jar otherwise + + MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(getTestRootDisposable()); + connection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() { + @Override + public void after(@NotNull List events) { + // jars must be invalidated immediately after deleting the local root + assertFalse(a.isValid()); + assertFalse(jarRoot.isValid()); + } + }); + VfsTestUtil.deleteFile(local); + assertFalse(a.isValid()); + assertFalse(jarRoot.isValid()); + } + } \ No newline at end of file