From eda34bb2819dd6fef36f869f894fe6c6829f7126 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 10 Aug 2026 15:37:13 +0200 Subject: [PATCH] Never resolve the target platform from PDELabelProvider Label providers run on the UI thread, so they must not ask a question whose answer requires resolving the target platform. PDELabelProvider did, through both model managers, and either one can freeze the IDE for minutes when the target contains an m2e Maven location. Sampling the main thread every 500 ms while an IDE with a Category Definition editor started up put 10 consecutive samples, roughly 8 seconds, in this chain: CategorySection.createClient() TreeViewer.setInput() CategoryLabelProvider.getText() PDELabelProvider.getObjectText(ISiteFeature) FeatureModelManager.findFeatureModel() FeatureModelManager.init() locked ExternalFeatureModelManager.initialize() locked TargetPlatformHelper.getWorkspaceTargetResolved() MavenTargetLocation.resolveArtifacts() locked The reported manifest editor freeze is the same shape through the other manager: RequiresSection.initialize() TableViewer.setInput() PDELabelProvider.getObjectImage(ImportObject) ImportObject.isResolved() PluginRegistry.findModel() PluginModelManager.findEntry() locked getEntryTable() -> initializeTable() TargetPlatformHelper.getWorkspaceTargetResolved() PluginModelManager and FeatureModelManager are two doors into the same room. Whichever is opened first pays for the full resolution and caches the resolved target, which is why the sampled run never entered initializeTable at all: the feature manager had already paid, and the plug-in manager got in free. Fixing one door alone would not change what the user sees. Both are guarded now. arePluginModelsAvailable() and areFeatureModelsAvailable() build on the isInitialized() fast paths both managers already had. While the models are unknown the plain label is returned and initialization is scheduled through the new initializeInBackground(Runnable) on each manager, sharing the race handling in BackgroundInitialization. When it completes the label provider fires a LabelProviderChangedEvent, so every viewer sharing it repaints with the resolved state. Unresolved imports and missing features still get their error overlay once the models are there. Guarded call sites, none of which had a non-resolving path before: plug-in models getObjectImage(ImportObject), getObjectImage(PackageObject), getObjectImage(IProductPlugin), getObjectImage(IFeatureImport), getObjectImage(IFeaturePlugin), getObjectText(IPluginBase), getObjectText(ImportObject), getObjectText(IPluginImport), getObjectText(BundleDescription), getObjectText(FeaturePlugin), getObjectText(ISiteBundle) feature models getObjectText(ISiteFeature), getObjectText(FeatureImport), getObjectImage(IProductFeature), getObjectImage(IFeatureChild), getObjectImage(IFeatureImport) getSystemBundleInfo() also stopped assuming that system.bundle resolves; it dereferenced a possibly null model. initializeInBackground() is new on both managers, but both live in org.eclipse.pde.internal.core, so this is not published API. Firing a PluginModelDelta after initialization was rejected on purpose: it would make PDERegistryStrategy create the extension registry, FeatureRebuilder touch all feature projects and PluginsView add every entry one by one. Both isInitialized() implementations had to become honest and non-blocking for the guards to work at all. FeatureModelManager reported success as soon as fActiveModels was assigned, which happens long before the external features are read, so a caller could be told the models are there and then block on the init() monitor. It now reports the completion of init(). PluginModelManager answered while holding fEntriesSynchronizer, which the background job holds for the whole resolution, so asking whether initialization is needed would wait for exactly that initialization. fEntries is volatile now and is read without the lock; it is only ever assigned a fully populated table. --- .../core/BackgroundInitialization.java | 59 +++++ .../internal/core/FeatureModelManager.java | 52 ++++- .../pde/internal/core/PluginModelManager.java | 38 +++- .../org/eclipse/pde/ui/tests/AllPDETests.java | 2 + .../ui/tests/util/PDELabelProviderTest.java | 209 ++++++++++++++++++ .../pde/internal/ui/PDELabelProvider.java | 132 ++++++++--- 6 files changed, 461 insertions(+), 31 deletions(-) create mode 100644 ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/BackgroundInitialization.java create mode 100644 ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/util/PDELabelProviderTest.java diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/BackgroundInitialization.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/BackgroundInitialization.java new file mode 100644 index 00000000000..3ab24c11957 --- /dev/null +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/BackgroundInitialization.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.internal.core; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; + +/** + * Helper for model managers whose initialization resolves the target platform + * and therefore must never run on the UI thread. + */ +final class BackgroundInitialization { + + private BackgroundInitialization() { + } + + /** + * Schedules the given initialization job unless {@code initialized} already + * reports success, and runs {@code callback} exactly once as soon as the + * models are available. The callback runs on the calling thread if nothing + * had to be scheduled, otherwise on the job's thread. + */ + static void whenInitialized(Job job, BooleanSupplier initialized, Runnable callback) { + if (initialized.getAsBoolean()) { + callback.run(); + return; + } + AtomicBoolean notified = new AtomicBoolean(); + job.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(IJobChangeEvent event) { + event.getJob().removeJobChangeListener(this); + if (notified.compareAndSet(false, true)) { + callback.run(); + } + } + }); + job.schedule(); + // the job may already have finished before the listener was attached + if (initialized.getAsBoolean() && notified.compareAndSet(false, true)) { + callback.run(); + } + } +} diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/FeatureModelManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/FeatureModelManager.java index fe4695d28b0..81350e11557 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/FeatureModelManager.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/FeatureModelManager.java @@ -23,9 +23,11 @@ import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.WorkspaceJob; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.ICoreRunnable; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.pde.core.IModel; import org.eclipse.pde.core.IModelProviderEvent; import org.eclipse.pde.core.IModelProviderListener; @@ -58,12 +60,24 @@ public class FeatureModelManager { private ExternalFeatureModelManager fExternalManager; - private boolean fReloadExternalNeeded = false; + private volatile boolean fReloadExternalNeeded = false; + + /** + * Set once {@link #init()} has done all its work. {@link #fActiveModels} is + * assigned much earlier, so it cannot be used to tell whether the models are + * available. + */ + private volatile boolean fModelsAvailable = false; private final WorkspaceFeatureModelManager fWorkspaceManager; private IModelProviderListener fProviderListener; + /** + * only access synchronized with this + **/ + private Job fInitializationJob; + /** * List of IFeatureModelListener */ @@ -83,15 +97,47 @@ public synchronized void shutdown() { } } + /** + * Returns whether the feature models can be queried without reading the + * external features, which resolves the target platform. + *

+ * Deliberately not synchronized: {@link #init()} holds the instance monitor + * while it resolves, so waiting for it would block the caller for exactly as + * long as the initialization it is asking about. + *

+ */ public boolean isInitialized() { - return (fActiveModels != null && !fReloadExternalNeeded); + return fModelsAvailable && !fReloadExternalNeeded; + } + + /** + * Initializes the feature models in a background job unless they are + * available already, and runs the given callback as soon as they are. + *

+ * The calling thread is never used to resolve the target platform, which + * external feature models are read from. + *

+ */ + public void initializeInBackground(Runnable whenInitialized) { + Job job; + synchronized (this) { + if (fInitializationJob == null) { + fInitializationJob = Job.create(PDECoreMessages.FeatureModelManager_initializingFeatureTargetPlatform, + (ICoreRunnable) monitor -> init()); + fInitializationJob.setPriority(Job.LONG); + } + job = fInitializationJob; + } + BackgroundInitialization.whenInitialized(job, this::isInitialized, whenInitialized); } private synchronized void init() { if (fActiveModels != null) { if (fReloadExternalNeeded) { + fModelsAvailable = false; fReloadExternalNeeded = false; fExternalManager.initialize(); + fModelsAvailable = true; } return; } @@ -132,7 +178,7 @@ public IStatus runInWorkspace(IProgressMonitor monitor) { } else { fExternalManager.initialize(); } - + fModelsAvailable = true; } /* diff --git a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java index 35b25eee27d..724bd822c10 100644 --- a/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java +++ b/ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/PluginModelManager.java @@ -39,6 +39,7 @@ import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.osgi.service.resolver.BundleDelta; import org.eclipse.osgi.service.resolver.BundleDescription; import org.eclipse.osgi.service.resolver.HostSpecification; @@ -117,9 +118,12 @@ public void removeModel(IPluginModelBase model) { private PDEState fState; // keeps the combined view of the target and workspace /** - * only access synchronized with fEntriesSynchronizer + * Only modify synchronized with fEntriesSynchronizer, and only assign a + * fully populated table. Volatile so that {@link #isInitialized()} can read + * it without acquiring fEntriesSynchronizer, which is held for the whole + * target platform resolution. **/ - private Map fEntries; // a master table keyed by plugin ID and the value is a ModelEntry + private volatile Map fEntries; // a master table keyed by plugin ID and the value is a ModelEntry /** * used to synchronize all public methods which (indirectly) use fEntries **/ @@ -129,6 +133,11 @@ public void removeModel(IPluginModelBase model) { private ArrayList fStateListeners; // a list of listeners interested in changes to the PDE/resolver State private boolean fCancelled = false; + /** + * only access synchronized with fEntriesSynchronizer + **/ + private Job fInitializationJob; + /** * Initialize the workspace and external (target) model manager * and add listeners to each one @@ -397,9 +406,32 @@ public boolean isEmpty() { * false otherwise. */ public boolean isInitialized() { + // deliberately not synchronized: fEntriesSynchronizer is held while the + // target platform is resolved, so waiting for it would block the caller + // for exactly as long as the initialization it is asking about + return fEntries != null; + } + + /** + * Initializes the master table in a background job unless it is initialized + * already, and runs the given callback as soon as the table is available. + *

+ * The calling thread is never used to resolve the target platform. The + * callback runs at most once, either directly on the calling thread when the + * table is already initialized, or on the job's thread. + *

+ */ + public void initializeInBackground(Runnable whenInitialized) { + Job job; synchronized (fEntriesSynchronizer) { - return fEntries != null; + if (fInitializationJob == null) { + fInitializationJob = Job.create(PDECoreMessages.PluginModelManager_InitializingPluginModels, + this::initialize); + fInitializationJob.setPriority(Job.LONG); + } + job = fInitializationJob; } + BackgroundInitialization.whenInitialized(job, this::isInitialized, whenInitialized); } /** diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java index 9624426f25a..7fc6d7e8e77 100644 --- a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/AllPDETests.java @@ -38,6 +38,7 @@ import org.eclipse.pde.ui.tests.runtime.AllPDERuntimeTests; import org.eclipse.pde.ui.tests.search.dependencies.GatherUnusedDependenciesOperationTest; import org.eclipse.pde.ui.tests.target.AllTargetTests; +import org.eclipse.pde.ui.tests.util.PDELabelProviderTest; import org.eclipse.pde.ui.tests.views.log.AllLogViewTests; import org.eclipse.pde.ui.tests.wizards.AllNewProjectTests; import org.eclipse.ui.tests.smartimport.ProjectSmartImportTest; @@ -74,6 +75,7 @@ AllPDECoreTests.class, // ProjectSmartImportTest.class, // GatherUnusedDependenciesOperationTest.class, // + PDELabelProviderTest.class, // }) public class AllPDETests { diff --git a/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/util/PDELabelProviderTest.java b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/util/PDELabelProviderTest.java new file mode 100644 index 00000000000..c59defb2821 --- /dev/null +++ b/ui/org.eclipse.pde.ui.tests/src/org/eclipse/pde/ui/tests/util/PDELabelProviderTest.java @@ -0,0 +1,209 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.pde.ui.tests.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.eclipse.pde.core.plugin.PluginRegistry; +import org.eclipse.pde.internal.core.FeatureModelManager; +import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.core.ifeature.IFeatureModel; +import org.eclipse.pde.internal.core.iproduct.IProductFeature; +import org.eclipse.pde.internal.core.isite.ISiteFeature; +import org.eclipse.pde.internal.core.plugin.ImportObject; +import org.eclipse.pde.internal.core.plugin.PluginImport; +import org.eclipse.pde.internal.core.plugin.WorkspacePluginModel; +import org.eclipse.pde.internal.core.product.WorkspaceProductModel; +import org.eclipse.pde.internal.core.site.WorkspaceSiteModel; +import org.eclipse.pde.internal.ui.PDELabelProvider; +import org.eclipse.pde.internal.ui.PDEPluginImages; +import org.eclipse.swt.graphics.Image; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +/** + * Verifies that {@link PDELabelProvider} does not resolve the target platform + * while decorating labels. + */ +public class PDELabelProviderTest { + + private static final String UNRESOLVED_ID = "org.eclipse.pde.tests.does.not.exist"; //$NON-NLS-1$ + private static final String VERSION = "1.0.0"; //$NON-NLS-1$ + + @ClassRule + public static final TestRule RESTORE_TARGET_DEFINITION = TargetPlatformUtil.RESTORE_CURRENT_TARGET_DEFINITION_AFTER; + + @Rule + public final TestRule deleteCreatedProjectsAfter = ProjectUtils.DELETE_CREATED_WORKSPACE_PROJECTS_AFTER; + + private ImportObject fUnresolvedImport; + + @Before + public void setUp() { + WorkspacePluginModel model = new WorkspacePluginModel(null, false); + fUnresolvedImport = new ImportObject(new PluginImport(model, UNRESOLVED_ID)); + } + + /** + * Once the models are available an unresolved import must still be decorated + * with the error overlay. + */ + @Test + public void testUnresolvedImportIsDecoratedWhenModelsAreAvailable() { + PluginRegistry.findEntry(UNRESOLVED_ID); + assertTrue("plug-in models should be initialized", //$NON-NLS-1$ + PDECore.getDefault().getModelManager().isInitialized()); + + PDELabelProvider labelProvider = new PDELabelProvider(); + try { + Image plain = labelProvider.get(PDEPluginImages.DESC_REQ_PLUGIN_OBJ); + assertNotSame("unresolved import should be decorated", plain, //$NON-NLS-1$ + labelProvider.getImage(fUnresolvedImport)); + } finally { + labelProvider.dispose(); + } + } + + /** + * As long as the models are not available the plain image must be returned + * instead of querying the plug-in registry, which would resolve the target + * platform on the calling thread. + */ + @Test + public void testImportIsUndecoratedWhileModelsAreUnavailable() { + PDELabelProvider labelProvider = new PDELabelProvider() { + @Override + protected boolean arePluginModelsAvailable() { + return false; + } + }; + try { + Image plain = labelProvider.get(PDEPluginImages.DESC_REQ_PLUGIN_OBJ); + assertSame("import must not be decorated before the models are known", plain, //$NON-NLS-1$ + labelProvider.getImage(fUnresolvedImport)); + } finally { + labelProvider.dispose(); + } + } + + /** + * Once the feature models are available a feature that cannot be found must + * still be decorated with the error overlay. + */ + @Test + public void testUnresolvedFeatureIsDecoratedWhenModelsAreAvailable() { + FeatureModelManager manager = PDECore.getDefault().getFeatureModelManager(); + manager.getModels(); + assertTrue("feature models should be initialized", manager.isInitialized()); //$NON-NLS-1$ + + PDELabelProvider labelProvider = new PDELabelProvider(); + try { + Image plain = labelProvider.get(PDEPluginImages.DESC_FEATURE_OBJ); + assertNotSame("unresolved feature should be decorated", plain, //$NON-NLS-1$ + labelProvider.getImage(createProductFeature(UNRESOLVED_ID))); + } finally { + labelProvider.dispose(); + } + } + + /** + * As long as the feature models are not available the plain image must be + * returned instead of querying the feature model manager, which would + * resolve the target platform on the calling thread. + */ + @Test + public void testFeatureIsUndecoratedWhileModelsAreUnavailable() { + PDELabelProvider labelProvider = newProviderWithoutFeatureModels(); + try { + Image plain = labelProvider.get(PDEPluginImages.DESC_FEATURE_OBJ); + assertSame("feature must not be decorated before the models are known", plain, //$NON-NLS-1$ + labelProvider.getImage(createProductFeature(UNRESOLVED_ID))); + } finally { + labelProvider.dispose(); + } + } + + /** + * The label of a site feature falls back to its URL while the feature models + * are unavailable. This is the path that froze the UI when a category + * definition editor was restored on start-up. + */ + @Test + public void testSiteFeatureFallsBackToUrlWhileModelsAreUnavailable() throws Exception { + ISiteFeature siteFeature = createSiteFeature(UNRESOLVED_ID); + + PDELabelProvider labelProvider = newProviderWithoutFeatureModels(); + try { + assertEquals("site feature must not be resolved before the models are known", siteFeature.getURL(), //$NON-NLS-1$ + labelProvider.getObjectText(siteFeature)); + } finally { + labelProvider.dispose(); + } + } + + /** + * Once the feature models are available the site feature is labeled with the + * feature it points to, not with its URL. + */ + @Test + public void testSiteFeatureIsResolvedWhenModelsAreAvailable() throws Exception { + TargetPlatformUtil.setRunningPlatformAsTarget(); + String id = "org.eclipse.pde.tests.label.feature"; //$NON-NLS-1$ + ProjectUtils.createFeatureProject(id, VERSION, f -> { + }); + IFeatureModel featureModel = PDECore.getDefault().getFeatureModelManager().findFeatureModel(id, VERSION); + assertNotNull("feature model should be known to the manager", featureModel); //$NON-NLS-1$ + + ISiteFeature siteFeature = createSiteFeature(id); + + PDELabelProvider labelProvider = new PDELabelProvider(); + try { + assertEquals("site feature should be labeled with the resolved feature", //$NON-NLS-1$ + labelProvider.getObjectText(featureModel), labelProvider.getObjectText(siteFeature)); + } finally { + labelProvider.dispose(); + } + } + + private static PDELabelProvider newProviderWithoutFeatureModels() { + return new PDELabelProvider() { + @Override + protected boolean areFeatureModelsAvailable() { + return false; + } + }; + } + + private static IProductFeature createProductFeature(String id) { + IProductFeature feature = new WorkspaceProductModel(null, false).getFactory().createFeature(); + feature.setId(id); + feature.setVersion(VERSION); + return feature; + } + + private static ISiteFeature createSiteFeature(String id) throws Exception { + ISiteFeature feature = new WorkspaceSiteModel(null).getFactory().createFeature(); + feature.setId(id); + feature.setVersion(VERSION); + feature.setURL("features/" + id + '_' + VERSION + ".jar"); //$NON-NLS-1$ //$NON-NLS-2$ + return feature; + } +} diff --git a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java index a6150e5175e..6a7d2b81788 100644 --- a/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java +++ b/ui/org.eclipse.pde.ui/src/org/eclipse/pde/internal/ui/PDELabelProvider.java @@ -17,6 +17,7 @@ package org.eclipse.pde.internal.ui; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -25,6 +26,7 @@ import org.eclipse.jdt.ui.ISharedImages; import org.eclipse.jdt.ui.JavaUI; import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.LabelProviderChangedEvent; import org.eclipse.osgi.service.resolver.BundleDescription; import org.eclipse.osgi.service.resolver.ResolverError; import org.eclipse.osgi.util.NLS; @@ -44,8 +46,10 @@ import org.eclipse.pde.core.plugin.IPluginObject; import org.eclipse.pde.core.plugin.PluginRegistry; import org.eclipse.pde.core.plugin.VersionMatchRule; +import org.eclipse.pde.internal.core.FeatureModelManager; import org.eclipse.pde.internal.core.ICoreConstants; import org.eclipse.pde.internal.core.PDECore; +import org.eclipse.pde.internal.core.PluginModelManager; import org.eclipse.pde.internal.core.TargetPlatformHelper; import org.eclipse.pde.internal.core.WorkspaceModelManager; import org.eclipse.pde.internal.core.builders.CompilerFlags; @@ -90,6 +94,7 @@ import org.eclipse.pde.internal.ui.util.SWTUtil; import org.eclipse.pde.internal.ui.util.SharedLabelProvider; import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; import org.eclipse.ui.PlatformUI; import org.osgi.framework.Version; import org.osgi.resource.Resource; @@ -97,9 +102,71 @@ public class PDELabelProvider extends SharedLabelProvider { private static final String SYSTEM_BUNDLE = "system.bundle"; //$NON-NLS-1$ + private final AtomicBoolean fPluginInitializationScheduled = new AtomicBoolean(); + private final AtomicBoolean fFeatureInitializationScheduled = new AtomicBoolean(); + public PDELabelProvider() { } + /** + * Returns whether the plug-in models can be queried without resolving the + * target platform, and schedules a background initialization if they cannot. + *

+ * Every lookup in the plug-in registry has to be guarded by this check: + * resolving the target platform can take minutes and label providers are + * called on the UI thread. Labels are refreshed once initialization has + * finished. + *

+ */ + protected boolean arePluginModelsAvailable() { + PluginModelManager manager = PDECore.getDefault().getModelManager(); + if (manager.isInitialized()) { + return true; + } + if (fPluginInitializationScheduled.compareAndSet(false, true)) { + manager.initializeInBackground(() -> { + // if initialization failed the flag stays set, so that a label + // refresh cannot schedule the failing job over and over again + if (manager.isInitialized()) { + fPluginInitializationScheduled.set(false); + } + refreshLabels(); + }); + } + return false; + } + + /** + * Same contract as {@link #arePluginModelsAvailable()}, for the feature + * models. Reading the external feature models resolves the target platform + * just like the plug-in models do. + */ + protected boolean areFeatureModelsAvailable() { + FeatureModelManager manager = PDECore.getDefault().getFeatureModelManager(); + if (manager.isInitialized()) { + return true; + } + if (fFeatureInitializationScheduled.compareAndSet(false, true)) { + manager.initializeInBackground(() -> { + if (manager.isInitialized()) { + fFeatureInitializationScheduled.set(false); + } + refreshLabels(); + }); + } + return false; + } + + private void refreshLabels() { + if (!PlatformUI.isWorkbenchRunning()) { + return; + } + Display display = PlatformUI.getWorkbench().getDisplay(); + if (!display.isDisposed()) { + display.asyncExec(() -> fireLabelProviderChanged(new LabelProviderChangedEvent(this))); + } + } + @Override public String getText(Object obj) { if (obj instanceof IPluginModelBase) { @@ -209,8 +276,14 @@ public String getObjectText(IPluginBase pluginBase) { } private String getSystemBundleInfo() { - IPluginBase systemBundle = PluginRegistry.findModel(SYSTEM_BUNDLE).getPluginBase(); - return NLS.bind(" [{0}]", systemBundle.getId()); //$NON-NLS-1$ + if (!arePluginModelsAvailable()) { + return ""; //$NON-NLS-1$ + } + IPluginModelBase model = PluginRegistry.findModel(SYSTEM_BUNDLE); + if (model == null) { + return ""; //$NON-NLS-1$ + } + return NLS.bind(" [{0}]", model.getPluginBase().getId()); //$NON-NLS-1$ } private String preventNull(String text) { @@ -253,7 +326,7 @@ public String getObjectText(IProductPlugin obj) { public String getObjectText(BundleDescription bundle) { String id = bundle.getSymbolicName(); - if (isFullNameModeEnabled()) { + if (isFullNameModeEnabled() && arePluginModelsAvailable()) { IPluginModelBase model = PluginRegistry.findModel((Resource) bundle); if (model != null) { return model.getPluginBase().getTranslatedName(); @@ -264,7 +337,7 @@ public String getObjectText(BundleDescription bundle) { } public String getObjectText(IPluginImport obj) { - if (isFullNameModeEnabled()) { + if (isFullNameModeEnabled() && arePluginModelsAvailable()) { String id = obj.getId(); IPluginModelBase model = PluginRegistry.findModel(obj.getId()); if (model != null) { @@ -314,7 +387,7 @@ private String getObjectText(Locale obj) { } public String getObjectText(FeaturePlugin obj) { - String name = isFullNameModeEnabled() ? obj.getLabel() : obj.getId(); + String name = isFullNameModeEnabled() && arePluginModelsAvailable() ? obj.getLabel() : obj.getId(); String version = obj.getVersion(); String text; @@ -330,12 +403,12 @@ public String getObjectText(FeaturePlugin obj) { public String getObjectText(FeatureImport obj) { int type = obj.getType(); if (type == IFeatureImport.PLUGIN) { - IPlugin plugin = obj.getPlugin(); + IPlugin plugin = arePluginModelsAvailable() ? obj.getPlugin() : null; if (plugin != null && isFullNameModeEnabled()) { return preventNull(plugin.getTranslatedName()); } } else if (type == IFeatureImport.FEATURE) { - IFeature feature = obj.getFeature(); + IFeature feature = areFeatureModelsAvailable() ? obj.getFeature() : null; if (feature != null && isFullNameModeEnabled()) { return preventNull(feature.getTranslatableLabel()); } @@ -385,9 +458,12 @@ private String getObjectText(IProductModel obj) { } public String getObjectText(ISiteFeature obj) { - IFeatureModel model = PDECore.getDefault().getFeatureModelManager().findFeatureModel(obj.getId(), obj.getVersion() != null ? obj.getVersion() : ICoreConstants.DEFAULT_VERSION); - if (model != null) { - return getObjectText(model); + if (areFeatureModelsAvailable()) { + IFeatureModel model = PDECore.getDefault().getFeatureModelManager().findFeatureModel(obj.getId(), + obj.getVersion() != null ? obj.getVersion() : ICoreConstants.DEFAULT_VERSION); + if (model != null) { + return getObjectText(model); + } } String url = obj.getURL(); if (url != null) { @@ -397,10 +473,12 @@ public String getObjectText(ISiteFeature obj) { } public String getObjectText(ISiteBundle obj) { - IPluginModelBase modelBase = PluginRegistry.findModel(obj.getId(), obj.getVersion(), - VersionMatchRule.COMPATIBLE); - if (modelBase != null) { - return getObjectText(modelBase.getPluginBase()); + if (arePluginModelsAvailable()) { + IPluginModelBase modelBase = PluginRegistry.findModel(obj.getId(), obj.getVersion(), + VersionMatchRule.COMPATIBLE); + if (modelBase != null) { + return getObjectText(modelBase.getPluginBase()); + } } return preventNull(obj.getId()); } @@ -644,7 +722,9 @@ public Image getObjectImage(IFragment fragment, boolean checkEnabled, boolean ja private Image getObjectImage(ImportObject iobj) { int flags = 0; IPluginImport iimport = iobj.getImport(); - if (!iobj.isResolved()) { + // The resolved state is only known once the target platform is available + boolean modelsAvailable = arePluginModelsAvailable(); + if (modelsAvailable && !iobj.isResolved()) { flags = iimport.isOptional() ? F_WARNING : F_ERROR; } else if (iimport.isReexported()) { flags = F_EXPORT; @@ -652,7 +732,7 @@ private Image getObjectImage(ImportObject iobj) { if (iimport.isOptional()) { flags |= F_OPTIONAL; } - IPlugin plugin = iobj.getPlugin(); + IPlugin plugin = modelsAvailable ? iobj.getPlugin() : null; if (plugin != null) { IPluginModelBase model = plugin.getPluginModel(); flags |= getModelFlags(model); @@ -712,6 +792,9 @@ private Image getObjectImage(IPluginImport obj) { } private Image getObjectImage(IProductPlugin obj) { + if (!arePluginModelsAvailable()) { + return get(PDEPluginImages.DESC_PLUGIN_OBJ); + } Version version = (obj.getVersion() != null && obj.getVersion().length() > 0 && !obj.getVersion().equals(ICoreConstants.DEFAULT_VERSION)) ? Version.parseVersion(obj.getVersion()) : null; BundleDescription desc = TargetPlatformHelper.getState().getBundle(obj.getId(), version); if (desc != null) { @@ -781,7 +864,7 @@ private Image getObjectImage(IFeatureURLElement url) { private Image getObjectImage(IFeaturePlugin plugin) { int flags = 0; - if (((FeaturePlugin) plugin).getPluginBase() == null) { + if (arePluginModelsAvailable() && ((FeaturePlugin) plugin).getPluginBase() == null) { int cflag = CompilerFlags.getFlag(null, CompilerFlags.F_UNRESOLVED_PLUGINS); if (cflag == CompilerFlags.ERROR) { flags = F_ERROR; @@ -797,7 +880,7 @@ private Image getObjectImage(IFeaturePlugin plugin) { private Image getObjectImage(IFeatureChild feature) { int flags = 0; - if (((FeatureChild) feature).getReferencedFeature() == null) { + if (areFeatureModelsAvailable() && ((FeatureChild) feature).getReferencedFeature() == null) { int cflag = CompilerFlags.getFlag(null, CompilerFlags.F_UNRESOLVED_FEATURES); if (cflag == CompilerFlags.ERROR) { flags = F_ERROR; @@ -811,8 +894,8 @@ private Image getObjectImage(IFeatureChild feature) { private Image getObjectImage(IProductFeature feature) { int flags = 0; String version = feature.getVersion().length() > 0 ? feature.getVersion() : ICoreConstants.DEFAULT_VERSION; - IFeatureModel model = PDECore.getDefault().getFeatureModelManager().findFeatureModel(feature.getId(), version); - if (model == null) { + if (areFeatureModelsAvailable() + && PDECore.getDefault().getFeatureModelManager().findFeatureModel(feature.getId(), version) == null) { flags = F_ERROR; } return get(PDEPluginImages.DESC_FEATURE_OBJ, flags); @@ -835,14 +918,12 @@ private Image getObjectImage(IFeatureImport obj) { if (type == IFeatureImport.FEATURE) { base = PDEPluginImages.DESC_FEATURE_OBJ; - IFeature feature = iimport.getFeature(); - if (feature == null) { + if (areFeatureModelsAvailable() && iimport.getFeature() == null) { flags = F_ERROR; } } else { base = PDEPluginImages.DESC_REQ_PLUGIN_OBJ; - IPlugin plugin = iimport.getPlugin(); - if (plugin == null) { + if (arePluginModelsAvailable() && iimport.getPlugin() == null) { flags = F_ERROR; } } @@ -907,7 +988,8 @@ public Image getObjectImage(PackageObject obj) { if (importPackageObject.isOptional()) { flags |= F_OPTIONAL; } - if (!importPackageObject.isResolved()) { + // The resolved state is only known once the target platform is available + if (arePluginModelsAvailable() && !importPackageObject.isResolved()) { flags |= importPackageObject.isOptional() ? F_WARNING : F_ERROR; } }