Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh the bundles after uninstalling #1253

Merged
merged 10 commits into from
Nov 13, 2019

Conversation

jdneo
Copy link
Contributor

@jdneo jdneo commented Nov 8, 2019

This PR add two things in loadBundles:

  • Refresh the uninstalled bundles before starting bundles
  • Avoid multiple singleton bundles installed

Why need this change

Scenario one

When a JDT.LS extension get updated, though the required bundle's version might be the same, but the location might be changed. Then the previous bundle get uninstalled, and the new bundle get installed. Now the framework will have both the uninstalled and installed bundles. When the bundle is declared as singleton, we will get an error when start the installed bundle.

Trigger the refresh action can avoid this exception

Scenario two

Two different extensions both want to install the same bundle with different version. If the bundle is declared as singleton, installing two different version of this bundle will cause exceptions.

So we can use a Collection to persist those installed singleton bundles.

Signed-off-by: Sheng Chen [email protected]

@jdneo jdneo force-pushed the cs/test-bundleload branch from e600cb5 to dcaa1a0 Compare November 8, 2019 05:47
@fbricon
Copy link
Contributor

fbricon commented Nov 8, 2019

Tests fail on Jenkins

if (installedBundle != null) {
Module module = installedBundle.adapt(Module.class);
if (isSingleton(module.getCurrentRevision())) {
// We have installed this singleton bundle before, so skip this one.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support installing two different versions of a non-singleton jar, make sense to me. But if two extensions try to install the same version of a non-singleton jar, will it succeed?

frameworkWiring.refreshBundles(toRefresh, new FrameworkListener() {
@Override
public void frameworkEvent(FrameworkEvent event) {
latch.countDown();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need check the specific finish event such as PACKAGES_REFRESHED, ERROR, STOPPED ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I should add event.getType() == FrameworkEvent.PACKAGES_REFRESHED check

@jdneo
Copy link
Contributor Author

jdneo commented Nov 8, 2019

Might caused by the JUnit 5's required bundles are not installed in the test environment. I'm trying the build a new bundle for test purpose which has no required bundles.

@@ -194,7 +215,9 @@ private static void refreshBundles(Set<Bundle> toRefresh, FrameworkWiring framew
frameworkWiring.refreshBundles(toRefresh, new FrameworkListener() {
@Override
public void frameworkEvent(FrameworkEvent event) {
latch.countDown();
if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when it's FrameworkEvent.ERROR, also exit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to find if there is any example also handling other events when refresh the packages but with no success.

Is there any example I can refer to?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No example yet. i just read the API document of FrameworkListener, it says when there is any exception happening during refresh, it will report ERROR event.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Then how about log an error message when this happens?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we always are able to receive a FrameworkEvent.PACKAGES_REFRESHED event regardless of success or fail, i'm OK. My point is to make sure the latch.await is not blocking when there is bad status happening.

boolean shouldSkip = false;
for (Bundle bundle : installedBundleList) {
Module module = bundle.adapt(Module.class);
if (isSingleton(module.getCurrentRevision())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The singleton information can be resolved from the method getBundleInfo(String bundleLocation) directly, no need to get it from the module revision.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean getting it by parsing the symbol name? What if it contains whitespace?

Copy link
Contributor

@testforstephen testforstephen Nov 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a sample format Bundle-SymbolicName: org.eclipse.jdt.ls.core;singleton:=true in MANIFEST.MF. Currently before installing the new bundle, we already have logic to resolve the symbolic name and version info from the manifest file directly, why not bring the singleton attribute by the way.

Copy link
Contributor Author

@jdneo jdneo Nov 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a way to do it:

ManifestElement[] symbolicNameElements = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, symbolicName);
if (symbolicNameElements.length > 0) {
    symbolicName = symbolicNameElements[0].getValue();
    String singleton = symbolicNameElements[0].getDirective(Constants.SINGLETON_DIRECTIVE);
    isSingleton = "true".equals(singleton);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the existing utility method in BundleUtils. Looks like your sample code is better.

	private static BundleInfo getBundleInfo(String bundleLocation) throws IOException {
		try (JarFile jarFile = new JarFile(bundleLocation)) {
			Manifest manifest = jarFile.getManifest();
			if (manifest != null) {
				Attributes mainAttributes = manifest.getMainAttributes();
				if (mainAttributes != null) {
					String bundleVersion = mainAttributes.getValue(Constants.BUNDLE_VERSION);
					if (StringUtils.isBlank(bundleVersion)) {
						return null;
					}
					String symbolicName = mainAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME);
					if (StringUtils.isNotBlank(symbolicName) && symbolicName.indexOf(';') >= 0) {
						symbolicName = symbolicName.substring(0, symbolicName.indexOf(';'));
					}
					return new BundleInfo(bundleVersion, symbolicName);
				}
			}
		}
		return null;
	}

@jdneo
Copy link
Contributor Author

jdneo commented Nov 11, 2019

Code updated.

@jdneo jdneo force-pushed the cs/test-bundleload branch from 4645b18 to a34f88a Compare November 11, 2019 05:27
latch.countDown();
} else if (event.getType() == FrameworkEvent.ERROR) {
latch.countDown();
JavaLanguageServerPlugin.logError("Error happens when refreshing the bundles");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

carry the throwable info into the log.

@jdneo
Copy link
Contributor Author

jdneo commented Nov 11, 2019

test this please

assertEquals(newBundle.getState(), Bundle.ACTIVE);
} finally {
// Uninstall the bundle to clean up the testing bundle context.
newBundle.uninstall();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BundleUtils.loadBundles(Arrays.asList(oldBundlePath, oldDependencyPath));
BundleUtils.loadBundles(Arrays.asList(newBundlePath, newDependencyPath));
probably throw uncaught exception, need extend the enclosing scope for try->finally.

@jdneo
Copy link
Contributor Author

jdneo commented Nov 12, 2019

@fbricon @testforstephen @snjeza Please let me know if there is any concern about this change.

Copy link
Contributor

@fbricon fbricon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me what happens if 2 extensions are loaded, each of them having a dependency on the same bundle, same version, singleton or not, coming from 2 different urls

@@ -158,6 +160,187 @@ public void testLoadSameBundleMultipleTimes() throws Exception {
}
}

@Test
public void testLoadMultipleNonSongletonBundles() throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testLoadMultipleNonSingletonBundles

if (bundleInfo == null) {
status.add(new Status(IStatus.ERROR, context.getBundle().getSymbolicName(), "Failed to get bundleInfo for bundle from " + bundleLocation, null));
continue;
}

// Platform.getBundle() won't return the bundle with INSTALLED state, so we use this Map to persist the current installed bundles
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can try the following method:

private static Bundle getBundle(String symbolicName) {
		FrameworkWiring fwkWiring = JavaLanguageServerPlugin.getBundleContext().getBundle(0).adapt(FrameworkWiring.class);
		BundleContext context = fwkWiring.getBundle().getBundleContext();
		if (Constants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(symbolicName)) {
			symbolicName = context.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).getSymbolicName();
		}
		StringBuilder filter = new StringBuilder();
		filter.append('(')
			.append(IdentityNamespace.IDENTITY_NAMESPACE)
			.append('=')
			.append(symbolicName)
			.append(')');
		Map<String, String> directives = Collections.singletonMap(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter.toString());
		Collection<BundleCapability> matchingBundleCapabilities = fwkWiring.findProviders(ModuleContainer.createRequirement(IdentityNamespace.IDENTITY_NAMESPACE, directives, Collections.emptyMap()));
		if (matchingBundleCapabilities.isEmpty()) {
			return null;
		}
		Bundle[] results = matchingBundleCapabilities.stream().map(c -> c.getRevision().getBundle())
				// Remove all the bundles that are uninstalled
				.filter(bundle -> (bundle.getState() & (Bundle.UNINSTALLED)) == 0)
				.sorted((b1, b2) -> b2.getVersion().compareTo(b1.getVersion())) // highest version first
				.toArray(Bundle[]::new);
		return results.length > 0 ? results[0] : null;
	}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @snjeza, this method works!

@jdneo jdneo force-pushed the cs/test-bundleload branch from a82445b to 34d5f95 Compare November 13, 2019 03:10
@jdneo
Copy link
Contributor Author

jdneo commented Nov 13, 2019

test this please

@jdneo
Copy link
Contributor Author

jdneo commented Nov 13, 2019

Summary of the current behavior:

  • Bundles with same symbolic name and same version will only be loaded with one instance, which is the first come one.
  • Non singleton bundles can be loaded with multiple instances as long as their versions are not the same
  • Singleton bundle will only be loaded with one instance, the last come one wins.

if (bundles != null) {
boolean shouldSkip = false;
for (Bundle bundle : bundles) {
if (bundle.getVersion().equals(Version.parseVersion(bundleInfo.getVersion()))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please compute Version.parseVersion(bundleInfo.getVersion())) only once, before the loop

@@ -118,11 +118,12 @@ public static void loadBundles(Collection<String> bundleLocations) throws CoreEx
continue;
}

Version currentBundleVersion = Version.parseVersion(bundleInfo.getVersion());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move within if (bundles != null) block please

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@fbricon fbricon merged commit 094fa48 into eclipse-jdtls:master Nov 13, 2019
@fbricon fbricon added this to the Mid November 2019 milestone Nov 13, 2019
@jdneo jdneo deleted the cs/test-bundleload branch November 21, 2019 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants