/* * Copyright 2000-2014 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.project.impl; import com.intellij.CommonBundle; import com.intellij.conversion.ConversionResult; import com.intellij.conversion.ConversionService; import com.intellij.ide.AppLifecycleListener; import com.intellij.ide.RecentProjectsManagerBase; import com.intellij.ide.impl.ProjectUtil; import com.intellij.ide.plugins.PluginManager; import com.intellij.ide.startup.impl.StartupManagerImpl; import com.intellij.notification.NotificationsManager; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.*; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.application.impl.ApplicationImpl; import com.intellij.openapi.components.ExportableApplicationComponent; import com.intellij.openapi.components.StateStorage; import com.intellij.openapi.components.StateStorageException; import com.intellij.openapi.components.TrackingPathMacroSubstitutor; import com.intellij.openapi.components.impl.stores.IComponentStore; import com.intellij.openapi.components.impl.stores.IProjectStore; import com.intellij.openapi.components.impl.stores.StorageUtil; import com.intellij.openapi.components.impl.stores.XmlElementStorage; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.progress.*; import com.intellij.openapi.progress.util.ProgressWindow; import com.intellij.openapi.project.*; import com.intellij.openapi.project.ex.ProjectEx; import com.intellij.openapi.project.ex.ProjectManagerEx; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; 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.wm.impl.welcomeScreen.WelcomeFrame; import com.intellij.util.Alarm; import com.intellij.util.ArrayUtil; import com.intellij.util.TimeoutUtil; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashMap; import com.intellij.util.messages.MessageBus; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.ui.UIUtil; import gnu.trove.THashSet; import gnu.trove.TObjectLongHashMap; import org.jdom.Element; import org.jdom.JDOMException; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.io.File; import java.io.IOException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; public class ProjectManagerImpl extends ProjectManagerEx implements NamedJDOMExternalizable, ExportableApplicationComponent { private static final Logger LOG = Logger.getInstance("#com.intellij.project.impl.ProjectManagerImpl"); public static final int CURRENT_FORMAT_VERSION = 4; private static final Key> LISTENERS_IN_PROJECT_KEY = Key.create("LISTENERS_IN_PROJECT_KEY"); private static final String ELEMENT_DEFAULT_PROJECT = "defaultProject"; @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) private ProjectImpl myDefaultProject; // Only used asynchronously in save and dispose, which itself are synchronized. @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) private Element myDefaultProjectRootElement; // Only used asynchronously in save and dispose, which itself are synchronized. private final List myOpenProjects = new ArrayList(); private Project[] myOpenProjectsArrayCache = {}; private final List myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private final Set myTestProjects = new THashSet(); private final Map mySavedCopies = new HashMap(); private final TObjectLongHashMap mySavedTimestamps = new TObjectLongHashMap(); private final Map>> myChangedProjectFiles = new HashMap>>(); private final Alarm myChangedFilesAlarm = new Alarm(); private final List> myChangedApplicationFiles = new ArrayList>(); private final AtomicInteger myReloadBlockCount = new AtomicInteger(0); private final ProgressManager myProgressManager; private volatile boolean myDefaultProjectWasDisposed = false; @NotNull private static List getListeners(Project project) { List array = project.getUserData(LISTENERS_IN_PROJECT_KEY); if (array == null) return Collections.emptyList(); return array; } /** @noinspection UnusedParameters*/ public ProjectManagerImpl(VirtualFileManager virtualFileManager, RecentProjectsManagerBase recentProjectsManager, ProgressManager progressManager) { myProgressManager = progressManager; Application app = ApplicationManager.getApplication(); MessageBus messageBus = app.getMessageBus(); messageBus.connect(app).subscribe(StateStorage.STORAGE_TOPIC, new StateStorage.Listener() { @Override public void storageFileChanged(@NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) { VirtualFile file = event.getFile(); if (!file.isDirectory() && !(event.getRequestor() instanceof StateStorage.SaveSession)) { saveChangedProjectFile(file, null, storage); } } }); final ProjectManagerListener busPublisher = messageBus.syncPublisher(TOPIC); addProjectManagerListener( new ProjectManagerListener() { @Override public void projectOpened(final Project project) { MessageBus messageBus = project.getMessageBus(); MessageBusConnection connection = messageBus.connect(project); connection.subscribe(StateStorage.STORAGE_TOPIC, new StateStorage.Listener() { @Override public void storageFileChanged(@NotNull final VirtualFileEvent event, @NotNull final StateStorage storage) { VirtualFile file = event.getFile(); if (!file.isDirectory() && !(event.getRequestor() instanceof StateStorage.SaveSession)) { saveChangedProjectFile(file, project, storage); } } }); busPublisher.projectOpened(project); for (ProjectManagerListener listener : getListeners(project)) { listener.projectOpened(project); } } @Override public void projectClosed(Project project) { busPublisher.projectClosed(project); for (ProjectManagerListener listener : getListeners(project)) { listener.projectClosed(project); } } @Override public boolean canCloseProject(Project project) { for (ProjectManagerListener listener : getListeners(project)) { if (!listener.canCloseProject(project)) { return false; } } return true; } @Override public void projectClosing(Project project) { busPublisher.projectClosing(project); for (ProjectManagerListener listener : getListeners(project)) { listener.projectClosing(project); } } } ); registerExternalProjectFileListener(virtualFileManager); } @Override public void initComponent() { } @Override public void disposeComponent() { ApplicationManager.getApplication().assertWriteAccessAllowed(); Disposer.dispose(myChangedFilesAlarm); if (myDefaultProject != null) { Disposer.dispose(myDefaultProject); myDefaultProject = null; myDefaultProjectWasDisposed = true; } } private static final boolean LOG_PROJECT_LEAKAGE_IN_TESTS = false; private static final int MAX_LEAKY_PROJECTS = 42; @SuppressWarnings("FieldCanBeLocal") private final Map myProjects = new WeakHashMap(); @Override @Nullable public Project newProject(final String projectName, @NotNull String filePath, boolean useDefaultProjectSettings, boolean isDummy) { filePath = toCanonicalName(filePath); //noinspection ConstantConditions if (LOG_PROJECT_LEAKAGE_IN_TESTS && ApplicationManager.getApplication().isUnitTestMode()) { for (int i = 0; i < 42; i++) { if (myProjects.size() < MAX_LEAKY_PROJECTS) break; System.gc(); TimeoutUtil.sleep(100); System.gc(); } if (myProjects.size() >= MAX_LEAKY_PROJECTS) { List copy = new ArrayList(myProjects.keySet()); myProjects.clear(); throw new TooManyProjectLeakedException(copy); } } ProjectImpl project = createProject(projectName, filePath, false, ApplicationManager.getApplication().isUnitTestMode()); try { initProject(project, useDefaultProjectSettings ? (ProjectImpl)getDefaultProject() : null); if (LOG_PROJECT_LEAKAGE_IN_TESTS) { myProjects.put(project, null); } return project; } catch (Throwable t) { LOG.info(t); Messages.showErrorDialog(message(t), ProjectBundle.message("project.load.default.error")); return null; } } @NonNls private static String message(Throwable e) { String message = e.getMessage(); if (message != null) return message; message = e.getLocalizedMessage(); if (message != null) return message; message = e.toString(); Throwable cause = e.getCause(); if (cause != null) { String causeMessage = message(cause); return message + " (cause: " + causeMessage + ")"; } return message; } private void initProject(@NotNull ProjectImpl project, @Nullable ProjectImpl template) throws IOException { ProgressIndicator indicator = myProgressManager.getProgressIndicator(); if (indicator != null && !project.isDefault()) { indicator.setText(ProjectBundle.message("loading.components.for", project.getName())); indicator.setIndeterminate(true); } ApplicationManager.getApplication().getMessageBus().syncPublisher(ProjectLifecycleListener.TOPIC).beforeProjectLoaded(project); boolean succeed = false; try { if (template != null) { project.getStateStore().loadProjectFromTemplate(template); } else { project.getStateStore().load(); } project.loadProjectComponents(); project.init(); succeed = true; } finally { if (!succeed) { scheduleDispose(project); } } } private ProjectImpl createProject(@Nullable String projectName, @NotNull String filePath, boolean isDefault, boolean isOptimiseTestLoadSpeed) { return isDefault ? new DefaultProject(this, "", isOptimiseTestLoadSpeed) : new ProjectImpl(this, new File(filePath).getAbsolutePath(), isOptimiseTestLoadSpeed, projectName); } private static void scheduleDispose(final ProjectImpl project) { if (project.isDefault()) { return; } ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { if (!project.isDisposed()) { Disposer.dispose(project); } } }); } }); } @Override @Nullable public Project loadProject(@NotNull String filePath) throws IOException, JDOMException, InvalidDataException { try { ProjectImpl project = createProject(null, filePath, false, false); initProject(project, null); return project; } catch (Throwable t) { LOG.info(t); throw new IOException(t); } } @NotNull private static String toCanonicalName(@NotNull final String filePath) { try { return FileUtil.resolveShortWindowsName(filePath); } catch (IOException e) { // OK. File does not yet exist so it's canonical path will be equal to its original path. } return filePath; } @TestOnly public synchronized boolean isDefaultProjectInitialized() { return myDefaultProject != null; } @Override @NotNull public synchronized Project getDefaultProject() { LOG.assertTrue(!myDefaultProjectWasDisposed, "Default project has been already disposed!"); if (myDefaultProject == null) { ProgressManager.getInstance().executeNonCancelableSection(new Runnable() { @Override public void run() { try { myDefaultProject = createProject(null, "", true, ApplicationManager.getApplication().isUnitTestMode()); initProject(myDefaultProject, null); myDefaultProjectRootElement = null; } catch (Throwable t) { PluginManager.processException(t); } } }); } return myDefaultProject; } @Nullable public Element getDefaultProjectRootElement() { return myDefaultProjectRootElement; } @Override @NotNull public Project[] getOpenProjects() { synchronized (myOpenProjects) { if (myOpenProjectsArrayCache.length != myOpenProjects.size()) { LOG.error("Open projects: " + myOpenProjects + "; cache: " + Arrays.asList(myOpenProjectsArrayCache)); } if (myOpenProjectsArrayCache.length > 0 && myOpenProjectsArrayCache[0] != myOpenProjects.get(0)) { LOG.error("Open projects cache corrupted. Open projects: " + myOpenProjects + "; cache: " + Arrays.asList(myOpenProjectsArrayCache)); } if (ApplicationManager.getApplication().isUnitTestMode()) { Project[] testProjects = myTestProjects.toArray(new Project[myTestProjects.size()]); for (Project testProject : testProjects) { assert !testProject.isDisposed() : testProject; } return ArrayUtil.mergeArrays(myOpenProjectsArrayCache, testProjects); } return myOpenProjectsArrayCache; } } @Override public boolean isProjectOpened(Project project) { synchronized (myOpenProjects) { return ApplicationManager.getApplication().isUnitTestMode() && myTestProjects.contains(project) || myOpenProjects.contains(project); } } @Override public boolean openProject(final Project project) { if (isLight(project)) { throw new AssertionError("must not open light project"); } final Application application = ApplicationManager.getApplication(); if (!application.isUnitTestMode() && !((ProjectEx)project).getStateStore().checkVersion()) { return false; } synchronized (myOpenProjects) { if (myOpenProjects.contains(project)) { return false; } myOpenProjects.add(project); cacheOpenProjects(); } fireProjectOpened(project); DumbService.getInstance(project).queueTask(new DumbModeTask() { @Override public void performInDumbMode(@NotNull ProgressIndicator indicator) { waitForFileWatcher(indicator); } @Override public String toString() { return "wait for file watcher"; } }); final StartupManagerImpl startupManager = (StartupManagerImpl)StartupManager.getInstance(project); boolean ok = myProgressManager.runProcessWithProgressSynchronously(new Runnable() { @Override public void run() { startupManager.runStartupActivities(); // dumb mode should start before post-startup activities // only when startCacheUpdate is called from UI thread, we can guarantee that // when the method returns, the application has entered dumb mode UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { startupManager.startCacheUpdate(); } }); startupManager.runPostStartupActivitiesFromExtensions(); UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { startupManager.runPostStartupActivities(); } }); } }, ProjectBundle.message("project.load.progress"), canCancelProjectLoading(), project); if (!ok) { closeProject(project, false, false, true); notifyProjectOpenFailed(); return false; } if (!application.isHeadlessEnvironment() && !application.isUnitTestMode()) { // should be invoked last startupManager.runWhenProjectIsInitialized(new Runnable() { @Override public void run() { final TrackingPathMacroSubstitutor macroSubstitutor = ((ProjectEx)project).getStateStore().getStateStorageManager().getMacroSubstitutor(); if (macroSubstitutor != null) { StorageUtil.notifyUnknownMacros(macroSubstitutor, project, null); } } }); } return true; } private static boolean canCancelProjectLoading() { ProgressIndicator indicator = ProgressIndicatorProvider.getGlobalProgressIndicator(); return !(indicator instanceof NonCancelableSection); } private void cacheOpenProjects() { myOpenProjectsArrayCache = myOpenProjects.toArray(new Project[myOpenProjects.size()]); } private static void waitForFileWatcher(ProgressIndicator indicator) { LocalFileSystem fs = LocalFileSystem.getInstance(); if (!(fs instanceof LocalFileSystemImpl)) return; final FileWatcher watcher = ((LocalFileSystemImpl)fs).getFileWatcher(); if (!watcher.isOperational() || !watcher.isSettingRoots()) return; LOG.info("FW/roots waiting started"); indicator.setIndeterminate(true); indicator.setText(ProjectBundle.message("project.load.waiting.watcher")); if (indicator instanceof ProgressWindow) { ((ProgressWindow)indicator).setCancelButtonText(CommonBundle.message("button.skip")); } while (watcher.isSettingRoots() && !indicator.isCanceled()) { TimeoutUtil.sleep(10); } LOG.info("FW/roots waiting finished"); } @Override public Project loadAndOpenProject(@NotNull final String filePath) throws IOException { final Project project = convertAndLoadProject(filePath); if (project == null) { WelcomeFrame.showIfNoProjectOpened(); return null; } // todo unify this logic with PlatformProjectOpenProcessor if (!openProject(project)) { WelcomeFrame.showIfNoProjectOpened(); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { Disposer.dispose(project); } }); } return project; } /** * Converts and loads the project at the specified path. * * @param filePath the path to open the project. * @return the project, or null if the user has cancelled opening the project. */ @Override @Nullable public Project convertAndLoadProject(String filePath) throws IOException { final String fp = toCanonicalName(filePath); final ConversionResult conversionResult = ConversionService.getInstance().convert(fp); if (conversionResult.openingIsCanceled()) { return null; } final Project project; try { project = loadProjectWithProgress(filePath); if (project == null) return null; } catch (IOException e) { LOG.info(e); throw e; } catch (Throwable t) { LOG.info(t); throw new IOException(t); } if (!conversionResult.conversionNotNeeded()) { StartupManager.getInstance(project).registerPostStartupActivity(new Runnable() { @Override public void run() { conversionResult.postStartupActivity(project); } }); } return project; } /** * Opens the project at the specified path. * * @param filePath the path to open the project. * @return the project, or null if the user has cancelled opening the project. */ @Nullable private Project loadProjectWithProgress(@NotNull final String filePath) throws IOException { final ProjectImpl project = createProject(null, toCanonicalName(filePath), false, false); try { myProgressManager.runProcessWithProgressSynchronously(new ThrowableComputable() { @Override @Nullable public Project compute() throws IOException { initProject(project, null); return project; } }, ProjectBundle.message("project.load.progress"), canCancelProjectLoading(), project); } catch (StateStorageException e) { throw new IOException(e); } catch (ProcessCanceledException ignore) { return null; } return project; } private static void notifyProjectOpenFailed() { ApplicationManager.getApplication().getMessageBus().syncPublisher(AppLifecycleListener.TOPIC).projectOpenFailed(); WelcomeFrame.showIfNoProjectOpened(); } private void registerExternalProjectFileListener(VirtualFileManager virtualFileManager) { virtualFileManager.addVirtualFileManagerListener(new VirtualFileManagerListener() { @Override public void beforeRefreshStart(boolean asynchronous) { } @Override public void afterRefreshFinish(boolean asynchronous) { scheduleReloadApplicationAndProject(); } }); } private void askToReloadProjectIfConfigFilesChangedExternally() { LOG.debug("[RELOAD] myReloadBlockCount = " + myReloadBlockCount.get()); if (myReloadBlockCount.get() == 0) { Set projects; synchronized (myChangedProjectFiles) { if (myChangedProjectFiles.isEmpty()) return; projects = new HashSet(myChangedProjectFiles.keySet()); } List projectsToReload = new ArrayList(); for (Project project : projects) { if (shouldReloadProject(project)) { projectsToReload.add(project); } } for (final Project projectToReload : projectsToReload) { reloadProjectImpl(projectToReload, false); } } } private boolean tryToReloadApplication() { try { final Application app = ApplicationManager.getApplication(); if (app.isDisposed()) return false; final HashSet> causes = new HashSet>(myChangedApplicationFiles); if (causes.isEmpty()) return true; final boolean[] reloadOk = {false}; final LinkedHashSet components = new LinkedHashSet(); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { try { reloadOk[0] = ((ApplicationImpl)app).getStateStore().reload(causes, components); } catch (StateStorageException e) { Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } catch (IOException e) { Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } } }); if (!reloadOk[0] && !components.isEmpty()) { String message = "Application components were changed externally and cannot be reloaded:\n"; for (String component : components) { message += component + "\n"; } final boolean canRestart = ApplicationManager.getApplication().isRestartCapable(); message += "Would you like to " + (canRestart ? "restart " : "shutdown "); message += ApplicationNamesInfo.getInstance().getProductName() + "?"; if (Messages.showYesNoDialog(message, "Application Configuration Reload", Messages.getQuestionIcon()) == Messages.YES) { for (Pair cause : causes) { StateStorage stateStorage = cause.getSecond(); if (stateStorage instanceof XmlElementStorage) { ((XmlElementStorage)stateStorage).disableSaving(); } } ApplicationManagerEx.getApplicationEx().restart(true); } } return reloadOk[0]; } finally { myChangedApplicationFiles.clear(); } } private boolean shouldReloadProject(final Project project) { if (project.isDisposed()) return false; final HashSet> causes = new HashSet>(); synchronized (myChangedProjectFiles) { final List> changes = myChangedProjectFiles.remove(project); if (changes != null) { causes.addAll(changes); } if (causes.isEmpty()) return false; } final boolean[] reloadOk = {false}; ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { try { LOG.debug("[RELOAD] Reloading project/components..."); reloadOk[0] = ((ProjectEx)project).getStateStore().reload(causes); } catch (StateStorageException e) { Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } catch (IOException e) { Messages.showWarningDialog(ProjectBundle.message("project.reload.failed", e.getMessage()), ProjectBundle.message("project.reload.failed.title")); } } }); if (reloadOk[0]) return false; String message; if (causes.size() == 1) { message = ProjectBundle.message("project.reload.external.change.single", causes.iterator().next().first.getPresentableUrl()); } else { StringBuilder filesBuilder = new StringBuilder(); boolean first = true; Set alreadyShown = new HashSet(); for (Pair cause : causes) { String url = cause.first.getPresentableUrl(); if (!alreadyShown.contains(url)) { if (alreadyShown.size() > 10) { filesBuilder.append("\n" + "and ").append(causes.size() - alreadyShown.size()).append(" more"); break; } if (!first) filesBuilder.append("\n"); first = false; filesBuilder.append(url); alreadyShown.add(url); } } message = ProjectBundle.message("project.reload.external.change.multiple", filesBuilder.toString()); } return Messages.showDialog(message, ProjectBundle.message("project.reload.external.change.title"), new String[]{"&Reload Project", "&Discard Changes"}, -1, Messages.getQuestionIcon()) == 0; } @Override public boolean isFileSavedToBeReloaded(VirtualFile candidate) { return mySavedCopies.containsKey(candidate); } @Override public void blockReloadingProjectOnExternalChanges() { myReloadBlockCount.incrementAndGet(); } @Override public void unblockReloadingProjectOnExternalChanges() { if (myReloadBlockCount.decrementAndGet() == 0) scheduleReloadApplicationAndProject(); } private void scheduleReloadApplicationAndProject() { // todo: commented due to "IDEA-61938 Libraries configuration is kept if switching branches" // because of save which may happen _before_ project reload ;( //ApplicationManager.getApplication().invokeLater(new Runnable() { // public void run() { //IdeEventQueue.getInstance().addIdleListener(new Runnable() { // @Override // public void run() { // IdeEventQueue.getInstance().removeIdleListener(this); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (!tryToReloadApplication()) return; askToReloadProjectIfConfigFilesChangedExternally(); } }, ModalityState.NON_MODAL); //} //}, 2000); //} //}, ModalityState.NON_MODAL); } @Override public void openTestProject(@NotNull final Project project) { synchronized (myOpenProjects) { assert ApplicationManager.getApplication().isUnitTestMode(); assert !project.isDisposed() : "Must not open already disposed project"; myTestProjects.add(project); } } @Override public Collection closeTestProject(@NotNull Project project) { synchronized (myOpenProjects) { assert ApplicationManager.getApplication().isUnitTestMode(); myTestProjects.remove(project); return myTestProjects; } } @Override public void saveChangedProjectFile(final VirtualFile file, final Project project) { if (file.exists()) { copyToTemp(file); } registerProjectToReload(project, file, null); } private void saveChangedProjectFile(final VirtualFile file, @Nullable final Project project, final StateStorage storage) { if (file.exists()) { copyToTemp(file); } registerProjectToReload(project, file, storage); } private void registerProjectToReload(@Nullable final Project project, final VirtualFile cause, @Nullable final StateStorage storage) { if (LOG.isDebugEnabled()) { LOG.debug("[RELOAD] Registering project to reload: " + cause, new Exception()); } if (project != null) { synchronized (myChangedProjectFiles) { List> changedProjectFiles = myChangedProjectFiles.get(project); if (changedProjectFiles == null) { changedProjectFiles = new ArrayList>(); myChangedProjectFiles.put(project, changedProjectFiles); } changedProjectFiles.add(Pair.create(cause, storage)); } } else { myChangedApplicationFiles.add(Pair.create(cause, storage)); } myChangedFilesAlarm.cancelAllRequests(); myChangedFilesAlarm.addRequest(new Runnable() { @Override public void run() { LOG.debug("[RELOAD] Scheduling reload application & project, myReloadBlockCount = " + myReloadBlockCount); if (myReloadBlockCount.get() == 0) { scheduleReloadApplicationAndProject(); } } }, 444); } private void copyToTemp(VirtualFile file) { try { final byte[] bytes = file.contentsToByteArray(); mySavedCopies.put(file, bytes); mySavedTimestamps.put(file, file.getTimeStamp()); } catch (IOException e) { LOG.error(e); } } private void restoreCopy(VirtualFile file) { try { if (file == null) return; // Externally deleted actually. if (!file.isWritable()) return; // IDEA was unable to save it as well. So no need to restore. final byte[] bytes = mySavedCopies.get(file); if (bytes != null) { try { file.setBinaryContent(bytes, -1, mySavedTimestamps.get(file)); } catch (IOException e) { Messages.showWarningDialog(ProjectBundle.message("project.reload.write.failed", file.getPresentableUrl()), ProjectBundle.message("project.reload.write.failed.title")); } } } finally { mySavedCopies.remove(file); mySavedTimestamps.remove(file); } } @Override public void reloadProject(@NotNull final Project p) { reloadProjectImpl(p, true); } public void reloadProjectImpl(@NotNull final Project p, final boolean clearCopyToRestore) { if (clearCopyToRestore) { mySavedCopies.clear(); mySavedTimestamps.clear(); } final Project[] project = {p}; ProjectReloadState.getInstance(project[0]).onBeforeAutomaticProjectReload(); final Application application = ApplicationManager.getApplication(); application.invokeLater(new Runnable() { @Override public void run() { LOG.debug("Reloading project."); ProjectImpl projectImpl = (ProjectImpl)project[0]; if (projectImpl.isDisposed()) return; IProjectStore projectStore = projectImpl.getStateStore(); final String location = projectImpl.getPresentableUrl(); final List original; try { IComponentStore.SaveSession saveSession = projectStore.startSave(); original = saveSession.getAllStorageFiles(true); saveSession.finishSave(); } catch (IOException e) { LOG.error(e); return; } if (project[0].isDisposed() || ProjectUtil.closeAndDispose(project[0])) { application.runWriteAction(new Runnable() { @Override public void run() { for (File originalFile : original) { restoreCopy(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(originalFile)); } } }); project[0] = null; // Let it go. ProjectUtil.openProject(location, null, true); } } }, ModalityState.NON_MODAL); } @Override public boolean closeProject(@NotNull final Project project) { return closeProject(project, true, false, true); } public boolean closeProject(@NotNull final Project project, final boolean save, final boolean dispose, boolean checkCanClose) { if (isLight(project)) { throw new AssertionError("must not close light project"); } if (!isProjectOpened(project)) return true; if (checkCanClose && !canClose(project)) return false; final ShutDownTracker shutDownTracker = ShutDownTracker.getInstance(); shutDownTracker.registerStopperThread(Thread.currentThread()); try { if (save) { FileDocumentManager.getInstance().saveAllDocuments(); project.save(); } if (checkCanClose && !ensureCouldCloseIfUnableToSave(project)) { return false; } fireProjectClosing(project); // somebody can start progress here, do not wrap in write action ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { synchronized (myOpenProjects) { myOpenProjects.remove(project); cacheOpenProjects(); myTestProjects.remove(project); } myChangedProjectFiles.remove(project); fireProjectClosed(project); if (dispose) { Disposer.dispose(project); } } }); } finally { shutDownTracker.unregisterStopperThread(Thread.currentThread()); } return true; } public static boolean isLight(@NotNull Project project) { return ApplicationManager.getApplication().isUnitTestMode() && project.toString().contains("light_temp_"); } @Override public boolean closeAndDispose(@NotNull final Project project) { return closeProject(project, true, true, true); } private void fireProjectClosing(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("enter: fireProjectClosing()"); } for (ProjectManagerListener listener : myListeners) { try { listener.projectClosing(project); } catch (Exception e) { LOG.error(e); } } } @Override public void addProjectManagerListener(@NotNull ProjectManagerListener listener) { myListeners.add(listener); } @Override public void addProjectManagerListener(@NotNull final ProjectManagerListener listener, @NotNull Disposable parentDisposable) { addProjectManagerListener(listener); Disposer.register(parentDisposable, new Disposable() { @Override public void dispose() { removeProjectManagerListener(listener); } }); } @Override public void removeProjectManagerListener(@NotNull ProjectManagerListener listener) { boolean removed = myListeners.remove(listener); LOG.assertTrue(removed); } @Override public void addProjectManagerListener(@NotNull Project project, @NotNull ProjectManagerListener listener) { List listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY); if (listeners == null) { listeners = ((UserDataHolderEx)project) .putUserDataIfAbsent(LISTENERS_IN_PROJECT_KEY, ContainerUtil.createLockFreeCopyOnWriteList()); } listeners.add(listener); } @Override public void removeProjectManagerListener(@NotNull Project project, @NotNull ProjectManagerListener listener) { List listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY); LOG.assertTrue(listeners != null); boolean removed = listeners.remove(listener); LOG.assertTrue(removed); } private void fireProjectOpened(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("projectOpened"); } for (ProjectManagerListener listener : myListeners) { try { listener.projectOpened(project); } catch (Exception e) { LOG.error(e); } } } private void fireProjectClosed(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("projectClosed"); } for (ProjectManagerListener listener : myListeners) { try { listener.projectClosed(project); } catch (Exception e) { LOG.error(e); } } } @Override public boolean canClose(Project project) { if (LOG.isDebugEnabled()) { LOG.debug("enter: canClose()"); } for (ProjectManagerListener listener : myListeners) { try { if (!listener.canCloseProject(project)) return false; } catch (Throwable e) { LOG.warn(e); // DO NOT LET ANY PLUGIN to prevent closing due to exception } } return true; } private static boolean ensureCouldCloseIfUnableToSave(@NotNull final Project project) { final ProjectImpl.UnableToSaveProjectNotification[] notifications = NotificationsManager.getNotificationsManager().getNotificationsOfType(ProjectImpl.UnableToSaveProjectNotification.class, project); if (notifications.length == 0) return true; final String fileNames = StringUtil.join(notifications[0].getFileNames(), "\n"); final String msg = String.format("%s was unable to save some project files,\nare you sure you want to close this project anyway?", ApplicationNamesInfo.getInstance().getProductName()); return Messages.showDialog(project, msg, "Unsaved Project", "Read-only files:\n\n" + fileNames, new String[]{"Yes", "No"}, 0, 1, Messages.getWarningIcon()) == 0; } @Override public void writeExternal(Element parentNode) { if (myDefaultProject != null) { myDefaultProject.save(); } if (myDefaultProjectRootElement != null) { myDefaultProjectRootElement.detach(); parentNode.addContent(myDefaultProjectRootElement); } } public void setDefaultProjectRootElement(final Element defaultProjectRootElement) { myDefaultProjectRootElement = defaultProjectRootElement; } @Override public void readExternal(Element parentNode) { myDefaultProjectRootElement = parentNode.getChild(ELEMENT_DEFAULT_PROJECT); if (myDefaultProjectRootElement != null) { myDefaultProjectRootElement.detach(); } } @Override public String getExternalFileName() { return "project.default"; } @Override @NotNull public String getComponentName() { return "ProjectManager"; } @Override @NotNull public File[] getExportFiles() { return new File[]{PathManager.getOptionsFile(this)}; } @Override @NotNull public String getPresentableName() { return ProjectBundle.message("project.default.settings"); } }