From 9782f044c75004609e2eef2caf43dcff75a84012 Mon Sep 17 00:00:00 2001 From: John Cortell Date: Thu, 14 Apr 2011 20:31:22 +0000 Subject: [PATCH] Bug 342141 - Executables view content goes stale in various scenarios --- .../debug/core/executables/Executable.java | 31 +- .../core/executables/ExecutablesManager.java | 799 ++++++++++++------ .../IExecutablesChangeListener.java | 56 +- .../IExecutablesChangeListener2.java | 37 + .../StandardExecutableImporter.java | 2 +- .../StandardSourceFilesProvider.java | 2 +- .../core/srcfinder/CSourceFinder.java | 33 +- .../ExecutablesContentProvider.java | 46 +- .../ui/views/executables/ExecutablesView.java | 13 +- .../views/executables/ExecutablesViewer.java | 53 +- .../ui/views/executables/Messages.java | 1 + .../ui/views/executables/Messages.properties | 1 + .../SourceFilesContentProvider.java | 405 +++++++-- .../views/executables/SourceFilesViewer.java | 122 +-- 14 files changed, 1075 insertions(+), 526 deletions(-) create mode 100644 debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener2.java diff --git a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/Executable.java b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/Executable.java index 124596bb58f..2cb2b47fdf9 100644 --- a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/Executable.java +++ b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/Executable.java @@ -22,6 +22,7 @@ import org.eclipse.cdt.core.CCorePlugin; import org.eclipse.cdt.core.model.CoreModel; import org.eclipse.cdt.core.model.ICProject; import org.eclipse.cdt.core.model.ITranslationUnit; +import org.eclipse.cdt.debug.internal.core.Trace; import org.eclipse.cdt.internal.core.model.CModelManager; import org.eclipse.cdt.internal.core.model.ExternalTranslationUnit; import org.eclipse.cdt.internal.core.model.TranslationUnit; @@ -40,7 +41,29 @@ import org.eclipse.core.runtime.content.IContentTypeManager; public class Executable extends PlatformObject { + /** + * Poorly named. This does not determine if the the file is an executable + * but rather a binary. Use {@link #isBinaryFile(IPath)} instead. + * + * @deprecated use {@link #isBinaryFile(IPath)} + */ + @Deprecated static public boolean isExecutableFile(IPath path) { + return isBinaryFile(path); + } + + /** + * Determines if the given file is a binary file. For our purposes, an + * "executable" is a runnable program (an .exe file on Windows, e.g.,) or a + * shared library. A binary can be an executable but it can also be an + * instruction-containing artifact of a build, which typically is linked to + * make an executable (.e.,g .o and .obj files) + * + * @param path + * @return + * @since 7.1 + */ + static public boolean isBinaryFile(IPath path) { // ignore directories if (path.toFile().isDirectory()) { return false; @@ -159,9 +182,12 @@ public class Executable extends PlatformObject { * @since 6.0 */ public synchronized ITranslationUnit[] getSourceFiles(IProgressMonitor monitor) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null); - if (!refreshSourceFiles && !remapSourceFiles) + if (!refreshSourceFiles && !remapSourceFiles) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "returning cached result"); //$NON-NLS-1$ return sourceFiles.toArray(new TranslationUnit[sourceFiles.size()]) ; + } // Try to get the list of source files used to build the binary from the // symbol information. @@ -274,6 +300,8 @@ public class Executable extends PlatformObject { * @since 6.0 */ public void setRefreshSourceFiles(boolean refreshSourceFiles) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, refreshSourceFiles); + this.refreshSourceFiles = refreshSourceFiles; } @@ -309,6 +337,7 @@ public class Executable extends PlatformObject { * @since 7.0 */ public void setRemapSourceFiles(boolean remapSourceFiles) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, remapSourceFiles); this.remapSourceFiles = remapSourceFiles; } diff --git a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/ExecutablesManager.java b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/ExecutablesManager.java index ff813480c14..eeab24b4c92 100644 --- a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/ExecutablesManager.java +++ b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/ExecutablesManager.java @@ -11,31 +11,36 @@ package org.eclipse.cdt.debug.core.executables; -import java.text.DateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.eclipse.cdt.core.model.CoreModel; +import org.eclipse.cdt.core.model.ElementChangedEvent; +import org.eclipse.cdt.core.model.IBinary; +import org.eclipse.cdt.core.model.ICElement; +import org.eclipse.cdt.core.model.ICElementDelta; +import org.eclipse.cdt.core.model.ICProject; +import org.eclipse.cdt.core.model.IElementChangedListener; import org.eclipse.cdt.core.settings.model.CProjectDescriptionEvent; import org.eclipse.cdt.core.settings.model.ICProjectDescription; import org.eclipse.cdt.core.settings.model.ICProjectDescriptionListener; import org.eclipse.cdt.debug.core.CDebugCorePlugin; +import org.eclipse.cdt.debug.internal.core.Trace; import org.eclipse.cdt.debug.internal.core.executables.StandardExecutableImporter; import org.eclipse.cdt.debug.internal.core.executables.StandardSourceFileRemappingFactory; import org.eclipse.cdt.debug.internal.core.executables.StandardSourceFilesProvider; +import org.eclipse.cdt.internal.core.model.CModelManager; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; -import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; -import org.eclipse.core.resources.IResourceChangeListener; -import org.eclipse.core.resources.IResourceDelta; -import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; @@ -52,9 +57,11 @@ import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.osgi.service.debug.DebugOptions; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationListener; +import org.eclipse.debug.core.sourcelookup.ISourceLookupDirector; +import org.eclipse.debug.core.sourcelookup.ISourceLookupParticipant; /** * The Executables Manager maintains a collection of executables built by all of @@ -64,35 +71,119 @@ import org.osgi.framework.ServiceReference; * @author Ken Ryall * */ -public class ExecutablesManager extends PlatformObject implements IResourceChangeListener, ICProjectDescriptionListener { +public class ExecutablesManager extends PlatformObject implements ICProjectDescriptionListener, IElementChangedListener { private static final String EXECUTABLES_MANAGER_DEBUG_TRACING = CDebugCorePlugin.PLUGIN_ID + "EXECUTABLES_MANAGER_DEBUG_TRACING"; //$NON-NLS-1$ private Map executablesProviderMap = new HashMap(); - private Map> executablesMap = new HashMap>(); private List changeListeners = Collections.synchronizedList(new ArrayList()); private List executableProviders; private List sourceFileProviders; private List sourceFileRemappingFactories; private List executableImporters; - private boolean DEBUG; - private Job refreshJob = new Job("Get Executables") { //$NON-NLS-1$ + /** + * Map of launch config names to the path locator memento string in the + * launch config, recorded in the most recent launch config change + * notification. We use this to ensure we flush source file mappings only + * when the launch config change involves a change to the source locators. + */ + private Map locatorMementos = new HashMap(); + + /** + * A cache of the executables in the workspace, categorized by project. + * + *

+ * This cache is updated by scheduling an asynchronous search. SearchJob is + * the only class that should modify this collection, including the + * sub collections of Executable objects. The collection can be read from + * any thread at any time. All access (read or write) must be serialized by + * synchronizing on the Map object. + *

+ * The same Executable may appear more than once. + */ + private Map> executablesMap = new HashMap>(); + + /** + * Provide a flat list of the executables in {@link #executablesMap}, with + * duplicates removed. That is effectively the list of all executables in + * the workspace that we know of as of now. + * + * @return + */ + private List flattenExecutablesMap() { + List result = new ArrayList(executablesMap.size() * 5); // most projects will have less than five executables + synchronized (executablesMap) { + for (List exes : executablesMap.values()) { + for (Executable exe : exes) { + if (!result.contains(exe)) { + result.add(exe); + } + } + } + } + return result; + } + + /** + * Job which searches through CDT projects for executables. Only one thread + * should be running this job at any one time. Running job should be + * cancelled and verified terminated before initiating another. + */ + class SearchJob extends Job { + SearchJob() { + super("Executables Search"); //$NON-NLS-1$ + } + + + /** + * The projects given to us when scheduled. If null, flush our entire + * cache and search all projects + */ + private IProject[] projectsToRefresh; + @Override public IStatus run(IProgressMonitor monitor) { - - trace("Get Executables job started at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Search for executables started"); //$NON-NLS-1$ - List projects = getProjectsToCheck(); + IStatus status = Status.OK_STATUS; + + // The executables we know of now. We'll compare the search results + // to this and see if we need to notify change listeners + List before = flattenExecutablesMap(); + + // Get the CDT projects in the workspace that we have no cached + // results for (are not in 'executablesMap'). Also, we may have been + // asked to refresh the cache for some projects we've search before + List projects = new ArrayList(); + synchronized (executablesMap) { + if (projectsToRefresh == null) { + executablesMap.clear(); + } + else { + for (IProject project : projectsToRefresh) { + executablesMap.remove(project); + } + } + + // Get the list of projects we plan to search + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (!executablesMap.containsKey(project) && CoreModel.hasCNature(project)) { + projects.add(project); + } + } + } + SubMonitor subMonitor = SubMonitor.convert(monitor, projects.size()); for (IProject project : projects) { if (subMonitor.isCanceled()) { - trace("Get Executables job cancelled at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ - return Status.CANCEL_STATUS; + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Search for executables canceled"); //$NON-NLS-1$ + status = Status.CANCEL_STATUS; + break; // we've already changed our model; stop searching but proceed to notify listeners that the model changed } subMonitor.subTask("Checking project: " + project.getName()); //$NON-NLS-1$ @@ -100,36 +191,78 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang // get the executables provider for this project IProjectExecutablesProvider provider = getExecutablesProviderForProject(project); if (provider != null) { - trace("Getting executables for project: " + project.getName() + " using " + provider.toString()); //$NON-NLS-1$//$NON-NLS-2$ + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Getting executables for project: " + project.getName() + " using " + provider.toString()); //$NON-NLS-1$ //$NON-NLS-2$ List executables = provider.getExecutables(project, subMonitor.newChild(1, SubMonitor.SUPPRESS_NONE)); // store the list of executables for this project synchronized (executablesMap) { - if (!monitor.isCanceled()) { - executablesMap.put(project, executables); - } + executablesMap.put(project, executables); } } } + + // See if, after all that work, there's a net change in the + // executables list. If so, notify listeners. + List after = flattenExecutablesMap(); + List removed = before; + List added = new ArrayList(after.size()); + for (Executable a : after) { + if (!removed.remove(a)) { + added.add(a); + } + } // notify the listeners synchronized (changeListeners) { - for (IExecutablesChangeListener listener : changeListeners) { - listener.executablesListChanged(); + if (removed.size() > 0 || added.size() > 0) { + for (IExecutablesChangeListener listener : changeListeners) { + // New interface + if (listener instanceof IExecutablesChangeListener2) { + if (removed.size() > 0) { + ((IExecutablesChangeListener2)listener).executablesRemoved(removed); + } + if (added.size() > 0) { + ((IExecutablesChangeListener2)listener).executablesAdded(added); + } + } + // Old interface + listener.executablesListChanged(); + } } } - trace("Get Executables job finished at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Search for executables finished"); //$NON-NLS-1$ - return Status.OK_STATUS; + return status; + } + + /** + * Schedules the search job. Use this, not the standard Job.schedule() + * method. + * + * @param projectsToRefresh + * if null, all CDT projects in the workspace are searched. + * If not null, we search only newly present projects and the + * projects provided (even if searched before). Empty list + * can be passed to search only newly present projects. + */ + public void schedule(IProject[] projectsToRefresh) { + this.projectsToRefresh = projectsToRefresh; + super.schedule(); } }; + + /** The search job. We only let one of these run at any one time */ + private SearchJob searchJob = new SearchJob(); - private static ExecutablesManager executablesManager = null; + /** Lock used to serialize the search jobs */ + private Object searchSchedulingLock = new Object(); + + /** The singleton */ + private static ExecutablesManager executablesManager; /** - * Get the executables manager instance - * @return the executables manager + * @return the singleton manager */ public static ExecutablesManager getExecutablesManager() { if (executablesManager == null) @@ -138,25 +271,7 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang } public ExecutablesManager() { - - // check if debugging is enabled - BundleContext context = CDebugCorePlugin.getDefault().getBundle().getBundleContext(); - if (context != null) { - ServiceReference reference = CDebugCorePlugin.getDefault().getBundle().getBundleContext().getServiceReference(DebugOptions.class.getName()); - if (reference != null) { - DebugOptions service = (DebugOptions) context.getService(reference); - if (service != null) { - try { - DEBUG = service.getBooleanOption(EXECUTABLES_MANAGER_DEBUG_TRACING, false); - } finally { - // we have what we want - release the service - context.ungetService(reference); - } - } - } - } - - refreshJob.setPriority(Job.SHORT); + searchJob.setPriority(Job.SHORT); // load the extension points loadExecutableProviderExtensions(); @@ -171,12 +286,101 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang executableImporters.add(0, new StandardExecutableImporter()); // listen for events we're interested in - ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.POST_BUILD); + CModelManager.getDefault().addElementChangedListener(this); CoreModel.getDefault().getProjectDescriptionManager().addCProjectDescriptionListener(this, CProjectDescriptionEvent.APPLIED); + + // Listen for changes to the global source locators. These locators + // affect how source files are found locally. The Executable objects + // cache their local source file paths and rely on us to tell them to + // flush those caches when applicable locators change. + CDebugCorePlugin.getDefault().getCommonSourceLookupDirector().addParticipants(new ISourceLookupParticipant[] { new ISourceLookupParticipant(){ + + public void init(ISourceLookupDirector director) {} + public Object[] findSourceElements(Object object) { return new Object[0]; } + public String getSourceName(Object object) throws CoreException { return ""; } //$NON-NLS-1$ + public void dispose() {} + public void sourceContainersChanged(ISourceLookupDirector director) { + // Unfortunately, it would be extremely difficult/costly to + // determine which binaries are effected by the source locator + // change, so we have to tell all Executables to flush + flushExecutablesSourceMappings(); + } + } }); + + // Source locators are also in launch configurations, and those too come + // into play when an Executable looks for a source file locally. So, + // listen for changes in those locators, too. + DebugPlugin.getDefault().getLaunchManager().addLaunchConfigurationListener(new ILaunchConfigurationListener() { + public void launchConfigurationChanged(ILaunchConfiguration configuration) { + // Expect lots of noise for working copies. We only care about + // changes to actual configs + if (configuration.isWorkingCopy()) { + return; + } + + // If the source locators in the launch config were not modified, then no-op + try { + String configName = configuration.getName(); + String mementoBefore = locatorMementos.get(configName); + String mementoNow = configuration.getAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_MEMENTO, ""); //$NON-NLS-1$ + if (mementoNow.equals(mementoBefore)) { + return; // launch config change had no affect on source locators + } + locatorMementos.put(configName, mementoNow); + } catch (CoreException e) { + CDebugCorePlugin.log(e); + } + + // TODO: For now, just tell all Executables to flush. Look + // into identifying which binary the config is associated + // with so we can flush only that Executable + flushExecutablesSourceMappings(); + } + public void launchConfigurationRemoved(ILaunchConfiguration configuration) { configAddedOrRemoved(configuration); } + public void launchConfigurationAdded(ILaunchConfiguration configuration) { configAddedOrRemoved(configuration); } + private void configAddedOrRemoved(ILaunchConfiguration configuration) { + // Expect lots of noise for working copies. We only care about + // changes to actual configs + if (configuration.isWorkingCopy()) { + return; + } + + // The addition or removal of a launch config could affect + // how files are found. It would be extremely costly to + // determine here whether it will or not, so assume it will. + + // TODO: For now, just tell all Executables to flush. Look + // into identifying which binary the config is associated + // with so we can flush only that Executable + flushExecutablesSourceMappings(); + } + }); // schedule a refresh so we get up to date - scheduleRefresh(); + scheduleExecutableSearch(null); + } + + /** + * Tell all Executable objects to flush their source file mappings, then + * notify our listeners that the executables changed. Even though the + * binaries may not have actually changed, the impact to a client of + * Executable is the same. If the client has cached any of the source file + * information the Executable provided, that info can no longer be trusted. + * The primary purpose of an Executable is to provide source file path + * information--not only the compile paths burned into the executable but + * also the local mappings of those paths. + */ + private void flushExecutablesSourceMappings() { + List exes = flattenExecutablesMap(); + for (Executable exe : exes) { + exe.setRemapSourceFiles(true); + } + synchronized (changeListeners) { + for (IExecutablesChangeListener listener : changeListeners) { + listener.executablesChanged(exes); + } + } } /** @@ -196,44 +400,38 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang } /** - * Gets the list of executables in the workspace. - * @param wait whether or not to wait if the list is being refreshed when this - * method is called. when true, this call will not return until the list is - * complete. when false, it will return with the last known list. if calling - * from any UI, you should not block the UI waiting for this to return, but rather - * register as an {@link IExecutablesChangeListener} to get notifications when the - * list changes. - * @return the list of executables which may be empty + * Gets the list of executables in the workspace. This method doesn't + * initiate a search. It returns the cached results of the most recent + * search, or waits for the ongoing search to complete. + * + * @param wait + * Whether or not to wait if the cache is in the process of being + * updated when this method is called. When true, the call will + * block until the update is complete. When false, it will return + * the current cache. Callers on the UI thread should pass false + * to avoid temporarily freezing the UI. Note that clients can + * register as a {@link IExecutablesChangeListener} or + * {@link IExecutablesChangeListener2}to get notifications when + * the cache changes. + * @return the list of executables; may be empty. List will not have + * duplicates. * @since 7.0 */ public Collection getExecutables(boolean wait) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, Boolean.valueOf(wait)); - trace("getExecutables called at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ - - List executables = new ArrayList(); - - if (wait && refreshJob.getState() != Job.NONE) { - trace("waiting for refresh job to finish at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ + // Wait for running search to finish, if asked to + if (wait && searchJob.getState() != Job.NONE) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Waiting for executable search to finish..."); //$NON-NLS-1$ try { - refreshJob.join(); + searchJob.join(); } catch (InterruptedException e) { } - trace("refresh job finished at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "...executable search finished."); //$NON-NLS-1$ } - synchronized (executablesMap) { - for (List exes : executablesMap.values()) { - for (Executable exe : exes) { - if (!executables.contains(exe)) { - executables.add(exe); - } - } - } - } - - trace("getExecutables returned at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ - - return executables; + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceExit(null); + return flattenExecutablesMap(); } /** @@ -300,7 +498,7 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang } if (handled) - scheduleRefresh(); + scheduleExecutableSearch(null); } /** @@ -329,10 +527,10 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang * @return an array of source files which may be empty */ public String[] getSourceFiles(final Executable executable, IProgressMonitor monitor) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, executable); + String[] result = new String[0]; - trace("getSourceFiles called at " + getStringFromTimestamp(System.currentTimeMillis()) + " for " + executable.getPath().toOSString()); //$NON-NLS-1$//$NON-NLS-2$ - synchronized (sourceFileProviders) { Collections.sort(sourceFileProviders, new Comparator() { @@ -351,17 +549,14 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang String[] sourceFiles = provider.getSourceFiles(executable, new SubProgressMonitor(monitor, 1000)); if (sourceFiles.length > 0) { result = sourceFiles; - - trace("getSourceFiles got " + sourceFiles.length + " files from " + provider.toString()); //$NON-NLS-1$ //$NON-NLS-2$ - + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Got " + sourceFiles.length + " files from " + provider.toString()); //$NON-NLS-1$ //$NON-NLS-2$ break; } } monitor.done(); } - trace("getSourceFiles returned at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ - + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceExit(null, result); return result; } @@ -382,221 +577,119 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang IProjectExecutablesProvider provider = getExecutablesProviderForProject(executable.getProject()); if (provider != null) { IStatus result = provider.removeExecutable(executable, new SubProgressMonitor(monitor, 1)); - if (result.isOK()) { - // remove the exe from the list - List exes = executablesMap.get(executable.getProject()); - if (exes != null) { - exes.remove(executable); - } - } else { + if (!result.isOK()) { status.add(result); } } } - - // notify listeners that the list has changed. only do this if at least one delete succeeded. - if (status.getChildren().length != executables.length) { - synchronized (changeListeners) { - for (IExecutablesChangeListener listener : changeListeners) { - listener.executablesListChanged(); - } - } - } + + // We don't need to directly call our listeners. The file removal will + // cause a C model change, which we will react to by then calling the + // listeners return status; } /** - * Refresh the list of executables for the given projects - * @param projects the list of projects, or null. if null or the list - * is empty, all projects will be refreshed. + * Initiates an asynchronous search of workspace CDT projects for + * executables. If a search is ongoing, it's cancelled and a new one is + * started. In all cases, this method returns quickly (does not wait/block). + * + *

+ * Listeners are notified when the search is complete and there is a change + * in the collection of found executables. The results of the search can be + * obtained by calling {@link #getExecutables(boolean)}. + * + * @param projectsToRefresh + * if null, we discard our entire Executables cache and search + * all CDT projects in the workspace. If not null, we purge our + * cache for only the given projects then search in all CDT + * projects for which we have no cache. Passing a project that we + * have no cache for is innocuous. In all cases, we search for + * executables in any newly available projects. This parameter is + * simply a way to get us to not skip one or more projects + * we already have the executables list for. + * * @since 7.0 */ - public void refresh(List projects) { - if (projects == null || projects.size() == 0) { - // clear the entire cache - executablesMap.clear(); - } else { - for (IProject project : projects) { - executablesMap.remove(project); - } - } - - scheduleRefresh(); + public void refresh(List projectsToRefresh) { + scheduleExecutableSearch(projectsToRefresh != null ? projectsToRefresh.toArray(new IProject[projectsToRefresh.size()]) : null); } /** * @since 7.0 + * @deprecated we no longer listen directly for platform resource changes + * but rather C model changes */ - public void resourceChanged(IResourceChangeEvent event) { - - synchronized (executablesMap) { - // project needs to be refreshed after a build/clean as the binary may - // be added/removed/renamed etc. - if (event.getType() == IResourceChangeEvent.POST_BUILD) { - Object obj = event.getSource(); - if (obj != null && obj instanceof IProject) { - try { - // make sure there's at least one builder for the project. this gets called even - // when there are no builder (e.g. the Executables project for imported executables). - IProject project = (IProject)obj; - if (project.getDescription().getBuildSpec().length > 0) { - if (executablesMap.containsKey(obj)) { - List executables = executablesMap.remove(obj); - - trace("Scheduling refresh because project " + ((IProject)obj).getName() + " built or cleaned"); //$NON-NLS-1$//$NON-NLS-2$ - - scheduleRefresh(); - - // notify the listeners that these executables have possibly changed - if (executables != null && executables.size() > 0) { - synchronized (changeListeners) { - for (IExecutablesChangeListener listener : changeListeners) { - listener.executablesChanged(executables); - } - } - } - } - } - } catch (CoreException e) { - e.printStackTrace(); - } - } - return; - } - - // refresh when projects are opened or closed. note that deleted - // projects are handled later in this method. new projects are handled - // in handleEvent. resource changed events always start at the workspace - // root, so projects are the next level down - boolean refreshNeeded = false; - IResourceDelta[] projects = event.getDelta().getAffectedChildren(); - for (IResourceDelta projectDelta : projects) { - if ((projectDelta.getFlags() & IResourceDelta.OPEN) != 0) { - if (projectDelta.getKind() == IResourceDelta.CHANGED) { - // project was opened or closed - if (executablesMap.containsKey(projectDelta.getResource())) { - executablesMap.remove(projectDelta.getResource()); - } - refreshNeeded = true; - } - } - } - - if (refreshNeeded) { - trace("Scheduling refresh because project(s) opened or closed"); //$NON-NLS-1$ - - scheduleRefresh(); - return; - } - - try { - event.getDelta().accept(new IResourceDeltaVisitor() { - - public boolean visit(IResourceDelta delta) throws CoreException { - if (delta.getKind() == IResourceDelta.ADDED || delta.getKind() == IResourceDelta.REMOVED) { - IResource deltaResource = delta.getResource(); - if (deltaResource != null) { - boolean refresh = false; - if (delta.getKind() == IResourceDelta.REMOVED && deltaResource instanceof IProject) { - // project deleted - if (executablesMap.containsKey(deltaResource)) { - executablesMap.remove(deltaResource); - refresh = true; - - trace("Scheduling refresh because project " + deltaResource.getName() + " deleted"); //$NON-NLS-1$//$NON-NLS-2$ - } - } else { - // see if a binary has been added/removed - IPath resourcePath = deltaResource.getLocation(); - if (resourcePath != null && Executable.isExecutableFile(resourcePath)) { - if (executablesMap.containsKey(deltaResource.getProject())) { - executablesMap.remove(deltaResource.getProject()); - refresh = true; - - trace("Scheduling refresh because a binary was added/removed"); //$NON-NLS-1$ - } - } - } - - if (refresh) { - scheduleRefresh(); - return false; - } - } - } - return true; - } - }); - } catch (CoreException e) { - } - } - } + @Deprecated + public void resourceChanged(IResourceChangeEvent event) {} /** * @since 7.0 */ public void handleEvent(CProjectDescriptionEvent event) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, event); + // this handles the cases where the active build configuration changes, // and when new projects are created or loaded at startup. - boolean refresh = false; - int eventType = event.getEventType(); if (eventType == CProjectDescriptionEvent.APPLIED) { - synchronized (executablesMap) { - // see if the active build config has changed - ICProjectDescription newDesc = event.getNewCProjectDescription(); - ICProjectDescription oldDesc = event.getOldCProjectDescription(); - if (oldDesc != null && newDesc != null) { - String newConfigName = newDesc.getActiveConfiguration().getName(); - String oldConfigName = oldDesc.getActiveConfiguration().getName(); - if (!newConfigName.equals(oldConfigName)) { - if (executablesMap.containsKey(newDesc.getProject())) { - executablesMap.remove(newDesc.getProject()); - refresh = true; + // see if the active build config has changed + ICProjectDescription newDesc = event.getNewCProjectDescription(); + ICProjectDescription oldDesc = event.getOldCProjectDescription(); + if (oldDesc != null && newDesc != null) { + String newConfigName = newDesc.getActiveConfiguration().getName(); + String oldConfigName = oldDesc.getActiveConfiguration().getName(); + if (!newConfigName.equals(oldConfigName)) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Scheduling refresh because active build configuration changed"); //$NON-NLS-1$ + scheduleExecutableSearch(new IProject[]{newDesc.getProject()}); + } + } else if (newDesc != null && oldDesc == null) { + // project just created + scheduleExecutableSearch(null); + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Scheduling refresh because project " + newDesc.getProject().getName() + " created"); //$NON-NLS-1$//$NON-NLS-2$ + } + } + } - trace("Scheduling refresh because active build configuration changed"); //$NON-NLS-1$ + /** + * Initiates an asynchronous search of workspace CDT projects for + * executables. For details, see {@link #refresh(List)}, which is a public + * wrapper for this internal method. This method is more aptly named and + * takes an array instead of a list + */ + private void scheduleExecutableSearch(final IProject[] projectsToRefresh) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, projectsToRefresh); + + // Don't schedule multiple search jobs simultaneously. If one is + // running, cancel it, wait for it to terminate, then schedule a new + // one. However we must not block our caller, so spawn an intermediary + // thread to do that leg work. This isn't an efficient design, but these + // searches aren't done in high volume. + Job job = new Job("Executable search scheduler") { //$NON-NLS-1$ + @Override + protected IStatus run(IProgressMonitor monitor) { + synchronized (searchSchedulingLock) { + searchJob.cancel(); + if (searchJob.getState() != Job.NONE) { + try { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Waiting for canceled job to terminate"); //$NON-NLS-1$ + searchJob.join(); + } catch (InterruptedException e) { } } - } else if (newDesc != null && oldDesc == null) { - // project just created - refresh = true; - - trace("Scheduling refresh because project " + newDesc.getProject().getName() + " created"); //$NON-NLS-1$//$NON-NLS-2$ + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Scheduling new search job"); //$NON-NLS-1$ + searchJob.schedule(projectsToRefresh); } + + return Status.OK_STATUS; } - } - - if (refresh) { - scheduleRefresh(); - } - } - - private List getProjectsToCheck() { - - List projects = new ArrayList(); - - synchronized (executablesMap) { - // look for any CDT projects not in our cache - for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { - if (!executablesMap.containsKey(project)) { - if (CoreModel.hasCNature(project)) { - projects.add(project); - } - } - } - } - - return projects; - } - - private void scheduleRefresh() { - trace("scheduleRefresh called at " + getStringFromTimestamp(System.currentTimeMillis())); //$NON-NLS-1$ - - refreshJob.cancel(); - refreshJob.schedule(); + + }; + job.setPriority(Job.SHORT); + job.schedule(); } private IProjectExecutablesProvider getExecutablesProviderForProject(IProject project) { @@ -762,14 +855,168 @@ public class ExecutablesManager extends PlatformObject implements IResourceChang } } - private void trace(String msg) { - if (DEBUG) { - // TODO use Logger? - System.out.println(msg); + /** + * We listen to C model changes and see if they affect what executables are + * in the workspace, and/or if the executables we already know of have + * changed. + * + * @since 7.1 + */ + public void elementChanged(ElementChangedEvent event) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null); + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "event = \n" + event); // must be done separately because of traceEntry() limitation //$NON-NLS-1$ + + // Examine the event and figure out what needs to be done + Set refreshProjects = new HashSet(5); + Set executablesChanged = new HashSet(5); + Set executablesRemoved = new HashSet(5); + processDeltas(event.getDelta().getAddedChildren(), null, refreshProjects, executablesRemoved, executablesChanged); + processDeltas(event.getDelta().getChangedChildren(), null, refreshProjects, executablesRemoved, executablesChanged); + processDeltas(event.getDelta().getRemovedChildren(), null, refreshProjects, executablesRemoved, executablesChanged); + + // Schedule executable searches in projects + if (refreshProjects.size() > 0) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "One or more projects need to be re-searched"); //$NON-NLS-1$ + scheduleExecutableSearch(refreshProjects.toArray(new IProject[refreshProjects.size()])); } + + // Invalidate the source file cache in changed Executables and inform + // listeners + if (executablesChanged.size() > 0) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "One or more executables changed"); //$NON-NLS-1$ + for (Executable exec : executablesChanged) { + exec.setRefreshSourceFiles(true); + } + List list = Arrays.asList(executablesChanged.toArray(new Executable[executablesChanged.size()])); + synchronized (changeListeners) { + for (IExecutablesChangeListener listener : changeListeners) { + listener.executablesChanged(list); + } + } + } + if (executablesRemoved.size() > 0) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "One or more executables were removed"); //$NON-NLS-1$ + List list = Arrays.asList(executablesRemoved.toArray(new Executable[executablesRemoved.size()])); + synchronized (changeListeners) { + for (IExecutablesChangeListener listener : changeListeners) { + if (listener instanceof IExecutablesChangeListener2) { + ((IExecutablesChangeListener2)listener).executablesRemoved(list); + } + } + } + } + + + return; } - private String getStringFromTimestamp(long timestamp) { - return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date(timestamp)); + /** + * Drills down a hierarchy of CDT model change events to determine the + * course of action. + * + * @param deltas + * CDT model events received by the viewer + * @param cproject + * the project the resources in [deltas] belong to + * @param projectsToRefresh + * implementation populates (appends) this list with the projects + * that need to be searched for executables. Note that Executable + * objects are created by an async job. The best we can do here + * is identify the projects that need to be searched. We can't + * provide a list of added Executables objects since they haven't + * been created yet. + * @param removedExecutables + * implementation populates (appends) this list with the + * Executable objects that have been removed, requiring listeners + * to be notified. + * @param changedExecutables + * implementation populates (appends) this list with the + * Executable objects that have changed, requiring listeners to + * be notified. + */ + private void processDeltas(ICElementDelta[] deltas, ICProject cproject, final Set projectsToRefresh, final Set removedExecutables, final Set changedExecutables) { + for (ICElementDelta delta : deltas) { + ICElement element = delta.getElement(); + if (element instanceof ICProject) { + // When a project is deleted, we get a REMOVED delta for the + // project only--none for the elements in the project. + IProject project = ((ICProject)element).getProject(); + if (delta.getKind() == ICElementDelta.REMOVED) { + projectsToRefresh.add(project); + List execs = null; + synchronized (executablesMap) { + execs = executablesMap.get(project); + } + if (execs != null) { + for (Executable exec : execs) { + if (exec.getResource().equals(delta.getElement().getResource())) { + removedExecutables.add(exec); + break; + } + } + + } + // Note that it's not our job to update 'executablesMap'. + // The async exec search job will do that. + } + } + else if (element instanceof IBinary) { + IProject project = cproject.getProject(); + switch (delta.getKind()) { + case ICElementDelta.ADDED: + projectsToRefresh.add(project); + break; + case ICElementDelta.REMOVED: { + projectsToRefresh.add(project); + List execs = null; + synchronized (executablesMap) { + execs = executablesMap.get(project); + } + if (execs != null) { + for (Executable exec : execs) { + if (exec.getResource().equals(delta.getElement().getResource())) { + removedExecutables.add(exec); + break; + } + } + } + // Note that it's not our job to update 'executablesMap'. + // The async exec search job will do that. + break; + } + + case ICElementDelta.CHANGED: { + List execs = null; + synchronized (executablesMap) { + execs = executablesMap.get(project); + } + if (execs == null) { + // Somehow, we missed the addition of the + // project. Request that the project be + // searched for executables + projectsToRefresh.add(project); + } + else { + // See if it's one of the executables we + // already know is in the project. If + // so, then we just need to tell + // listeners the executable changed + for (Executable exec : execs) { + if (exec.getResource().equals(delta.getElement().getResource())) { + changedExecutables.add(exec); + break; + } + } + } + break; + } + } + } + if (element instanceof ICProject) { + cproject = (ICProject)element; + } + // recursively call ourselves to handle this delta's children + processDeltas(delta.getAffectedChildren(), cproject, projectsToRefresh, removedExecutables, changedExecutables); + } } } diff --git a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener.java b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener.java index 25b18c59d52..0245dbcc62b 100644 --- a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener.java +++ b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener.java @@ -14,19 +14,65 @@ package org.eclipse.cdt.debug.core.executables; import java.util.EventListener; import java.util.List; +/** + * Listener interface for finding out when the list of Executable objects in the + * workspace changes or when the objects themselves change. + * + *

+ * Executable objects are ephemeral representations of Eclipse workspace model + * elements. A particular executable in the workspace is typically represented + * by many Executable objects. For example, an executable in the workspace that + * changes twice can cause the listener's {@link #executablesChanged(List)} to + * be called with a different Executable instance each of the two times it's invoked. + * + */ public interface IExecutablesChangeListener extends EventListener { /** - * Called whenever the list of executables in the workspace changes, e.g. a - * project was opened/closed/created/deleted + * Called whenever the list of executables in the workspace changes. Many + * types of operations cause the list to change, for example: + *

    + *
  • project is built for the first time + *
  • project with executables already in place is open, closed, removed or + * cleaned + *
  • user deletes one or more executables + *
+ * + * Clients can get the list by calling {@link ExecutablesManager#getExecutables()} + * * @since 7.0 */ public void executablesListChanged(); /** - * Called whenever some executables have changed, e.g. when a project is rebuilt or - * cleaned. The content may have changed for example, so the list of source files - * may be different. + * Called whenever one or more executables have changed, e.g. when a project + * is rebuilt. This is sometimes also called if the executable has not + * changed (i.e., the file on disk) but the information the Executable + * object provides has changed. One such case is when there's a change in + * the source locators, as such locators guide the Executable in finding the + * local path for the compile path. + * + *

+ * The Executable instances in the given list have had their caches flushed + * by ExecutableManager. Clients that keep references to Executable objects + * must keep in mind that those particular instances may no longer be + * managed by ExecutableManager and as such it is the client's + * responsibility to tell those instances to flush when this listener method + * is called. E.g., + * + *

+	 * public void executablesChanged(List executables) {
+	 *    for (Executable e : executables) {
+	 *       if (e.equals(fExecutable) {
+	 *          fExecutable.setRefreshSourceFiles(true);
+	 *       }
+	 *    }
+	 * }
+	 * 
+ * + *

+ * This is not called when an executable is added or removed + * * @param executables * @since 7.0 */ diff --git a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener2.java b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener2.java new file mode 100644 index 00000000000..87e3540bac6 --- /dev/null +++ b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/core/executables/IExecutablesChangeListener2.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2011 Freescale Semiconductor and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Freescale Semiconductor - Initial API and implementation + *******************************************************************************/ + +package org.eclipse.cdt.debug.core.executables; + +import java.util.List; + + +/** + * Extension of IExecutablesChangeListener which allows listeners to more + * precisely find out when an Executable is added or removed from the workspace + * + * @since 7.1 + */ +public interface IExecutablesChangeListener2 extends IExecutablesChangeListener { + + /** + * Called when one or more Executable objects have been added to the + * workspace + */ + public void executablesAdded(List executables); + + /** + * Called when one or more Executable objects have been removed from the + * workspace + */ + public void executablesRemoved(List executables); + +} \ No newline at end of file diff --git a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardExecutableImporter.java b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardExecutableImporter.java index 7325d0b0845..b1761fa209c 100644 --- a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardExecutableImporter.java +++ b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardExecutableImporter.java @@ -167,7 +167,7 @@ public class StandardExecutableImporter implements IExecutableImporter { } private void ensureBinaryType(IPath exectuableFilePath) { - if (Executable.isExecutableFile(exectuableFilePath)) + if (Executable.isBinaryFile(exectuableFilePath)) return; String ext = exectuableFilePath.getFileExtension(); if (ext != null) { diff --git a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardSourceFilesProvider.java b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardSourceFilesProvider.java index b231cdd0b06..4bf2b89165d 100644 --- a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardSourceFilesProvider.java +++ b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/executables/StandardSourceFilesProvider.java @@ -45,7 +45,7 @@ public class StandardSourceFilesProvider extends PlatformObject implements ISour return null; } - if (!Executable.isExecutableFile(executable.getPath())) + if (!Executable.isBinaryFile(executable.getPath())) return null; File f = new File(path.toOSString()); diff --git a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/srcfinder/CSourceFinder.java b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/srcfinder/CSourceFinder.java index 30ce03e2e61..6ffb0fae505 100644 --- a/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/srcfinder/CSourceFinder.java +++ b/debug/org.eclipse.cdt.debug.core/src/org/eclipse/cdt/debug/internal/core/srcfinder/CSourceFinder.java @@ -69,10 +69,11 @@ public class CSourceFinder implements ISourceFinder, ILaunchConfigurationListene * launch config. This is a heavy operation. As an optimization, we cache * the locators we create and discard when the launch config changes or is * disposed. Collection is subject to be changed by listener invocations. + * Map key is the launch configuration name. * * @see CSourceFinder#getLocator(ILaunchConfiguration) */ - private Map fConfigLocators = Collections.synchronizedMap(new HashMap()); + private Map fConfigLocators = Collections.synchronizedMap(new HashMap()); /** * We use this when we don't have an ILaunch or ILaunchConfiguration @@ -134,13 +135,15 @@ public class CSourceFinder implements ISourceFinder, ILaunchConfigurationListene if (fLaunchLocator == null) { for (ILaunchConfiguration config : lmgr.getLaunchConfigurations()) { if (isMatch(config)) { + String configName = config.getName(); + // Search our cache of locators that we // instantiate for configurations. Create one if // not found - ISourceLocator configLocator = fConfigLocators.get(config); + ISourceLocator configLocator = fConfigLocators.get(configName); if (configLocator == null) { configLocator = getLocator(config); // heavy operation - fConfigLocators.put(config, configLocator); // cache to avoid next time + fConfigLocators.put(configName, configLocator); // cache to avoid next time } // In practice, a config's locator is always an ISourceLookupDirector if (configLocator instanceof ISourceLookupDirector) { @@ -330,14 +333,15 @@ public class CSourceFinder implements ISourceFinder, ILaunchConfigurationListene /* (non-Javadoc) * @see org.eclipse.debug.core.ILaunchConfigurationListener#launchConfigurationChanged(org.eclipse.debug.core.ILaunchConfiguration) */ - public void launchConfigurationChanged(ILaunchConfiguration config) { + synchronized public void launchConfigurationChanged(ILaunchConfiguration config) { // We don't care if it's a working copy. if (config.isWorkingCopy()) { return; } + // the source locator attribute may have changed - fConfigLocators.remove(config); - if ((fLaunchLocator != null) && (fLaunchLocator.getLaunchConfiguration() == config)) { + fConfigLocators.remove(config.getName()); + if ((fLaunchLocator != null) && (fLaunchLocator.getLaunchConfiguration().getName() == config.getName())) { fLaunchLocator = null; } } @@ -345,9 +349,14 @@ public class CSourceFinder implements ISourceFinder, ILaunchConfigurationListene /* (non-Javadoc) * @see org.eclipse.debug.core.ILaunchConfigurationListener#launchConfigurationRemoved(org.eclipse.debug.core.ILaunchConfiguration) */ - public void launchConfigurationRemoved(ILaunchConfiguration config) { - fConfigLocators.remove(config); - if ((fLaunchLocator != null) && (fLaunchLocator.getLaunchConfiguration() == config)) { + synchronized public void launchConfigurationRemoved(ILaunchConfiguration config) { + // We don't care if it's a working copy. + if (config.isWorkingCopy()) { + return; + } + + fConfigLocators.remove(config.getName()); + if ((fLaunchLocator != null) && (fLaunchLocator.getLaunchConfiguration().getName() == config.getName())) { fLaunchLocator = null; } } @@ -367,7 +376,7 @@ public class CSourceFinder implements ISourceFinder, ILaunchConfigurationListene /* (non-Javadoc) * @see org.eclipse.debug.core.ILaunchesListener#launchesAdded(org.eclipse.debug.core.ILaunch[]) */ - synchronized public void launchesAdded(ILaunch[] launches) { + public void launchesAdded(ILaunch[] launches) { // If there's a new launch in town, we need to take it into // consideration. E.g., if it targets our binary, and we're currently // searching using an inactive launch configuration's locator, then the @@ -375,7 +384,9 @@ public class CSourceFinder implements ISourceFinder, ILaunchConfigurationListene for (ILaunch launch : launches) { ILaunchConfiguration config = launch.getLaunchConfiguration(); if (config != null && isMatch(config)) { - fLaunchLocator = null; + synchronized(this) { + fLaunchLocator = null; + } } } } diff --git a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesContentProvider.java b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesContentProvider.java index 18e7ced7a4c..132c4a45fba 100644 --- a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesContentProvider.java +++ b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesContentProvider.java @@ -11,28 +11,44 @@ package org.eclipse.cdt.debug.internal.ui.views.executables; -import com.ibm.icu.text.DateFormat; import java.util.Date; +import java.util.List; import org.eclipse.cdt.debug.core.executables.Executable; import org.eclipse.cdt.debug.core.executables.ExecutablesManager; +import org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.ui.progress.WorkbenchJob; -class ExecutablesContentProvider extends ColumnLabelProvider implements IStructuredContentProvider, ITreeContentProvider { +import com.ibm.icu.text.DateFormat; - public ExecutablesContentProvider(TreeViewer viewer) { +class ExecutablesContentProvider extends ColumnLabelProvider implements IStructuredContentProvider, ITreeContentProvider, IExecutablesChangeListener { + + final private TreeViewer viewer; + + public ExecutablesContentProvider(final TreeViewer viewer) { + this.viewer = viewer; + ExecutablesManager.getExecutablesManager().addExecutablesChangeListener(this); } public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.BaseLabelProvider#dispose() + */ + @Override public void dispose() { + ExecutablesManager.getExecutablesManager().removeExecutablesChangeListener(this); } public Object[] getElements(final Object inputElement) { @@ -100,4 +116,28 @@ class ExecutablesContentProvider extends ColumnLabelProvider implements IStructu return new Object[] {}; } + /* (non-Javadoc) + * @see org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener#executablesListChanged() + */ + public void executablesListChanged() { + new WorkbenchJob("execs list changed") { //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + viewer.refresh(null); + if (viewer instanceof BaseViewer) { + ((BaseViewer)viewer).packColumns(); + } + return Status.OK_STATUS; + } + }.schedule(); + } + + /* (non-Javadoc) + * @see org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener#executablesChanged(java.util.List) + */ + public void executablesChanged(List executables) { + // Our concern is only if the list of executables changed. The + // content provider for the source files viewer will care about + // whether the Executables themselves change + } } \ No newline at end of file diff --git a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesView.java b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesView.java index 4a35f5c5b0b..83b37d6be72 100644 --- a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesView.java +++ b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesView.java @@ -58,7 +58,7 @@ import org.eclipse.ui.XMLMemento; import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.dialogs.ListSelectionDialog; import org.eclipse.ui.part.ViewPart; -import org.eclipse.ui.progress.UIJob; +import org.eclipse.ui.progress.WorkbenchJob; /** * ExecutablesView displays a list of executable files either in the workspace @@ -176,6 +176,7 @@ public class ExecutablesView extends ViewPart { class ColumnLabelProvider extends LabelProvider { + @Override public String getText(Object element) { return (String) element; } @@ -193,6 +194,7 @@ public class ExecutablesView extends ViewPart { * * @see org.eclipse.jface.action.Action#run() */ + @Override public void run() { ListSelectionDialog dialog = new ListSelectionDialog(ExecutablesView.this.getExecutablesViewer().getTree().getShell(), this, new ColumnContentProvider(), new ColumnLabelProvider(), Messages.ExecutablesView_SelectColumns); @@ -250,7 +252,6 @@ public class ExecutablesView extends ViewPart { // Create the two sub viewers. executablesViewer = new ExecutablesViewer(this, sashForm, SWT.FULL_SELECTION | SWT.BORDER | SWT.MULTI); focusedViewer = executablesViewer; - ExecutablesManager.getExecutablesManager().addExecutablesChangeListener(executablesViewer); sourceFilesViewer = new SourceFilesViewer(this, sashForm, SWT.BORDER | SWT.MULTI); executablesViewer.getTree().addFocusListener(new FocusListener() { @@ -416,6 +417,7 @@ public class ExecutablesView extends ViewPart { private Action createRemoveAction() { Action action = new Action(Messages.ExecutablesView_Remove) { + @Override public void run() { ISelection selection = getExecutablesViewer().getSelection(); if (selection instanceof IStructuredSelection) @@ -432,8 +434,9 @@ public class ExecutablesView extends ViewPart { if (confirm) { - Job removeJob = new UIJob(Messages.ExecutablesView_RemoveExes) { + Job removeJob = new WorkbenchJob(Messages.ExecutablesView_RemoveExes) { + @Override public IStatus runInUIThread(IProgressMonitor monitor) { IStatus result = ExecutablesManager.getExecutablesManager().removeExecutables(selectedExesArray, monitor); if (result.getSeverity() != IStatus.OK) @@ -490,6 +493,7 @@ public class ExecutablesView extends ViewPart { private Action createImportAction() { Action action = new Action(Messages.ExecutablesView_Import) { + @Override public void run() { FileDialog dialog = new FileDialog(getViewSite().getShell(), SWT.NONE); dialog.setText(Messages.ExecutablesView_SelectExeFile); @@ -515,8 +519,10 @@ public class ExecutablesView extends ViewPart { private Action createRefreshAction() { Action action = new Action(Messages.ExecutablesView_Refresh) { + @Override public void run() { ExecutablesManager.getExecutablesManager().refresh(null); + sourceFilesViewer.restartCanceledExecutableParse(); } }; action.setToolTipText(Messages.ExecutablesView_RefreshList); @@ -574,7 +580,6 @@ public class ExecutablesView extends ViewPart { @Override public void dispose() { - ExecutablesManager.getExecutablesManager().removeExecutablesChangeListener(executablesViewer); super.dispose(); } diff --git a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesViewer.java b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesViewer.java index bc5f2135c42..cd225886b70 100644 --- a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesViewer.java +++ b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/ExecutablesViewer.java @@ -10,19 +10,12 @@ *******************************************************************************/ package org.eclipse.cdt.debug.internal.ui.views.executables; -import java.util.List; - import org.eclipse.cdt.debug.core.executables.Executable; import org.eclipse.cdt.debug.core.executables.ExecutablesManager; -import org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; -import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.jface.viewers.ViewerDropAdapter; @@ -35,12 +28,11 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.ui.IWorkbenchActionConstants; -import org.eclipse.ui.progress.UIJob; /** * Displays the list of executables gathered by the ExecutablesManager */ -public class ExecutablesViewer extends BaseViewer implements IExecutablesChangeListener { +public class ExecutablesViewer extends BaseViewer { private static final String P_COLUMN_ORDER_KEY_EXE = "columnOrderKeyEXE"; //$NON-NLS-1$ private static final String P_SORTED_COLUMN_INDEX_KEY_EXE = "sortedColumnIndexKeyEXE"; //$NON-NLS-1$ @@ -155,6 +147,7 @@ public class ExecutablesViewer extends BaseViewer implements IExecutablesChangeL protected ViewerComparator getViewerComparator(int sortType) { if (sortType == ExecutablesView.PROJECT) { return new ExecutablesViewerComparator(sortType, column_sort_order[ExecutablesView.PROJECT]) { + @Override @SuppressWarnings("unchecked") public int compare(Viewer viewer, Object e1, Object e2) { Executable entry1 = (Executable) e1; @@ -192,46 +185,4 @@ public class ExecutablesViewer extends BaseViewer implements IExecutablesChangeL // default visible columns return "1,1,1,0,0,0"; //$NON-NLS-1$ } - - public void executablesChanged(final List executables) { - // some executables have been updated. if one of them is currently - // selected, we need to update the source file list - UIJob refreshJob = new UIJob(Messages.ExecutablesViewer_RefreshExecutablesView) { - - @Override - public IStatus runInUIThread(IProgressMonitor monitor) { - // if the user has selected an executable, they expect its - // list of source files to be refreshed automatically - if (getSelection() != null && - getSelection() instanceof IStructuredSelection) { - IStructuredSelection selection = (IStructuredSelection)getSelection(); - - Object firstElement = selection.getFirstElement(); - if (firstElement instanceof Executable) { - Executable executable = (Executable) firstElement; - if (executables.contains(executable)) { - executable.setRefreshSourceFiles(true); - setSelection(selection); - } - } - } - return Status.OK_STATUS; - } - }; - refreshJob.schedule(); - } - - public void executablesListChanged() { - // Executables list has changed so refresh the view. - UIJob refreshJob = new UIJob(Messages.ExecutablesViewer_RefreshExecutablesView) { - - @Override - public IStatus runInUIThread(IProgressMonitor monitor) { - refresh(null); - packColumns(); - return Status.OK_STATUS; - } - }; - refreshJob.schedule(); - } } \ No newline at end of file diff --git a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.java b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.java index 1507dfb3d19..be16c63a60c 100644 --- a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.java +++ b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.java @@ -53,6 +53,7 @@ public class Messages extends NLS { public static String SourceFilesContentProvider_NoFilesFound; public static String SourceFilesContentProvider_ReadingDebugSymbolInformationLabel; public static String SourceFilesContentProvider_Refreshing; + public static String SourceFilesContentProvider_Canceled; public static String SourceFilesViewer_RefreshSourceFiles; public static String SourceFilesViewer_Location; public static String SourceFilesViewer_Modified; diff --git a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.properties b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.properties index 4262cb72bcd..1cf86d51dd3 100644 --- a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.properties +++ b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/Messages.properties @@ -48,6 +48,7 @@ ExecutablesViewer_Type=Type SourceFilesContentProvider_NoFilesFound=No source files found in SourceFilesContentProvider_ReadingDebugSymbolInformationLabel=Reading Debug Symbol Information: SourceFilesContentProvider_Refreshing=Refreshing... +SourceFilesContentProvider_Canceled=Parse canceled. Hit refresh to restart. SourceFilesViewer_RefreshSourceFiles=Refresh Source Files SourceFilesViewer_Location=Location SourceFilesViewer_Modified=Modified diff --git a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesContentProvider.java b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesContentProvider.java index 80a2ffe9998..b515756542a 100644 --- a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesContentProvider.java +++ b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesContentProvider.java @@ -18,7 +18,8 @@ import java.util.Map; import org.eclipse.cdt.core.model.ITranslationUnit; import org.eclipse.cdt.debug.core.executables.Executable; import org.eclipse.cdt.debug.core.executables.ExecutablesManager; -import org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener; +import org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener2; +import org.eclipse.cdt.debug.internal.core.Trace; import org.eclipse.cdt.debug.internal.ui.views.executables.SourceFilesViewer.TranslationUnitInfo; import org.eclipse.cdt.ui.CElementContentProvider; import org.eclipse.core.runtime.IPath; @@ -30,8 +31,9 @@ import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.progress.WorkbenchJob; -public class SourceFilesContentProvider extends CElementContentProvider implements IExecutablesChangeListener { +public class SourceFilesContentProvider extends CElementContentProvider implements IExecutablesChangeListener2 { static class QuickParseJob extends Job { final Executable executable; @@ -45,18 +47,65 @@ public class SourceFilesContentProvider extends CElementContentProvider implemen @Override protected IStatus run(IProgressMonitor monitor) { - tus = executable.getSourceFiles(monitor); - return Status.OK_STATUS; + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Quick parsing of executable for source files has begun (" + this + ')'); //$NON-NLS-1$ + + // Ask the Executable for its source files. This could take a while... + ITranslationUnit[] mytus = executable.getSourceFiles(monitor); + + IStatus status; + if (!monitor.isCanceled()) { + tus = mytus; + status = Status.OK_STATUS; + } + else { + status = Status.CANCEL_STATUS; + } + + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Quick parsing of executable has finished, status is " + status); //$NON-NLS-1$ + return status; } } - /** contains running jobs */ + /** + * The collection of running file parsing jobs. Each executable file (not + * object) can independently be parsed, and these parses can happen + * simultaneously. Normally, each executable file has at most one ongoing + * parse. An exception is when a search is canceled. We don't wait for the + * search to actually end if a subsequent search comes in shortly after the + * first one is canceled. We cancel the first one, remove it from this list, + * schedule a new one, then add that to the list. It's safe to assume the + * canceled one will complete before the new one. + * + *

This collection must be accessed only from the UI thread + */ private Map pathToJobMap = new HashMap(); /** those executables for which we asked the question and got a result. * NOTE: this contains a duplicate of into in Executable, because we can't * guarantee or check whether Executable still has the info itself. */ - private Map fetchedExecutables = new HashMap(); + private static class TUData{ + /** Constructor used when search completes successfully */ + public TUData(ITranslationUnit[] tus, long timestamp) { + this.tus = tus; + this.timestamp = timestamp; + } + + /** Constructor used when search is canceled */ + public TUData() { + this.canceled = true; + } + + ITranslationUnit[] tus; + /** IResource.getModificationStamp value of when this data was last updated */ + long timestamp; + + boolean canceled; + } + + /** + * The cached file info. Key is the path of the executable. This collection must be accessed only on the UI thread. + */ + private Map fetchedExecutables = new HashMap(); private final SourceFilesViewer viewer; @@ -72,12 +121,14 @@ public class SourceFilesContentProvider extends CElementContentProvider implemen @Override public void dispose() { ExecutablesManager.getExecutablesManager().removeExecutablesChangeListener(this); - synchronized (fetchedExecutables) { - fetchedExecutables.clear(); - } - synchronized (pathToJobMap) { - pathToJobMap.clear(); - } + new WorkbenchJob("") { //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + fetchedExecutables.clear(); + pathToJobMap.clear(); + return Status.OK_STATUS; + } + }.schedule(); super.dispose(); } @@ -92,63 +143,71 @@ public class SourceFilesContentProvider extends CElementContentProvider implemen return super.hasChildren(element); } + /* (non-Javadoc) + * @see org.eclipse.cdt.internal.ui.BaseCElementContentProvider#getElements(java.lang.Object) + */ + @Override public Object[] getElements(Object inputElement) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, inputElement); + if (inputElement instanceof Executable) { final Executable executable = (Executable) inputElement; final IPath exePath = executable.getPath(); // look for a job that is currently fetching this info QuickParseJob job; - synchronized (pathToJobMap) { - job = pathToJobMap.get(exePath); - } + job = pathToJobMap.get(exePath); if (job != null) { // job is still running return new String[] { Messages.SourceFilesContentProvider_Refreshing }; } - - // see if we already checked - synchronized (fetchedExecutables) { - if (fetchedExecutables.containsKey(exePath)) { - return fetchedExecutables.get(exePath); - } - } - - // start a background job to look for the sources + // create a background job to look for the sources but don't start it yet job = new QuickParseJob(executable); - synchronized (pathToJobMap) { - pathToJobMap.put(exePath, job); + pathToJobMap.put(exePath, job); + + // See if we have the result cached for this executable. If so + // return that. It's also possible that the most resent search was + // canceled + Object[] cachedResult = null; + TUData tud = fetchedExecutables.get(exePath); + if (tud != null) { + if (tud.canceled) + cachedResult = new String[]{Messages.SourceFilesContentProvider_Canceled}; + else + cachedResult = tud.tus; + } + if (cachedResult != null) { + pathToJobMap.remove(exePath); // removed the unused search job + return cachedResult; } - // once the job finishes, update the viewer + // Schedule the job; once it finishes, update the viewer final QuickParseJob theJob = job; job.addJobChangeListener(new JobChangeAdapter() { - public void done(IJobChangeEvent event) { - synchronized (pathToJobMap) { - pathToJobMap.values().remove(theJob); - } - if (event.getResult().isOK()) { - synchronized (fetchedExecutables) { - fetchedExecutables.put(exePath, theJob.tus); - } - Display.getDefault().asyncExec(new Runnable() { - public void run() { - // update the viewer - if (!viewer.getControl().isDisposed()) { - viewer.getTree().setLayoutDeferred(true); - viewer.refresh(executable); - viewer.packColumns(); - viewer.getTree().setLayoutDeferred(false); - } + @Override + public void done(final IJobChangeEvent event) { + new WorkbenchJob("refreshing source files viewer"){ //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + if (event.getResult().isOK()) { + fetchedExecutables.put(exePath, new TUData(theJob.tus, theJob.executable.getResource().getModificationStamp())); } - }); - } + else { + fetchedExecutables.put(exePath, new TUData()); + } + + pathToJobMap.values().remove(theJob); + + refreshViewer(executable); + return Status.OK_STATUS; + } + }.schedule(); } }); - + job.schedule(); - // while it's running... + // show the user a string that lets him know we're searching return new String[] { Messages.SourceFilesContentProvider_Refreshing }; } return new Object[] {}; @@ -159,46 +218,62 @@ public class SourceFilesContentProvider extends CElementContentProvider implemen * @see org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener#executablesListChanged() */ public void executablesListChanged() { - // Don't clear executables -- closing/opening project doesn't imply - // the info is different. But cancel all the jobs in case projects - // were closed. It's non-obvious how to map executables to projects, - // so just bail and cancel all the current parsing. The viewer - // will be refreshed and re-request source lists for any executables - // that are still applicable. - cancelQuickParseJobs(); + // we react via IExecutablesChangeListener2 methods } - /** - * - */ - private void cancelQuickParseJobs() { - synchronized (pathToJobMap) { - for (QuickParseJob job : pathToJobMap.values()) { - job.cancel(); - } - pathToJobMap.clear(); - } - - } - /* (non-Javadoc) * @see org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener#executablesChanged(java.util.List) */ - public void executablesChanged(List executables) { - for (Executable executable : executables) { - IPath exePath = executable.getPath(); - synchronized (fetchedExecutables) { - fetchedExecutables.remove(exePath); - } - synchronized (pathToJobMap) { - QuickParseJob job = pathToJobMap.get(exePath); - if (job != null) { - job.cancel(); - pathToJobMap.remove(exePath); + public void executablesChanged(final List executables) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, executables); + + new WorkbenchJob("Refreshing viewer") { //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + for (Executable executable : executables) { + IPath exePath = executable.getPath(); + fetchedExecutables.remove(exePath); + QuickParseJob job = pathToJobMap.get(exePath); + if (job != null) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Cancelling QuickParseJob: " + job); //$NON-NLS-1$ + job.cancel(); + pathToJobMap.remove(exePath); + } } + + if (!viewer.getControl().isDisposed()) { + // See if our current input is one of the executables that has changed. + for (Executable executable : executables) { + if (executable.equals(fInput)) { + // Executable.equals() is not a simple reference + // check. Two Executable objects are equal if they + // represent the same file on disk. I.e., our input + // object might not be one of the instances on the + // changed-list, but for sure the file on disk has + // changed. Now, the manager that called this + // listener has already told the Executable + // instances on the changed list to flush their + // source files list. However, if our input is not + // exactly one of those references, it means the + // manager is no longer managing the Executable + // that's our input. In that case, it's up to us to + // tell that Executable to flush its source file + // cache so that refreshing the viewer will cause a + // fresh fetch of the source file information. + Executable execInput = (Executable)fInput; + if (executable != execInput) { + execInput.setRefreshSourceFiles(true); + } + refreshViewer(execInput); + break; + } + } + } + return Status.OK_STATUS; } - } + + }.schedule(); } /* (non-Javadoc) @@ -208,14 +283,178 @@ public class SourceFilesContentProvider extends CElementContentProvider implemen public void inputChanged(Viewer viewer, Object oldInput, final Object newInput) { super.inputChanged(viewer, oldInput, newInput); - Display.getDefault().asyncExec(new Runnable() { - public void run() { + new WorkbenchJob("Refreshing viewer") { //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { // pack because the quick parse job won't run if (newInput instanceof Executable && fetchedExecutables.containsKey(((Executable) newInput).getPath())) SourceFilesContentProvider.this.viewer.packColumns(); + return Status.OK_STATUS; } - }); + }.schedule(); + } + + /* (non-Javadoc) + * @see org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener2#executablesAdded(java.util.List) + */ + public void executablesAdded(final List executables) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, executables); + + // Throw out our cached translation units for the executable *file* but + // only if the file hasn't changed. Executable objects come and go + // independently of the file on disk. + new WorkbenchJob("executables removed") { //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + for (Executable exec : executables) { + final IPath exePath = exec.getPath(); + final long timestamp = exec.getResource().getModificationStamp(); + TUData tud = fetchedExecutables.get(exePath); + if (tud != null && tud.timestamp != timestamp) { + fetchedExecutables.remove(exePath); + } + } + + if (!viewer.getControl().isDisposed()) { + // See if current viewer input is one of the executables that + // was added. If so, this is likely an exec that was rebuilt + // and CDT missed sending a REMOVED model event. There's + // some crazy race condition going on, but basically CDT + // sends out an event that the binary has changed, then + // sends one that says it was added. Anyway, the best thing + // for us to do is to cause a refresh of the viewer since + // the addition notification probably caused us to cancel + // the parse of the exec that was initiated by the change + // event and the viewer will be stuck with a "canceled" + // message in the viewer table. + for (Executable executable : executables) { + if (executable.equals(fInput)) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "refreshing viewer; added executable is our current input"); //$NON-NLS-1$ + refreshViewer((Executable)fInput); + break; + } + } + } + + return Status.OK_STATUS; + } + }.schedule(); + } + + /* (non-Javadoc) + * @see org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener2#executablesRemoved(java.util.List) + */ + public void executablesRemoved(final List executables) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().traceEntry(null, executables); + + // The fact that the Executable was removed from the workspace doesn't + // mean we need to throw out the source file info we've cached. If a + // project is closed then reopened, we are able to reuse the info as + // long as the timestamp of the resource hasn't changed. But, there's no + // point in continuing any ongoing searches in the executables. + new WorkbenchJob("executables removed") { //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + for (Executable exec : executables) { + final IPath exePath = exec.getPath(); + QuickParseJob job = pathToJobMap.get(exePath); + if (job != null) { + if (Trace.DEBUG_EXECUTABLES) Trace.getTrace().trace(null, "Cancelling QuickParseJob: " + job); //$NON-NLS-1$ + job.cancel(); + pathToJobMap.remove(exePath); + } + } + return Status.OK_STATUS; + } + }.schedule(); + } + + /** + * Restarts a parse of the current input (Executable) if and only if its + * last search was canceled. The viewer is refresh accordingly. + * + *

+ * Must be called on the UI thread + * + */ + public void restartCanceledExecutableParse() { + assert Display.getCurrent() != null; + + Object input = viewer.getInput(); + if (input instanceof Executable) { + final Executable executable = (Executable)input; + final IPath exePath = executable.getPath(); + + // Ignore restart if there's an ongoing search. + QuickParseJob job; + job = pathToJobMap.get(exePath); + if (job != null) { + return; + } + + TUData tud = fetchedExecutables.get(exePath); + + // Ignore request if the most recent search wasn't canceled + if (tud != null && !tud.canceled) { + pathToJobMap.remove(exePath); + return; + } + + // Create and schedule a parse job. Once the job finishes, update + // the viewer + job = new QuickParseJob(executable); + pathToJobMap.put(exePath, job); + final QuickParseJob theJob = job; + job.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(final IJobChangeEvent event) { + + new WorkbenchJob("refreshing source files viewer"){ //$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + // Update the model with the search results + if (event.getResult().isOK()) { + fetchedExecutables.put(exePath, new TUData(theJob.tus, theJob.executable.getResource().getModificationStamp())); + } + else { + // The search job apparently always completes + // successfully or it was canceled (failure was + // not a considered outcome). If it was canceled, + // well then we're back to where we started + fetchedExecutables.put(exePath, new TUData()); + } + pathToJobMap.values().remove(theJob); + + refreshViewer(executable); + return Status.OK_STATUS; + } + }.schedule(); + } + }); + + job.schedule(); + + // The viewer is currently showing "search canceled". Cause an + // immediate refresh so that it shows "refreshing" while the new + // search is ongoing + refreshViewer(executable); + } + } + + /** + * Utility method to invoke a viewer refresh for the given element + * @param input the Executable to show content for + * + *

Must be called on the UI thread + */ + private void refreshViewer(Executable input) { + if (!viewer.getControl().isDisposed()) { + viewer.getTree().setLayoutDeferred(true); + viewer.refresh(input); + viewer.packColumns(); + viewer.getTree().setLayoutDeferred(false); + } } } diff --git a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesViewer.java b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesViewer.java index f69c3f7c840..640ba1291f2 100644 --- a/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesViewer.java +++ b/debug/org.eclipse.cdt.debug.ui/src/org/eclipse/cdt/debug/internal/ui/views/executables/SourceFilesViewer.java @@ -11,35 +11,28 @@ package org.eclipse.cdt.debug.internal.ui.views.executables; import java.io.File; +import java.util.List; import org.eclipse.cdt.core.model.ICElement; import org.eclipse.cdt.core.model.ISourceReference; import org.eclipse.cdt.core.model.ITranslationUnit; -import org.eclipse.cdt.debug.core.CDebugCorePlugin; import org.eclipse.cdt.debug.core.executables.Executable; +import org.eclipse.cdt.debug.core.executables.ExecutablesManager; +import org.eclipse.cdt.debug.core.executables.IExecutablesChangeListener; import org.eclipse.cdt.debug.internal.ui.sourcelookup.CSourceNotFoundEditorInput; import org.eclipse.cdt.debug.ui.ICDebugUIConstants; import org.eclipse.cdt.internal.core.util.LRUCache; import org.eclipse.cdt.internal.ui.util.EditorUtility; import org.eclipse.cdt.ui.CUIPlugin; -import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; -import org.eclipse.debug.core.DebugPlugin; -import org.eclipse.debug.core.ILaunchConfiguration; -import org.eclipse.debug.core.ILaunchConfigurationListener; -import org.eclipse.debug.core.sourcelookup.ISourceLookupDirector; -import org.eclipse.debug.core.sourcelookup.ISourceLookupParticipant; import org.eclipse.jface.viewers.IOpenListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.OpenEvent; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.swt.SWT; -import org.eclipse.swt.events.DisposeEvent; -import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.ui.IEditorPart; @@ -50,7 +43,7 @@ import org.eclipse.ui.PartInitException; * Displays the list of source files for the executable selected in the * ExecutablesViewer. */ -public class SourceFilesViewer extends BaseViewer implements ISourceLookupParticipant, ILaunchConfigurationListener { +public class SourceFilesViewer extends BaseViewer { /** Information from an ITranslationUnit for the various displayed columns */ static class TranslationUnitInfo { @@ -100,25 +93,21 @@ public class SourceFilesViewer extends BaseViewer implements ISourceLookupPartic openSourceFile(event); } }); - - // We implement ISourceLookupParticipant so we can listen for changes to - // source lookup as this viewer shows both original and remapped - // locations - CDebugCorePlugin.getDefault().getCommonSourceLookupDirector().addParticipants(new ISourceLookupParticipant[] { this }); - // We also look for launch configuration changes, since their source - // locators are involved in source path remapping, too - DebugPlugin.getDefault().getLaunchManager().addLaunchConfigurationListener(this); - - sourceFilesTree.addDisposeListener(new DisposeListener() { - - public void widgetDisposed(DisposeEvent e) { - DebugPlugin.getDefault().getLaunchManager().removeLaunchConfigurationListener(SourceFilesViewer.this); - - CDebugCorePlugin.getDefault().getCommonSourceLookupDirector().removeParticipants( - new ISourceLookupParticipant[] { SourceFilesViewer.this }); + ExecutablesManager.getExecutablesManager().addExecutablesChangeListener(new IExecutablesChangeListener(){ + public void executablesListChanged() { + // this doesn't directly affect us } - }); + + public void executablesChanged(List executables) { + // TODO: be more selective; we don't know what TUs go with which executables yet + flushTranslationUnitCache(); + + // Note that we don't invoke a viewer refresh. Our content + // provider needs to also be listening for this notification. + // It's up to him to invoke a refresh on us if the model has + // been affected by the Executable change + }}); } private void openSourceFile(OpenEvent event) { @@ -198,10 +187,12 @@ public class SourceFilesViewer extends BaseViewer implements ISourceLookupPartic typeColumn.addSelectionListener(new ColumnSelectionAdapter(ExecutablesView.TYPE)); } + @Override protected ViewerComparator getViewerComparator(int sortType) { if (sortType == ExecutablesView.ORG_LOCATION) { return new ExecutablesViewerComparator(sortType, column_sort_order[ExecutablesView.ORG_LOCATION]) { + @Override @SuppressWarnings("unchecked") public int compare(Viewer viewer, Object e1, Object e2) { if (e1 instanceof ITranslationUnit && e2 instanceof ITranslationUnit) { @@ -219,40 +210,6 @@ public class SourceFilesViewer extends BaseViewer implements ISourceLookupPartic return new ExecutablesViewerComparator(sortType, column_sort_order[sortType]); } - public void dispose() { - } - - public Object[] findSourceElements(Object object) throws CoreException { - return new Object[0]; - } - - public String getSourceName(Object object) throws CoreException { - return ""; //$NON-NLS-1$ - } - - public void init(ISourceLookupDirector director) { - } - - public void sourceContainersChanged(ISourceLookupDirector director) { - refreshContent(); - } - - private void refreshContent() { - Display.getDefault().asyncExec(new Runnable() { - public void run() { - Object input = getInput(); - if (input != null && input instanceof Executable) { - ((Executable)input).setRemapSourceFiles(true); - - // TODO: be more selective; we don't know what TUs go with which executables yet - flushTranslationUnitCache(); - - refresh(true); - } - } - }); - } - @Override protected String getColumnOrderKey() { return P_COLUMN_ORDER_KEY_SF; @@ -279,34 +236,6 @@ public class SourceFilesViewer extends BaseViewer implements ISourceLookupPartic return "1,1,0,0,0,0"; //$NON-NLS-1$ } - /* (non-Javadoc) - * @see org.eclipse.debug.core.ILaunchConfigurationListener#launchConfigurationAdded(org.eclipse.debug.core.ILaunchConfiguration) - */ - public void launchConfigurationAdded(ILaunchConfiguration configuration) { - if (!configuration.isWorkingCopy()) { - refreshContent(); - } - } - - /* (non-Javadoc) - * @see org.eclipse.debug.core.ILaunchConfigurationListener#launchConfigurationChanged(org.eclipse.debug.core.ILaunchConfiguration) - */ - public void launchConfigurationChanged(ILaunchConfiguration configuration) { - if (!configuration.isWorkingCopy()) { - refreshContent(); - } - } - - /* (non-Javadoc) - * @see org.eclipse.debug.core.ILaunchConfigurationListener#launchConfigurationRemoved(org.eclipse.debug.core.ILaunchConfiguration) - */ - public void launchConfigurationRemoved(ILaunchConfiguration configuration) { - if (!configuration.isWorkingCopy()) { - refreshContent(); - } - } - - static TranslationUnitInfo fetchTranslationUnitInfo(Executable executable, Object element) { if (!(element instanceof ITranslationUnit)) { return null; @@ -359,4 +288,17 @@ public class SourceFilesViewer extends BaseViewer implements ISourceLookupPartic } + /** + * The view's refresh action calls this to restart an executable parse for + * the current input if the most recent search (for that element) was + * canceled. If it wasn't canceled, this is a no-op. + */ + public void restartCanceledExecutableParse() { + SourceFilesContentProvider provider = (SourceFilesContentProvider)getContentProvider(); + if (provider != null) { + provider.restartCanceledExecutableParse(); + } + + } + } \ No newline at end of file