| /* |
| * 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<List<ProjectManagerListener>> 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<Project> myOpenProjects = new ArrayList<Project>(); |
| private Project[] myOpenProjectsArrayCache = {}; |
| private final List<ProjectManagerListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); |
| |
| private final Set<Project> myTestProjects = new THashSet<Project>(); |
| |
| private final Map<VirtualFile, byte[]> mySavedCopies = new HashMap<VirtualFile, byte[]>(); |
| private final TObjectLongHashMap<VirtualFile> mySavedTimestamps = new TObjectLongHashMap<VirtualFile>(); |
| private final Map<Project, List<Pair<VirtualFile, StateStorage>>> myChangedProjectFiles = |
| new HashMap<Project, List<Pair<VirtualFile, StateStorage>>>(); |
| private final Alarm myChangedFilesAlarm = new Alarm(); |
| private final List<Pair<VirtualFile, StateStorage>> myChangedApplicationFiles = new ArrayList<Pair<VirtualFile, StateStorage>>(); |
| private final AtomicInteger myReloadBlockCount = new AtomicInteger(0); |
| private final ProgressManager myProgressManager; |
| private volatile boolean myDefaultProjectWasDisposed = false; |
| |
| @NotNull |
| private static List<ProjectManagerListener> getListeners(Project project) { |
| List<ProjectManagerListener> 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<Project, String> myProjects = new WeakHashMap<Project, String>(); |
| |
| @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<Project> copy = new ArrayList<Project>(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<Project, IOException>() { |
| @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<Project> projects; |
| |
| synchronized (myChangedProjectFiles) { |
| if (myChangedProjectFiles.isEmpty()) return; |
| projects = new HashSet<Project>(myChangedProjectFiles.keySet()); |
| } |
| |
| List<Project> projectsToReload = new ArrayList<Project>(); |
| |
| 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<Pair<VirtualFile, StateStorage>> causes = new HashSet<Pair<VirtualFile, StateStorage>>(myChangedApplicationFiles); |
| if (causes.isEmpty()) return true; |
| |
| final boolean[] reloadOk = {false}; |
| final LinkedHashSet<String> components = new LinkedHashSet<String>(); |
| |
| 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<VirtualFile, StateStorage> 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<Pair<VirtualFile, StateStorage>> causes = new HashSet<Pair<VirtualFile, StateStorage>>(); |
| |
| synchronized (myChangedProjectFiles) { |
| final List<Pair<VirtualFile, StateStorage>> 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<String> alreadyShown = new HashSet<String>(); |
| for (Pair<VirtualFile, StateStorage> 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<Project> 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<Pair<VirtualFile, StateStorage>> changedProjectFiles = myChangedProjectFiles.get(project); |
| if (changedProjectFiles == null) { |
| changedProjectFiles = new ArrayList<Pair<VirtualFile, StateStorage>>(); |
| 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<File> 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<ProjectManagerListener> listeners = project.getUserData(LISTENERS_IN_PROJECT_KEY); |
| if (listeners == null) { |
| listeners = ((UserDataHolderEx)project) |
| .putUserDataIfAbsent(LISTENERS_IN_PROJECT_KEY, ContainerUtil.<ProjectManagerListener>createLockFreeCopyOnWriteList()); |
| } |
| listeners.add(listener); |
| } |
| |
| @Override |
| public void removeProjectManagerListener(@NotNull Project project, @NotNull ProjectManagerListener listener) { |
| List<ProjectManagerListener> 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"); |
| } |
| } |