fix "IDEA-221944 Deadlock on opening second project" and support preloading for proje...
[idea/community.git] / platform / lang-impl / src / com / intellij / openapi / roots / impl / ProjectRootManagerComponent.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.roots.impl;
3
4 import com.intellij.ProjectTopics;
5 import com.intellij.openapi.Disposable;
6 import com.intellij.openapi.application.*;
7 import com.intellij.openapi.components.ProjectComponent;
8 import com.intellij.openapi.components.impl.stores.BatchUpdateListener;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.fileTypes.FileTypeEvent;
11 import com.intellij.openapi.fileTypes.FileTypeListener;
12 import com.intellij.openapi.fileTypes.FileTypeManager;
13 import com.intellij.openapi.module.Module;
14 import com.intellij.openapi.module.ModuleManager;
15 import com.intellij.openapi.module.impl.ModuleEx;
16 import com.intellij.openapi.project.DumbModeTask;
17 import com.intellij.openapi.project.DumbServiceImpl;
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.roots.AdditionalLibraryRootsProvider;
20 import com.intellij.openapi.roots.ModuleRootManager;
21 import com.intellij.openapi.roots.OrderRootType;
22 import com.intellij.openapi.roots.WatchedRootsProvider;
23 import com.intellij.openapi.startup.StartupManager;
24 import com.intellij.openapi.util.Disposer;
25 import com.intellij.openapi.util.Pair;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.vfs.*;
28 import com.intellij.openapi.vfs.impl.VirtualFilePointerContainerImpl;
29 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
30 import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
31 import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
32 import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
33 import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
34 import com.intellij.project.ProjectKt;
35 import com.intellij.ui.GuiUtils;
36 import com.intellij.util.ConcurrencyUtil;
37 import com.intellij.util.concurrency.AppExecutorUtil;
38 import com.intellij.util.containers.ContainerUtil;
39 import com.intellij.util.indexing.FileBasedIndex;
40 import com.intellij.util.indexing.FileBasedIndexImpl;
41 import com.intellij.util.indexing.FileBasedIndexProjectHandler;
42 import com.intellij.util.indexing.UnindexedFilesUpdater;
43 import com.intellij.util.messages.MessageBusConnection;
44 import gnu.trove.THashSet;
45 import org.jetbrains.annotations.CalledInAwt;
46 import org.jetbrains.annotations.NotNull;
47
48 import java.io.File;
49 import java.util.Collection;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Set;
53 import java.util.concurrent.CompletableFuture;
54 import java.util.concurrent.ExecutorService;
55 import java.util.concurrent.Future;
56
57 /**
58  * ProjectRootManager extended with ability to watch events.
59  */
60 public class ProjectRootManagerComponent extends ProjectRootManagerImpl implements ProjectComponent, Disposable {
61   private static final Logger LOG = Logger.getInstance(ProjectRootManagerComponent.class);
62   private static final boolean LOG_CACHES_UPDATE =
63     ApplicationManager.getApplication().isInternal() && !ApplicationManager.getApplication().isUnitTestMode();
64
65   private final ExecutorService myExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("Project Root Manager", 1);
66   private Future<?> myCollectWatchRootsFuture = CompletableFuture.completedFuture(null);
67
68   private boolean myPointerChangesDetected;
69   private int myInsideRefresh;
70   @NotNull
71   private Set<LocalFileSystem.WatchRequest> myRootsToWatch = new THashSet<>();
72   private Disposable myRootPointersDisposable = Disposer.newDisposable(); // accessed in EDT
73
74   public ProjectRootManagerComponent(@NotNull Project project) {
75     super(project);
76
77     MessageBusConnection connection = project.getMessageBus().connect(this);
78     connection.subscribe(FileTypeManager.TOPIC, new FileTypeListener() {
79       @Override
80       public void beforeFileTypesChanged(@NotNull FileTypeEvent event) {
81         beforeRootsChange(true);
82       }
83
84       @Override
85       public void fileTypesChanged(@NotNull FileTypeEvent event) {
86         rootsChanged(true);
87       }
88     });
89
90     VirtualFileManager.getInstance().addVirtualFileManagerListener(new VirtualFileManagerListener() {
91       @Override
92       public void afterRefreshFinish(boolean asynchronous) {
93         doUpdateOnRefresh();
94       }
95     }, project);
96
97     if (!myProject.isDefault()) {
98       StartupManager.getInstance(project).registerStartupActivity(() -> myStartupActivityPerformed = true);
99     }
100
101     connection.subscribe(BatchUpdateListener.TOPIC, new BatchUpdateListener() {
102       @Override
103       public void onBatchUpdateStarted() {
104         myRootsChanged.levelUp();
105         myFileTypesChanged.levelUp();
106       }
107
108       @Override
109       public void onBatchUpdateFinished() {
110         myRootsChanged.levelDown();
111         myFileTypesChanged.levelDown();
112       }
113     });
114   }
115
116   @Override
117   public void projectOpened() {
118     addRootsToWatch();
119     ApplicationManager.getApplication().addApplicationListener(new AppListener(), myProject);
120   }
121
122   @Override
123   public void projectClosed() {
124     LocalFileSystem.getInstance().removeWatchedRoots(myRootsToWatch);
125   }
126
127   @CalledInAwt
128   private void addRootsToWatch() {
129     if (myProject.isDefault()) return;
130
131     Disposable prev = myRootPointersDisposable;
132     Disposable next = Disposer.newDisposable();
133
134     Application application = ApplicationManager.getApplication();
135     ExecutorService service = application.isUnitTestMode() ? ConcurrencyUtil.newSameThreadExecutorService() : myExecutor;
136
137     myCollectWatchRootsFuture.cancel(false);
138     myCollectWatchRootsFuture = service.submit(() -> {
139       if (myProject.isDisposed()) return;
140       Pair<Set<String>, Set<String>> pair = ReadAction.compute(() -> collectWatchRoots(next));
141       GuiUtils.invokeLaterIfNeeded(() -> {
142         myRootPointersDisposable = next;
143         Disposer.dispose(prev); // dispose after the re-creating container to keep VFPs from disposing and re-creating back
144         myRootsToWatch = LocalFileSystem.getInstance().replaceWatchedRoots(myRootsToWatch, pair.first, pair.second);
145       }, ModalityState.any());
146     });
147   }
148
149   private void beforeRootsChange(boolean fileTypes) {
150     if (myProject.isDisposed()) return;
151     getBatchSession(fileTypes).beforeRootsChanged();
152   }
153
154   private void rootsChanged(boolean fileTypes) {
155     getBatchSession(fileTypes).rootsChanged();
156   }
157
158   private void doUpdateOnRefresh() {
159     if (ApplicationManager.getApplication().isUnitTestMode() && (!myStartupActivityPerformed || myProject.isDisposed())) {
160       return; // in test mode suppress addition to a queue unless project is properly initialized
161     }
162     if (myProject.isDefault()) {
163       return;
164     }
165
166     if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
167       LOG.debug("refresh");
168     }
169     DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
170     DumbModeTask task = FileBasedIndexProjectHandler.createChangedFilesIndexingTask(myProject);
171     if (task != null) {
172       dumbService.queueTask(task);
173     }
174   }
175
176   @Override
177   protected void fireBeforeRootsChangeEvent(boolean fileTypes) {
178     isFiringEvent = true;
179     try {
180       myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).beforeRootsChange(new ModuleRootEventImpl(myProject, fileTypes));
181     }
182     finally {
183       isFiringEvent = false;
184     }
185   }
186
187   @Override
188   protected void fireRootsChangedEvent(boolean fileTypes) {
189     isFiringEvent = true;
190     try {
191       myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).rootsChanged(new ModuleRootEventImpl(myProject, fileTypes));
192     }
193     finally {
194       isFiringEvent = false;
195     }
196
197     synchronizeRoots();
198     addRootsToWatch();
199   }
200
201   private Pair<Set<String>, Set<String>> collectWatchRoots(@NotNull Disposable disposable) {
202     Set<String> recursivePaths = new THashSet<>(FileUtil.PATH_HASHING_STRATEGY);
203     Set<String> flatPaths = new THashSet<>(FileUtil.PATH_HASHING_STRATEGY);
204
205     String projectFilePath = myProject.getProjectFilePath();
206     if (projectFilePath != null && !Project.DIRECTORY_STORE_FOLDER.equals(new File(projectFilePath).getParentFile().getName())) {
207       flatPaths.add(FileUtil.toSystemIndependentName(projectFilePath));
208       String wsFilePath = ProjectKt.getStateStore(myProject).getWorkspaceFilePath();  // may not exist yet
209       if (wsFilePath != null) {
210         flatPaths.add(FileUtil.toSystemIndependentName(wsFilePath));
211       }
212     }
213
214     for (AdditionalLibraryRootsProvider extension : AdditionalLibraryRootsProvider.EP_NAME.getExtensions()) {
215       Collection<VirtualFile> toWatch = extension.getRootsToWatch(myProject);
216       if (!toWatch.isEmpty()) {
217         recursivePaths.addAll(ContainerUtil.map(toWatch, VirtualFile::getPath));
218       }
219     }
220
221     for (WatchedRootsProvider extension : WatchedRootsProvider.EP_NAME.getExtensions(myProject)) {
222       Set<String> toWatch = extension.getRootsToWatch();
223       if (!toWatch.isEmpty()) {
224         recursivePaths.addAll(ContainerUtil.map(toWatch, FileUtil::toSystemIndependentName));
225       }
226     }
227
228
229     List<String> recursiveUrls = ContainerUtil.map(recursivePaths, VfsUtilCore::pathToUrl);
230     Set<String> excludedUrls = new THashSet<>();
231     // changes in files provided by this method should be watched manually because no-one's bothered to set up correct pointers for them
232     for (DirectoryIndexExcludePolicy excludePolicy : DirectoryIndexExcludePolicy.EP_NAME.getExtensions(myProject)) {
233       Collections.addAll(excludedUrls, excludePolicy.getExcludeUrlsForProject());
234     }
235
236     // avoid creating empty unnecessary container
237     if (!recursiveUrls.isEmpty() || !flatPaths.isEmpty() || !excludedUrls.isEmpty()) {
238       Disposer.register(this, disposable);
239       // creating a container with these URLs with the sole purpose to get events to getRootsValidityChangedListener() when these roots change
240       VirtualFilePointerContainer container =
241         VirtualFilePointerManager.getInstance().createContainer(disposable, getRootsValidityChangedListener());
242
243       ((VirtualFilePointerContainerImpl)container).addAllJarDirectories(recursiveUrls, true);
244       flatPaths.forEach(path -> container.add(VfsUtilCore.pathToUrl(path)));
245       ((VirtualFilePointerContainerImpl)container).addAll(excludedUrls);
246     }
247
248     // module roots already fire validity change events, see usages of ProjectRootManagerComponent.getRootsValidityChangedListener
249     collectModuleWatchRoots(recursivePaths, flatPaths);
250
251     return Pair.create(recursivePaths, flatPaths);
252   }
253
254   private void collectModuleWatchRoots(@NotNull Set<String> recursivePaths, @NotNull Set<String> flatPaths) {
255     Set<String> urls = ContainerUtil.newTroveSet(FileUtil.PATH_HASHING_STRATEGY);
256
257     for (Module module : ModuleManager.getInstance(myProject).getModules()) {
258       ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
259
260       ContainerUtil.addAll(urls, rootManager.getContentRootUrls());
261
262       rootManager.orderEntries().withoutModuleSourceEntries().withoutDepModules().forEach(entry -> {
263         for (OrderRootType type : OrderRootType.getAllTypes()) {
264           ContainerUtil.addAll(urls, entry.getUrls(type));
265         }
266         return true;
267       });
268     }
269
270     for (String url : urls) {
271       String protocol = VirtualFileManager.extractProtocol(url);
272       if (protocol == null || StandardFileSystems.FILE_PROTOCOL.equals(protocol)) {
273         recursivePaths.add(extractLocalPath(url));
274       }
275       else if (StandardFileSystems.JAR_PROTOCOL.equals(protocol)) {
276         flatPaths.add(extractLocalPath(url));
277       }
278       else if (StandardFileSystems.JRT_PROTOCOL.equals(protocol)) {
279         recursivePaths.add(extractLocalPath(url));
280       }
281     }
282   }
283
284   private void synchronizeRoots() {
285     if (!myStartupActivityPerformed) return;
286
287     if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
288       LOG.debug(new Throwable("sync roots"));
289     }
290     else if (!ApplicationManager.getApplication().isUnitTestMode()) {
291       LOG.info("project roots have changed");
292     }
293
294     DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
295     if (FileBasedIndex.getInstance() instanceof FileBasedIndexImpl) {
296       dumbService.queueTask(new UnindexedFilesUpdater(myProject));
297     }
298   }
299
300   @Override
301   protected void clearScopesCaches() {
302     super.clearScopesCaches();
303
304     LibraryScopeCache libraryScopeCache = myProject.getServiceIfCreated(LibraryScopeCache.class);
305     if (libraryScopeCache != null) {
306       libraryScopeCache.clear();
307     }
308   }
309
310   @Override
311   public void clearScopesCachesForModules() {
312     super.clearScopesCachesForModules();
313     Module[] modules = ModuleManager.getInstance(myProject).getModules();
314     for (Module module : modules) {
315       ((ModuleEx)module).clearScopesCache();
316     }
317   }
318
319   @Override
320   public void markRootsForRefresh() {
321     Set<String> paths = new THashSet<>(FileUtil.PATH_HASHING_STRATEGY);
322     collectModuleWatchRoots(paths, paths);
323
324     LocalFileSystem fs = LocalFileSystem.getInstance();
325     for (String path : paths) {
326       VirtualFile root = fs.findFileByPath(path);
327       if (root instanceof NewVirtualFile) {
328         ((NewVirtualFile)root).markDirtyRecursively();
329       }
330     }
331   }
332
333   @Override
334   public void dispose() {
335     myCollectWatchRootsFuture.cancel(false);
336     myExecutor.shutdownNow();
337     assertListenersAreDisposed();
338   }
339
340   private class AppListener implements ApplicationListener {
341     @Override
342     public void beforeWriteActionStart(@NotNull Object action) {
343       myInsideRefresh++;
344     }
345
346     @Override
347     public void writeActionFinished(@NotNull Object action) {
348       if (--myInsideRefresh == 0 && myPointerChangesDetected) {
349         myPointerChangesDetected = false;
350         incModificationCount();
351
352         myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).rootsChanged(new ModuleRootEventImpl(myProject, false));
353
354         synchronizeRoots();
355         addRootsToWatch();
356       }
357     }
358   }
359
360   private final VirtualFilePointerListener myRootsChangedListener = new VirtualFilePointerListener() {
361     @Override
362     public void beforeValidityChanged(@NotNull VirtualFilePointer[] pointers) {
363       if (myProject.isDisposed()) {
364         return;
365       }
366
367       if (myInsideRefresh == 0) {
368         beforeRootsChange(false);
369         if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
370           LOG.debug(new Throwable(pointers.length > 0 ? pointers[0].getPresentableUrl():""));
371         }
372       }
373       else if (!myPointerChangesDetected) {
374         //this is the first pointer changing validity
375         myPointerChangesDetected = true;
376         myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).beforeRootsChange(new ModuleRootEventImpl(myProject, false));
377         if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
378           LOG.debug(new Throwable(pointers.length > 0 ? pointers[0].getPresentableUrl() : ""));
379         }
380       }
381     }
382
383     @Override
384     public void validityChanged(@NotNull VirtualFilePointer[] pointers) {
385       if (myProject.isDisposed()) {
386         return;
387       }
388
389       if (myInsideRefresh > 0) {
390         clearScopesCaches();
391       }
392       else {
393         rootsChanged(false);
394       }
395     }
396   };
397
398   @NotNull
399   @Override
400   public VirtualFilePointerListener getRootsValidityChangedListener() {
401     return myRootsChangedListener;
402   }
403 }