Skip to content

Commit

Permalink
Add a 'wires' command to allow easy investigation of bundle wires
Browse files Browse the repository at this point in the history
Currently it is quite hard to find out if/why a certain bundle is using
some other bundles or are used by others.

This adds a new 'wires' command that when executed against a given
bundle prints all wires that are used by the bundle and any wires it
provides to other bundles. This results in a very powerful tool to
analyze dependency problems and find out why a bundle is actually used.
  • Loading branch information
laeubi committed Jan 5, 2024
1 parent d52b39e commit f463b8e
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 108 deletions.
1 change: 1 addition & 0 deletions bundles/org.eclipse.equinox.console/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Bundle-Vendor: %bundleVendor
Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: org.apache.felix.service.command;version="[1.0,2.0)",
org.eclipse.osgi.container;version="[1.7.0,2.0.0]",
org.eclipse.osgi.framework.console,
org.eclipse.osgi.report.resolution;version="[1.0,2.0)",
org.eclipse.osgi.service.environment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.eclipse.equinox.console.commands.EquinoxCommandProvider;
import org.eclipse.equinox.console.commands.HelpCommand;
import org.eclipse.equinox.console.commands.ManCommand;
import org.eclipse.equinox.console.commands.WireCommand;
import org.eclipse.equinox.console.telnet.TelnetCommand;
import org.eclipse.osgi.framework.console.CommandInterpreter;
import org.eclipse.osgi.framework.console.CommandProvider;
Expand Down Expand Up @@ -324,6 +325,9 @@ public void start(BundleContext context) throws Exception {

CommandsTracker commandsTracker = new CommandsTracker(context);
context.registerService(CommandsTracker.class.getName(), commandsTracker, null);

WireCommand wireCommand = new WireCommand(context);
wireCommand.startService();

GOGO.RUNTIME.start(frameworkWiring);
GOGO.SHELL.start(frameworkWiring);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*******************************************************************************
* Copyright (c) 2024 Christoph Läubrich 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:
* Christoph Läubrich - initial API and implementation
*******************************************************************************/
package org.eclipse.equinox.console.commands;

import java.io.PrintStream;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.felix.service.command.CommandProcessor;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Descriptor;
import org.eclipse.osgi.container.ModuleContainer;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.BundleWire;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.resource.Resource;

/**
* provides a command to print information about the wiring of a bundle
*/
public class WireCommand {

private static final Comparator<BundleRevision> BUNDLE_REVISIONS_BY_NAME = Comparator.comparing(
BundleRevision::getSymbolicName, String.CASE_INSENSITIVE_ORDER);

private BundleContext context;

public WireCommand(BundleContext context) {
this.context = context;
}

@Descriptor("Prints information about the wiring of a particular bundle")
public void wires(CommandSession session, long id) {
PrintStream console = session.getConsole();
Bundle bundle = context.getBundle(id);
if (bundle == null) {
console.println(String.format("Bundle with id %d not found!", id));
return;
}
BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
if (bundleWiring == null) {
console.println(String.format("Bundle with id %d has no wiring!", id));
return;
}
console.println("Bundle " + bundle.getSymbolicName() + " " + bundle.getVersion() + ":");
printWiring(console, bundleWiring);
}

static void printWiring(PrintStream console, BundleWiring bundleWiring) {
BundleRevision resource = bundleWiring.getResource();
Map<BundleRevision, List<BundleWire>> usedWires =
bundleWiring.getRequiredWires(null).stream()
.filter(bw -> !resource.equals(bw.getProvider()))
.collect(Collectors.groupingBy(BundleWire::getProvider));
if (usedWires.isEmpty()) {
console.println("is not wired to any other bundle");
} else {
console.println("is wired to:");
usedWires.entrySet().stream().sorted(Comparator
.comparing(java.util.Map.Entry::getKey, BUNDLE_REVISIONS_BY_NAME))
.forEach(bre -> {
console.println("\t - " + getResource(bre.getKey()));
for (BundleWire bw : bre.getValue()) {
console.println("\t - because of "
+ ModuleContainer.toString(bw.getRequirement()));
}
});

}
Map<BundleRevision, List<BundleWire>> consumersWires =
bundleWiring.getProvidedWires(null).stream()
.collect(Collectors.groupingBy(BundleWire::getRequirer));
if (consumersWires.isEmpty()) {
console.println("and is not consumed by any bundle");
} else {
console.println("and is consumed by:");
consumersWires.entrySet().stream().sorted(Comparator
.comparing(java.util.Map.Entry::getKey, BUNDLE_REVISIONS_BY_NAME))
.forEach(bre -> {
console.println("\t - " + getResource(bre.getKey()));
for (BundleWire bw : bre.getValue()) {
console.println("\t - because it "
+ ModuleContainer.toString(bw.getRequirement()));
}
});
}
}

static String getResource(Resource resource) {
if (resource instanceof BundleRevision) {
BundleRevision bundleRevision = (BundleRevision) resource;
return bundleRevision.getSymbolicName() + " " + bundleRevision.getVersion();
}
return String.valueOf(resource);
}

public void startService() {
Dictionary<String, Object> dict = new Hashtable<>();
dict.put(CommandProcessor.COMMAND_SCOPE, "wiring");
dict.put(CommandProcessor.COMMAND_FUNCTION, new String[] { "wires" });
context.registerService(WireCommand.class, this, dict);
}

}
4 changes: 2 additions & 2 deletions bundles/org.eclipse.osgi/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Bundle-ManifestVersion: 2
Export-Package: org.eclipse.core.runtime.adaptor;x-friends:="org.eclipse.core.runtime",
org.eclipse.core.runtime.internal.adaptor;x-internal:=true,
org.eclipse.equinox.log;version="1.1";uses:="org.osgi.framework,org.osgi.service.log",
org.eclipse.osgi.container;version="1.6";
org.eclipse.osgi.container;version="1.7.0";
uses:="org.eclipse.osgi.report.resolution,
org.osgi.framework.wiring,
org.eclipse.osgi.framework.eventmgr,
Expand Down Expand Up @@ -107,7 +107,7 @@ Bundle-Activator: org.eclipse.osgi.internal.framework.SystemBundleActivator
Bundle-Description: %systemBundle
Bundle-Copyright: %copyright
Bundle-Vendor: %eclipse.org
Bundle-Version: 3.18.700.qualifier
Bundle-Version: 3.19.0.qualifier
Bundle-Localization: systembundle
Bundle-DocUrl: http://www.eclipse.org
Eclipse-ExtensibleAPI: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ public ModuleRevision getResource() {

@Override
public String toString() {
return namespace + ModuleRevision.toString(attributes, false) + ModuleRevision.toString(directives, true);
return namespace + ModuleContainer.toString(attributes, false) + ModuleContainer.toString(directives, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.eclipse.osgi.internal.container.NamespaceList;
import org.eclipse.osgi.internal.debug.Debug;
import org.eclipse.osgi.internal.framework.EquinoxConfiguration;
import org.eclipse.osgi.internal.framework.FilterImpl;
import org.eclipse.osgi.internal.messages.Msg;
import org.eclipse.osgi.report.resolution.ResolutionReport;
import org.eclipse.osgi.report.resolution.ResolutionReport.Entry;
Expand All @@ -66,14 +67,17 @@
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.Version;
import org.osgi.framework.namespace.BundleNamespace;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.startlevel.FrameworkStartLevel;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.resource.Capability;
import org.osgi.resource.Namespace;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
Expand Down Expand Up @@ -223,6 +227,112 @@ public static Requirement createRequirement(String namespace, Map<String, String
return new ModuleRequirement(namespace, directives, attributes, null);
}

/**
* Generates a human readable string representation of the the given capability,
* mapping the namespace to well know header names.
*
* @param capability the {@link Capability} where a string representation is
* desired
* @since 3.19
*/
public static String toString(Capability capability) {
if (PackageNamespace.PACKAGE_NAMESPACE.equals(capability.getNamespace())) {
return Constants.EXPORT_PACKAGE + ": " + createOSGiCapability(capability); //$NON-NLS-1$
} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(capability.getNamespace())) {
return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(capability); //$NON-NLS-1$
} else if (HostNamespace.HOST_NAMESPACE.equals(capability.getNamespace())) {
return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(capability); //$NON-NLS-1$
}
return Constants.PROVIDE_CAPABILITY + ": " + capability.toString(); //$NON-NLS-1$
}

private static String createOSGiCapability(Capability cap) {
Map<String, Object> attributes = new HashMap<>(cap.getAttributes());
Map<String, String> directives = cap.getDirectives();
String name = String.valueOf(attributes.remove(cap.getNamespace()));
return name + toString(attributes, false, true) + toString(directives, true, true);
}

/**
* Generates a human readable string representation of the the given
* requirement, mapping the namespace to well know header names.
*
* @param requirement the {@link Requirement} where a string representation is
* desired
* @since 3.19
*/
public static String toString(Requirement requirement) {
if (PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace())) {
return Constants.IMPORT_PACKAGE + ": " //$NON-NLS-1$
+ createOSGiRequirement(requirement, PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE,
PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE);
} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(requirement.getNamespace())) {
return Constants.REQUIRE_BUNDLE + ": " //$NON-NLS-1$
+ createOSGiRequirement(requirement, BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE);
} else if (HostNamespace.HOST_NAMESPACE.equals(requirement.getNamespace())) {
return Constants.FRAGMENT_HOST + ": " //$NON-NLS-1$
+ createOSGiRequirement(requirement, HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE);
}
return Constants.REQUIRE_CAPABILITY + ": " + requirement.toString(); //$NON-NLS-1$
}

private static String createOSGiRequirement(Requirement requirement, String... versions) {
Map<String, String> directives = new HashMap<>(requirement.getDirectives());
String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE);
if (filter == null)
throw new IllegalArgumentException("No filter directive found:" + requirement); //$NON-NLS-1$
FilterImpl filterImpl;
try {
filterImpl = FilterImpl.newInstance(filter);
} catch (InvalidSyntaxException e) {
throw new IllegalArgumentException("Invalid filter directive", e); //$NON-NLS-1$
}
Map<String, String> matchingAttributes = filterImpl.getStandardOSGiAttributes(versions);
String name = matchingAttributes.remove(requirement.getNamespace());
if (name == null)
throw new IllegalArgumentException("Invalid requirement: " + requirement); //$NON-NLS-1$
return name + toString(matchingAttributes, false, true) + toString(directives, true, true);
}

static <V> String toString(Map<String, V> map, boolean directives) {
return toString(map, directives, false);
}

static <V> String toString(Map<String, V> map, boolean directives, boolean stringsOnly) {
if (map.size() == 0)
return ""; //$NON-NLS-1$
String assignment = directives ? ":=" : "="; //$NON-NLS-1$ //$NON-NLS-2$
Set<java.util.Map.Entry<String, V>> set = map.entrySet();
StringBuilder sb = new StringBuilder();
for (java.util.Map.Entry<String, V> entry : set) {
sb.append("; "); //$NON-NLS-1$
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof List) {
@SuppressWarnings("unchecked")
List<Object> list = (List<Object>) value;
if (list.isEmpty())
continue;
Object component = list.get(0);
String className = component.getClass().getName();
String type = className.substring(className.lastIndexOf('.') + 1);
sb.append(key).append(':').append("List<").append(type).append(">").append(assignment).append('"'); //$NON-NLS-1$ //$NON-NLS-2$
for (Object object : list)
sb.append(object).append(',');
sb.setLength(sb.length() - 1);
sb.append('"');
} else {
String type = ""; //$NON-NLS-1$
if (!(value instanceof String) && !stringsOnly) {
String className = value.getClass().getName();
type = ":" + className.substring(className.lastIndexOf('.') + 1); //$NON-NLS-1$
}
sb.append(key).append(type).append(assignment).append('"').append(value).append('"');
}
}
return sb.toString();
}

/**
* Installs a new module using the specified location. The specified
* builder is used to create a new {@link ModuleRevision revision}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.eclipse.osgi.internal.container.Capabilities;
import org.eclipse.osgi.internal.framework.FilterImpl;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.namespace.*;
import org.osgi.framework.namespace.BundleNamespace;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRequirement;
import org.osgi.resource.Namespace;
Expand Down Expand Up @@ -87,7 +89,7 @@ public ModuleRevision getResource() {

@Override
public String toString() {
return namespace + ModuleRevision.toString(attributes, false) + ModuleRevision.toString(directives, true);
return namespace + ModuleContainer.toString(attributes, false) + ModuleContainer.toString(directives, true);
}

private static final String PACKAGENAME_FILTER_COMPONENT = PackageNamespace.PACKAGE_NAMESPACE + "="; //$NON-NLS-1$
Expand Down
Loading

0 comments on commit f463b8e

Please sign in to comment.