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 4, 2024
1 parent d52b39e commit 3bd2a9d
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 107 deletions.
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.util.ResourceFormatter;
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 "
+ ResourceFormatter.printRequirement(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 "
+ ResourceFormatter.printRequirement(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);
}

}
2 changes: 1 addition & 1 deletion bundles/org.eclipse.osgi/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
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 @@ -16,6 +16,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.osgi.util.ResourceFormatter;
import org.osgi.framework.namespace.NativeNamespace;
import org.osgi.framework.wiring.BundleCapability;

Expand Down Expand Up @@ -88,6 +89,6 @@ public ModuleRevision getResource() {

@Override
public String toString() {
return namespace + ModuleRevision.toString(attributes, false) + ModuleRevision.toString(directives, true);
return namespace + ResourceFormatter.toString(attributes, false) + ResourceFormatter.toString(directives, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
import java.util.Map;
import org.eclipse.osgi.internal.container.Capabilities;
import org.eclipse.osgi.internal.framework.FilterImpl;
import org.eclipse.osgi.util.ResourceFormatter;
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 +90,7 @@ public ModuleRevision getResource() {

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

private static final String PACKAGENAME_FILTER_COMPONENT = PackageNamespace.PACKAGE_NAMESPACE + "="; //$NON-NLS-1$
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,11 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.osgi.internal.framework.FilterImpl;
import org.eclipse.osgi.internal.messages.Msg;
import org.eclipse.osgi.report.resolution.ResolutionReport;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.namespace.BundleNamespace;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.PackageNamespace;
import org.eclipse.osgi.util.ResourceFormatter;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.Capability;
import org.osgi.resource.Namespace;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
import org.osgi.resource.Wire;
Expand Down Expand Up @@ -132,7 +126,8 @@ private static String getResolutionReport0(String prepend, ModuleRevision revisi
private static void printResolutionEntry(StringBuilder result, String prepend, ResolutionReport.Entry entry, Map<Resource, List<ResolutionReport.Entry>> reportEntries, Set<BundleRevision> visited) {
switch (entry.getType()) {
case MISSING_CAPABILITY :
result.append(prepend).append(Msg.ModuleResolutionReport_UnresolvedReq).append(printRequirement(entry.getData())).append('\n');
result.append(prepend).append(Msg.ModuleResolutionReport_UnresolvedReq)
.append(ResourceFormatter.printRequirement((Requirement) entry.getData())).append('\n');
break;
case SINGLETON_SELECTION :
result.append(prepend).append(Msg.ModuleResolutionReport_AnotherSingleton).append(entry.getData()).append('\n');
Expand All @@ -147,8 +142,11 @@ private static void printResolutionEntry(StringBuilder result, String prepend, R
Capability unresolvedCapability = unresolvedCapabilities.iterator().next();
// make sure this is not a case of importing and exporting the same package
if (!unresolvedRequirement.getKey().getResource().equals(unresolvedCapability.getResource())) {
result.append(prepend).append(Msg.ModuleResolutionReport_UnresolvedReq).append(printRequirement(unresolvedRequirement.getKey())).append('\n');
result.append(prepend).append(" -> ").append(printCapability(unresolvedCapability)).append('\n'); //$NON-NLS-1$
result.append(prepend).append(Msg.ModuleResolutionReport_UnresolvedReq)
.append(ResourceFormatter.printRequirement(unresolvedRequirement.getKey()))
.append('\n');
result.append(prepend).append(" -> ") //$NON-NLS-1$
.append(ResourceFormatter.printCapability(unresolvedCapability)).append('\n');
result.append(getResolutionReport0(prepend + " ", (ModuleRevision) unresolvedCapability.getResource(), reportEntries, visited)); //$NON-NLS-1$
}
}
Expand All @@ -167,57 +165,6 @@ private static void printResolutionEntry(StringBuilder result, String prepend, R
}
}

private static Object printCapability(Capability cap) {
if (PackageNamespace.PACKAGE_NAMESPACE.equals(cap.getNamespace())) {
return Constants.EXPORT_PACKAGE + ": " + createOSGiCapability(cap); //$NON-NLS-1$
} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(cap.getNamespace())) {
return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(cap); //$NON-NLS-1$
} else if (HostNamespace.HOST_NAMESPACE.equals(cap.getNamespace())) {
return Constants.BUNDLE_SYMBOLICNAME + ": " + createOSGiCapability(cap); //$NON-NLS-1$
}
return Constants.PROVIDE_CAPABILITY + ": " + cap.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 + ModuleRevision.toString(attributes, false, true) + ModuleRevision.toString(directives, true, true);
}

private static String printRequirement(Object data) {
if (!(data instanceof Requirement)) {
return String.valueOf(data);
}
Requirement req = (Requirement) data;
if (PackageNamespace.PACKAGE_NAMESPACE.equals(req.getNamespace())) {
return Constants.IMPORT_PACKAGE + ": " + createOSGiRequirement(req, PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE, PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); //$NON-NLS-1$
} else if (BundleNamespace.BUNDLE_NAMESPACE.equals(req.getNamespace())) {
return Constants.REQUIRE_BUNDLE + ": " + createOSGiRequirement(req, BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); //$NON-NLS-1$
} else if (HostNamespace.HOST_NAMESPACE.equals(req.getNamespace())) {
return Constants.FRAGMENT_HOST + ": " + createOSGiRequirement(req, HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE); //$NON-NLS-1$
}
return Constants.REQUIRE_CAPABILITY + ": " + req.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 + ModuleRevision.toString(matchingAttributes, false, true) + ModuleRevision.toString(directives, true, true);
}

@Override
public String getResolutionReportMessage(Resource resource) {
return getResolutionReport0(null, (ModuleRevision) resource, getEntries(), null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import org.eclipse.osgi.container.ModuleRevisionBuilder.GenericInfo;
import org.eclipse.osgi.container.namespaces.EquinoxModuleDataNamespace;
Expand Down Expand Up @@ -202,45 +200,6 @@ public String toString() {
return identities.get(0).toString();
}

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<Entry<String, V>> set = map.entrySet();
StringBuilder sb = new StringBuilder();
for (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();
}

NamespaceList<ModuleCapability> getCapabilities() {
return capabilities;
}
Expand Down
2 changes: 1 addition & 1 deletion bundles/org.eclipse.osgi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</parent>
<groupId>org.eclipse.osgi</groupId>
<artifactId>org.eclipse.osgi</artifactId>
<version>3.18.700-SNAPSHOT</version>
<version>3.19.0-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
<properties>
<!-- The actual TCKs are executed in the org.eclipse.osgi.tck module because of reference to other service implementations -->
Expand Down
Loading

0 comments on commit 3bd2a9d

Please sign in to comment.