GP-1816 - OSGi Bundles - minor refactoring of OSGi bundle code during exploration.

This commit is contained in:
dragonmacher 2022-04-13 12:56:21 -04:00
parent d89764d9bb
commit 2d5f53e051
20 changed files with 629 additions and 583 deletions

View file

@ -18,16 +18,15 @@ package ghidra.app.plugin.core.osgi;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
/** /**
* An error produced during {@link GhidraBundle#build()} with a time stamp * An error produced during {@link GhidraBundle#build()} with a timestamp.
*/ */
public class BuildError { public class BuildError {
// the lastModified time of the source causing this error // the lastModified time of the source causing this error
private final long lastModified; private final long lastModified;
private final StringBuilder message = new StringBuilder(); private final StringBuilder message = new StringBuilder();
/** /**
* Construct an object to record error message produced for {@code sourceFile} * Construct an object to record error message produced for {@code sourceFile}.
* @param sourceFile the file causing this error * @param sourceFile the file causing this error
*/ */
public BuildError(ResourceFile sourceFile) { public BuildError(ResourceFile sourceFile) {
@ -35,15 +34,15 @@ public class BuildError {
} }
/** /**
* Append {@code str} to the current error message * Append the given string to the current error message.
* * @param s the string to append
* @param str the string to append
*/ */
public void append(String str) { public void append(String s) {
message.append(str); message.append(s);
} }
/** /**
* The error message.
* @return the error message * @return the error message
*/ */
String getMessage() { String getMessage() {
@ -51,6 +50,7 @@ public class BuildError {
} }
/** /**
* The last modified time of the source for this build error.
* @return the last modified time of the source for this build error * @return the last modified time of the source for this build error
*/ */
public long getLastModified() { public long getLastModified() {

View file

@ -15,7 +15,8 @@
*/ */
package ghidra.app.plugin.core.osgi; package ghidra.app.plugin.core.osgi;
import java.io.*; import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@ -46,19 +47,19 @@ import ghidra.util.task.TaskMonitor;
* Hosts the embedded OSGi framework and manages {@link GhidraBundle}s. * Hosts the embedded OSGi framework and manages {@link GhidraBundle}s.
* *
* <br/><br/> * <br/><br/>
* note: {@link GhidraBundle}, its implementations, and this class constitute * Note: {@link GhidraBundle}, its implementations, and this class constitute a bridge between
* a bridge between OSGi's {@link Bundle} and Ghidra. * OSGi's {@link Bundle} and Ghidra.
* <ul> * <ul>
* <li> unqualified, "bundle" will mean {@link GhidraBundle} * <li> unqualified, "bundle" will mean {@link GhidraBundle}
* <li> use of OSGi types, including {@link Bundle} and {@link Framework}, should be package * <li> use of OSGi types, including {@link Bundle} and {@link Framework}, should be package scoped
* scoped (not public) * (not public)
* <li> bundle lifecycle is simplified to "active"(same as OSGi "active" state) * <li> bundle lifecycle is simplified to "active"(same as OSGi "active" state) and "inactive"
* and "inactive" (OSGi "uninstalled" state) * (OSGi "uninstalled" state)
* </ul> * </ul>
*/ */
public class BundleHost { public class BundleHost {
public static final String ACTIVATING_BUNDLE_ERROR_MSG = "activating bundle"; public static final String ACTIVATING_BUNDLE_ERROR_MSG = "activating bundle";
protected static final boolean STDERR_DEBUGGING = false; private static final boolean STDERR_DEBUGGING = false;
private static final String SAVE_STATE_TAG_FILE = "BundleHost_FILE"; private static final String SAVE_STATE_TAG_FILE = "BundleHost_FILE";
private static final String SAVE_STATE_TAG_ENABLE = "BundleHost_ENABLE"; private static final String SAVE_STATE_TAG_ENABLE = "BundleHost_ENABLE";
private static final String SAVE_STATE_TAG_ACTIVE = "BundleHost_ACTIVE"; private static final String SAVE_STATE_TAG_ACTIVE = "BundleHost_ACTIVE";
@ -66,22 +67,10 @@ public class BundleHost {
private final BundleMap bundleMap = new BundleMap(); private final BundleMap bundleMap = new BundleMap();
BundleContext frameworkBundleContext; private BundleContext frameworkBundleContext;
Framework felixFramework; private Framework felixFramework;
List<BundleHostListener> listeners = new CopyOnWriteArrayList<>(); private List<BundleHostListener> listeners = new CopyOnWriteArrayList<>();
/** constructor */
public BundleHost() {
//
}
/**
* stop the framework.
*/
public void dispose() {
stopFramework();
}
/** /**
* If a {@link GhidraBundle} hasn't already been added for {@bundleFile}, add it now as a * If a {@link GhidraBundle} hasn't already been added for {@bundleFile}, add it now as a
@ -132,8 +121,8 @@ public class BundleHost {
} }
/** /**
* Assuming there is currently a bundle managed with file {@code bundleFile}, * Assuming there is currently a bundle managed with file {@code bundleFile}, return its
* return its {@link GhidraBundle}, otherwise show an error dialog and return {@code null}. * {@link GhidraBundle}, otherwise show an error dialog and return {@code null}.
* *
* @param bundleFile the bundleFile of the sought bundle * @param bundleFile the bundleFile of the sought bundle
* @return a {@link GhidraBundle} or {@code null} * @return a {@link GhidraBundle} or {@code null}
@ -149,8 +138,8 @@ public class BundleHost {
} }
/** /**
* If there is currently a bundle managed with file {@code bundleFile}, * If there is currently a bundle managed with file {@code bundleFile}, return its
* return its {@link GhidraBundle}, otherwise return {@code null}. * {@link GhidraBundle}, otherwise return {@code null}.
* *
* @param bundleFile the bundleFile of the sought bundle * @param bundleFile the bundleFile of the sought bundle
* @return a {@link GhidraBundle} or {@code null} * @return a {@link GhidraBundle} or {@code null}
@ -180,7 +169,7 @@ public class BundleHost {
} }
/** /**
* Create a new GhidraBundle and add to the list of managed bundles * Create a new GhidraBundle and add to the list of managed bundles.
* *
* @param bundleFile the bundle file * @param bundleFile the bundle file
* @param enabled if the new bundle should be enabled * @param enabled if the new bundle should be enabled
@ -282,7 +271,7 @@ public class BundleHost {
} }
} }
Bundle installFromLoc(String bundleLocation) throws GhidraBundleException { private Bundle installFromLoc(String bundleLocation) throws GhidraBundleException {
try { try {
return frameworkBundleContext.installBundle(bundleLocation); return frameworkBundleContext.installBundle(bundleLocation);
} }
@ -291,17 +280,8 @@ public class BundleHost {
} }
} }
Bundle installAsLoc(String bundleLocation, InputStream contents) throws GhidraBundleException {
try {
return frameworkBundleContext.installBundle(bundleLocation, contents);
}
catch (BundleException e) {
throw new GhidraBundleException(bundleLocation, "installing as bundle location", e);
}
}
/** /**
* return all of the currently managed bundles * Return all of the currently managed bundles.
* *
* @return all the bundles * @return all the bundles
*/ */
@ -310,7 +290,7 @@ public class BundleHost {
} }
/** /**
* return the list of currently managed bundle files * Return the list of currently managed bundle files.
* *
* @return all the bundle files * @return all the bundle files
*/ */
@ -318,14 +298,6 @@ public class BundleHost {
return bundleMap.getBundleFiles(); return bundleMap.getBundleFiles();
} }
void dumpLoadedBundles() {
System.err.printf("=== Bundles ===\n");
for (Bundle bundle : frameworkBundleContext.getBundles()) {
System.err.printf("%s: %s: %s: %s\n", bundle.getBundleId(), bundle.getSymbolicName(),
bundle.getState(), bundle.getVersion());
}
}
/** /**
* Attempt to resolve a list of BundleRequirements with active Bundle capabilities. * Attempt to resolve a list of BundleRequirements with active Bundle capabilities.
* *
@ -370,7 +342,7 @@ public class BundleHost {
return tmpRequirements.isEmpty(); return tmpRequirements.isEmpty();
} }
protected String buildExtraSystemPackages() { private String buildExtraSystemPackages() {
Set<String> packages = new HashSet<>(); Set<String> packages = new HashSet<>();
OSGiUtils.getPackagesFromClasspath(packages); OSGiUtils.getPackagesFromClasspath(packages);
return packages.stream().collect(Collectors.joining(",")); return packages.stream().collect(Collectors.joining(","));
@ -387,11 +359,11 @@ public class BundleHost {
} }
/** /**
* A directory for use by the OSGi framework as a cache * A directory for use by the OSGi framework as a cache.
* *
* @return the directory * @return the directory
*/ */
protected static Path getCacheDir() { private static Path getCacheDir() {
return BundleHost.getOsgiDir().resolve("felixcache"); return BundleHost.getOsgiDir().resolve("felixcache");
} }
@ -401,16 +373,19 @@ public class BundleHost {
return cacheDir.toAbsolutePath().toString(); return cacheDir.toAbsolutePath().toString();
} }
protected void createAndConfigureFramework() throws IOException { private void createAndConfigureFramework() throws IOException {
Properties config = new Properties(); Properties config = new Properties();
// allow multiple bundles w/ the same symbolic name -- location can distinguish // allow multiple bundles w/ the same symbolic name -- location can distinguish
config.setProperty(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MULTIPLE); config.setProperty(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MULTIPLE);
// use the default, inferred from environment
// config.setProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES,"osgi.ee; osgi.ee=\"JavaSE\";version:List=\"...\"");
// compute and add everything in the class path. extra packages have lower precedence than imports, // use the default, inferred from environment
// so an Import-Package / @importpackage will override the "living off the land" default // config.setProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES,
// "osgi.ee; osgi.ee=\"JavaSE\";version:List=\"...\"");
// compute and add everything in the class path. extra packages have lower precedence than
// imports, so an Import-Package / @importpackage will override the "living off the land"
// default
config.setProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, buildExtraSystemPackages()); config.setProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, buildExtraSystemPackages());
// only clean on first startup, o/w keep our storage around // only clean on first startup, o/w keep our storage around
@ -423,23 +398,26 @@ public class BundleHost {
config.put(FelixConstants.LOG_LEVEL_PROP, "1"); config.put(FelixConstants.LOG_LEVEL_PROP, "1");
if (STDERR_DEBUGGING) { if (STDERR_DEBUGGING) {
config.put(FelixConstants.LOG_LEVEL_PROP, "999"); config.put(FelixConstants.LOG_LEVEL_PROP, "999");
// config.put(FelixConstants.LOG_LOGGER_PROP, new org.apache.felix.framework.Logger() {...}); // config.put(FelixConstants.LOG_LOGGER_PROP,
// new org.apache.felix.framework.Logger() {...});
} }
FrameworkFactory factory = new FrameworkFactory(); FrameworkFactory factory = new FrameworkFactory();
felixFramework = factory.newFramework(config); felixFramework = factory.newFramework(config);
} }
protected void addDebuggingListeners() { private void addDebuggingListeners() {
frameworkBundleContext.addFrameworkListener(new FrameworkListener() { if (!STDERR_DEBUGGING) {
@Override return;
public void frameworkEvent(FrameworkEvent event) {
System.err.printf("%s %s\n", event.getBundle(), event);
} }
frameworkBundleContext.addFrameworkListener(
event -> {
String msg = String.format("AA: %s %s\n", event.getBundle(), event);
Msg.debug(this, msg);
}); });
frameworkBundleContext.addServiceListener(new ServiceListener() {
@Override frameworkBundleContext.addServiceListener(event -> {
public void serviceChanged(ServiceEvent event) {
String type = "?"; String type = "?";
if (event.getType() == ServiceEvent.REGISTERED) { if (event.getType() == ServiceEvent.REGISTERED) {
@ -449,15 +427,14 @@ public class BundleHost {
type = "unregistering"; type = "unregistering";
} }
System.err.printf("%s %s from %s\n", event.getSource(), type, String msg = String.format("BB: %s %s from %s\n", event.getSource(), type,
event.getServiceReference().getBundle().getLocation()); event.getServiceReference().getBundle().getLocation());
Msg.debug(this, msg);
}
}); });
} }
/** /**
* start the framework * Start the framework.
* *
* @throws OSGiException framework failures * @throws OSGiException framework failures
* @throws IOException filesystem setup * @throws IOException filesystem setup
@ -469,48 +446,53 @@ public class BundleHost {
felixFramework.init(); felixFramework.init();
} }
catch (BundleException e) { catch (BundleException e) {
throw new OSGiException("initializing felix OSGi framework", e); throw new OSGiException("Exception initializing felix OSGi framework", e);
} }
frameworkBundleContext = felixFramework.getBundleContext(); frameworkBundleContext = felixFramework.getBundleContext();
if (STDERR_DEBUGGING) {
addDebuggingListeners(); addDebuggingListeners();
}
frameworkBundleContext Bundle bundle = frameworkBundleContext.getBundle();
.addBundleListener(new MyBundleListener(frameworkBundleContext.getBundle())); frameworkBundleContext.addBundleListener(new MyBundleListener(bundle));
try { try {
felixFramework.start(); felixFramework.start();
} }
catch (BundleException e) { catch (BundleException e) {
throw new OSGiException("starting felix OSGi framework", e); throw new OSGiException("Exception starting felix OSGi framework", e);
} }
} }
/** /**
* stop the OSGi framework synchronously * Stop the OSGi framework.
*
* <p>This may wait for up to 5 seconds for the framework to fully stop. If that timeout
* passes an error will be logged.
*/ */
protected void stopFramework() { public void stopFramework() {
if (felixFramework != null) { if (felixFramework == null) {
return;
}
try { try {
felixFramework.stop(); felixFramework.stop();
// any bundles that linger after a few seconds might be the source
// of subtle problems, so wait for them to stop and report any problems. // any bundles that linger after a few seconds might be the source of subtle problems,
// so wait for them to stop and report any problems.
FrameworkEvent event = felixFramework.waitForStop(5000); FrameworkEvent event = felixFramework.waitForStop(5000);
if (event.getType() == FrameworkEvent.WAIT_TIMEDOUT) { if (event.getType() == FrameworkEvent.WAIT_TIMEDOUT) {
Msg.error(this, "Stopping OSGi framework timed out after 5 seconds."); Msg.error(this, "Stopping OSGi framework timed-out after 5 seconds.");
} }
felixFramework = null; felixFramework = null;
} }
catch (BundleException | InterruptedException e) { catch (BundleException | InterruptedException e) {
Msg.error(this, "Failed to stop OSGi framework."); Msg.error(this, "Failed to stop OSGi framework.", e);
e.printStackTrace();
}
} }
} }
/** /**
* Gets the host framework.
* @return the OSGi framework * @return the OSGi framework
*/ */
Framework getHostFramework() { Framework getHostFramework() {
@ -565,7 +547,8 @@ public class BundleHost {
} }
/** /**
* Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED" state. * Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED"
* state.
* *
* @param bundle the bundle * @param bundle the bundle
* @throws GhidraBundleException if there's a problem activating * @throws GhidraBundleException if there's a problem activating
@ -575,7 +558,7 @@ public class BundleHost {
return; return;
} }
FrameworkWiring frameworkWiring = felixFramework.adapt(FrameworkWiring.class); FrameworkWiring frameworkWiring = felixFramework.adapt(FrameworkWiring.class);
LinkedList<Bundle> dependentBundles = new LinkedList<Bundle>( LinkedList<Bundle> dependentBundles = new LinkedList<>(
frameworkWiring.getDependencyClosure(Collections.singleton(bundle))); frameworkWiring.getDependencyClosure(Collections.singleton(bundle)));
while (!dependentBundles.isEmpty()) { while (!dependentBundles.isEmpty()) {
Bundle dependentBundle = dependentBundles.pop(); Bundle dependentBundle = dependentBundles.pop();
@ -596,7 +579,8 @@ public class BundleHost {
} }
/** /**
* Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED" state. * Deactivate a bundle. Either an exception is thrown or the bundle will be in "UNINSTALLED"
* state.
* *
* @param bundleLocation the bundle location identifier * @param bundleLocation the bundle location identifier
* @throws InterruptedException if the wait is interrupted * @throws InterruptedException if the wait is interrupted
@ -611,8 +595,8 @@ public class BundleHost {
} }
/** /**
* Refreshes the specified bundles. This forces the update (replacement) * Refreshes the specified bundles. This forces the update (replacement) or removal of packages
* or removal of packages exported by the specified bundles. * exported by the specified bundles.
* *
* @param bundles the bundles to refresh * @param bundles the bundles to refresh
* @see FrameworkWiring#refreshBundles * @see FrameworkWiring#refreshBundles
@ -620,22 +604,19 @@ public class BundleHost {
protected void refreshBundlesSynchronously(Collection<Bundle> bundles) { protected void refreshBundlesSynchronously(Collection<Bundle> bundles) {
FrameworkWiring frameworkWiring = felixFramework.adapt(FrameworkWiring.class); FrameworkWiring frameworkWiring = felixFramework.adapt(FrameworkWiring.class);
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
frameworkWiring.refreshBundles(bundles, new FrameworkListener() { frameworkWiring.refreshBundles(bundles, event -> {
@Override
public void frameworkEvent(FrameworkEvent event) {
if (event.getType() == FrameworkEvent.ERROR) { if (event.getType() == FrameworkEvent.ERROR) {
Bundle bundle = event.getBundle(); Bundle bundle = event.getBundle();
Msg.error(BundleHost.this, Msg.error(BundleHost.this,
String.format("OSGi error refreshing bundle: %s", bundle)); String.format("OSGi error refreshing bundle: %s", bundle));
} }
latch.countDown(); latch.countDown();
}
}); });
try { try {
latch.await(); latch.await();
} }
catch (InterruptedException e) { catch (InterruptedException e) {
e.printStackTrace(); Msg.error(this, "Exception waiting for bundles to refresh", e);
} }
} }
@ -649,8 +630,11 @@ public class BundleHost {
} }
/** /**
* Activate a set of bundles and any dependencies in topological order. This method doesn't rely on the * Activate a set of bundles and any dependencies in topological order. This method doesn't
* framework, and so will add non-active dependencies. * rely on the framework, and so will add non-active dependencies.
*
* <p>To load bundles without loading inactive dependencies, call
* {@link #activateInStages(Collection, TaskMonitor, PrintWriter)}.
* *
* @param bundles bundles to activate * @param bundles bundles to activate
* @param monitor a task monitor * @param monitor a task monitor
@ -671,9 +655,13 @@ public class BundleHost {
activateSynchronously(bundle.getLocationIdentifier()); activateSynchronously(bundle.getLocationIdentifier());
} }
catch (GhidraBundleException e) { catch (GhidraBundleException e) {
// TODO should we report failing bundles to the console as well so they get logged
// in headless mode?
fireBundleException(e); fireBundleException(e);
} }
catch (Exception e) { catch (Exception e) {
// write the error to the console or log file
console.println("Unexpected error activating bundles: " + bundles);
e.printStackTrace(console); e.printStackTrace(console);
} }
monitor.incrementProgress(1); monitor.incrementProgress(1);
@ -682,7 +670,10 @@ public class BundleHost {
/** /**
* Activate a set of bundles in dependency topological order by resolving against currently * Activate a set of bundles in dependency topological order by resolving against currently
* active bundles in stages. No bundles outside those requested will be activated. * active bundles in stages. <b>No bundles outside those requested will be activated.</b>
*
* <p>To have inactive dependencies loaded, call
* {@link #activateAll(Collection, TaskMonitor, PrintWriter)}.
* *
* @param bundles bundles to activate * @param bundles bundles to activate
* @param monitor a task monitor * @param monitor a task monitor
@ -702,11 +693,13 @@ public class BundleHost {
requirementMap.put(bundle, requirements); requirementMap.put(bundle, requirements);
} }
catch (GhidraBundleException e) { catch (GhidraBundleException e) {
// TODO should we report failing bundles to the console as well so they get logged
// in headless mode?
fireBundleException(e); fireBundleException(e);
} }
} }
List<GhidraBundle> bundlesRemaining = new ArrayList<>(requirementMap.keySet());
List<GhidraBundle> bundlesRemaining = new ArrayList<>(requirementMap.keySet());
monitor.setMaximum(bundlesRemaining.size()); monitor.setMaximum(bundlesRemaining.size());
while (!bundlesRemaining.isEmpty() && !monitor.isCancelled()) { while (!bundlesRemaining.isEmpty() && !monitor.isCancelled()) {
List<GhidraBundle> resolvableBundles = bundlesRemaining.stream() List<GhidraBundle> resolvableBundles = bundlesRemaining.stream()
@ -731,9 +724,13 @@ public class BundleHost {
activateSynchronously(bundle.getLocationIdentifier()); activateSynchronously(bundle.getLocationIdentifier());
} }
catch (GhidraBundleException e) { catch (GhidraBundleException e) {
// TODO should we report failing bundles to the console as well so they get logged
// in headless mode?
fireBundleException(e); fireBundleException(e);
} }
catch (Exception e) { catch (Exception e) {
// write the error to the console or log file
console.println("Unexpected error activating bundles: " + bundles);
e.printStackTrace(console); e.printStackTrace(console);
} }
monitor.incrementProgress(1); monitor.incrementProgress(1);
@ -813,7 +810,7 @@ public class BundleHost {
* *
* <p>Bundles that had been active are reactivated. * <p>Bundles that had been active are reactivated.
* *
* <p>note: This is done once on startup after system bundles have been added. * <p>Note: This is done once on startup after system bundles have been added.
* *
* @param saveState the state object * @param saveState the state object
* @param tool the tool * @param tool the tool
@ -863,7 +860,7 @@ public class BundleHost {
} }
if (!bundlesToActivate.isEmpty()) { if (!bundlesToActivate.isEmpty()) {
TaskLauncher.launchNonModal("restoring bundle state", TaskLauncher.launchNonModal("Restoring bundle state",
(monitor) -> activateInStages(bundlesToActivate, monitor, new NullPrintWriter())); (monitor) -> activateInStages(bundlesToActivate, monitor, new NullPrintWriter()));
} }
} }
@ -896,35 +893,43 @@ public class BundleHost {
saveState.putBooleans(SAVE_STATE_TAG_SYSTEM, bundleIsSystem); saveState.putBooleans(SAVE_STATE_TAG_SYSTEM, bundleIsSystem);
} }
private static class Dependency { //=================================================================================================
// exists only to be distinguished by id // Inner Classes
//=================================================================================================
private static class BundleEdge {
// edge type for dependency graph
} }
/** /**
* Utility class to build a dependency graph from bundles where capabilities map to requirements. * Utility class to build a dependency graph from bundles where capabilities map to
* requirements.
*/ */
private class BundleDependencyGraph extends DirectedMultigraph<GhidraBundle, Dependency> { private class BundleDependencyGraph extends DirectedMultigraph<GhidraBundle, BundleEdge> {
final Map<GhidraBundle, List<BundleCapability>> capabilityMap = new HashMap<>();
final List<GhidraBundle> availableBundles;
final TaskMonitor monitor;
BundleDependencyGraph(Collection<GhidraBundle> startingBundles, TaskMonitor monitor) { private final List<GhidraBundle> availableBundles = new ArrayList<>();
private final Map<GhidraBundle, List<BundleCapability>> capabilityMap = new HashMap<>();
private final TaskMonitor monitor;
BundleDependencyGraph(Collection<GhidraBundle> activatingBundles, TaskMonitor monitor) {
super(null, null, false); super(null, null, false);
this.monitor = monitor; this.monitor = monitor;
// maintain a list of bundles available for resolution, starting with all of the enabled bundles // maintain a list of bundles available for resolution, starting with all of the
this.availableBundles = new ArrayList<>(); // enabled bundles
for (GhidraBundle bundle : getGhidraBundles()) { for (GhidraBundle bundle : getGhidraBundles()) {
if (bundle.isEnabled()) { if (bundle.isEnabled()) {
addToAvailable(bundle); addToAvailable(bundle);
} }
} }
// An edge A->B indicates that the capabilities of A resolve some requirement(s) of B // An edge A->B indicates that the capabilities of A resolve some requirement(s) of B
// "front" accumulates bundles and links to bundles already in the graph that they provide capabilities for. // "front" accumulates bundles and links to bundles already in the graph that they
// e.g. if front[A]=[B,...] then A->B, B is already in the graph, and we will add A next iteration. // provide capabilities for. e.g., if front[A]=[B,...] then A->B, B is already in the
// graph, and we will add A next iteration.
Map<GhidraBundle, Set<GhidraBundle>> front = new HashMap<>(); Map<GhidraBundle, Set<GhidraBundle>> front = new HashMap<>();
for (GhidraBundle bundle : startingBundles) { for (GhidraBundle bundle : activatingBundles) {
front.put(bundle, null); front.put(bundle, null);
} }
@ -939,46 +944,45 @@ public class BundleHost {
handleBackEdges(newFront); handleBackEdges(newFront);
front = newFront; front = newFront;
} }
} }
Iterable<GhidraBundle> inTopologicalOrder() { Iterable<GhidraBundle> inTopologicalOrder() {
return () -> new TopologicalOrderIterator<>(this); return () -> new TopologicalOrderIterator<>(this);
} }
void handleBackEdges(Map<GhidraBundle, Set<GhidraBundle>> newFront) { private void handleBackEdges(Map<GhidraBundle, Set<GhidraBundle>> newFront) {
Iterator<Entry<GhidraBundle, Set<GhidraBundle>>> newFrontIter = Iterator<Entry<GhidraBundle, Set<GhidraBundle>>> it =
newFront.entrySet().iterator(); newFront.entrySet().iterator();
while (newFrontIter.hasNext() && !monitor.isCancelled()) { while (it.hasNext() && !monitor.isCancelled()) {
Entry<GhidraBundle, Set<GhidraBundle>> entry = newFrontIter.next(); Entry<GhidraBundle, Set<GhidraBundle>> entry = it.next();
GhidraBundle source = entry.getKey(); GhidraBundle source = entry.getKey();
if (containsVertex(source)) { if (containsVertex(source)) {
for (GhidraBundle destination : entry.getValue()) { for (GhidraBundle destination : entry.getValue()) {
if (source != destination) { if (source != destination) {
addEdge(source, destination, new Dependency()); addEdge(source, destination, new BundleEdge());
} }
} }
newFrontIter.remove(); it.remove();
} }
} }
} }
void addFront(Map<GhidraBundle, Set<GhidraBundle>> front) { private void addFront(Map<GhidraBundle, Set<GhidraBundle>> front) {
for (Entry<GhidraBundle, Set<GhidraBundle>> e : front.entrySet()) { for (Entry<GhidraBundle, Set<GhidraBundle>> entry : front.entrySet()) {
GhidraBundle source = e.getKey(); GhidraBundle source = entry.getKey();
if (addToAvailable(source)) { if (addToAvailable(source)) {
addVertex(source); addVertex(source);
Set<GhidraBundle> destinations = e.getValue(); Set<GhidraBundle> dependents = entry.getValue();
if (destinations != null) { if (dependents != null) {
for (GhidraBundle destination : destinations) { for (GhidraBundle destination : dependents) {
addEdge(source, destination, new Dependency()); addEdge(source, destination, new BundleEdge());
} }
} }
} }
} }
} }
boolean addToAvailable(GhidraBundle bundle) { private boolean addToAvailable(GhidraBundle bundle) {
try { try {
capabilityMap.put(bundle, bundle.getAllCapabilities()); capabilityMap.put(bundle, bundle.getAllCapabilities());
availableBundles.add(bundle); availableBundles.add(bundle);
@ -990,33 +994,38 @@ public class BundleHost {
} }
} }
// populate newFront with edges depBundle -> bundle, // Populate newFront with edges supplierBundle -> dependentBundle, where supplierBundle has
// where depBundle has a capability that resolves a requirement of bundle // a capability that resolves a requirement of dependentBundle. Items added to newFront are
void resolve(GhidraBundle bundle, Map<GhidraBundle, Set<GhidraBundle>> newFront) { // already in the graph. These items will be added if the given bundle to resolve
// becomes a dependent on this added supplier.
private void resolve(GhidraBundle toResolve,
Map<GhidraBundle, Set<GhidraBundle>> newFront) {
List<BundleRequirement> requirements; List<BundleRequirement> requirements;
try { try {
requirements = new ArrayList<>(bundle.getAllRequirements()); List<BundleRequirement> bundleRequirements = toResolve.getAllRequirements();
if (requirements.isEmpty()) { if (bundleRequirements.isEmpty()) {
return; return; // no dependencies to resolve
} }
requirements = new ArrayList<>(bundleRequirements);
} }
catch (GhidraBundleException e) { catch (GhidraBundleException e) {
fireBundleException(e); fireBundleException(e);
removeVertex(bundle); removeVertex(toResolve);
return; return;
} }
for (GhidraBundle depBundle : availableBundles) { for (GhidraBundle supplierBundle : availableBundles) {
for (BundleCapability capability : capabilityMap.get(depBundle)) { for (BundleCapability capability : capabilityMap.get(supplierBundle)) {
if (monitor.isCancelled()) { if (monitor.isCancelled()) {
return; return;
} }
Iterator<BundleRequirement> reqIter = requirements.iterator(); Iterator<BundleRequirement> it = requirements.iterator();
while (reqIter.hasNext()) { while (it.hasNext()) {
BundleRequirement req = reqIter.next(); BundleRequirement req = it.next();
if (req.matches(capability)) { if (req.matches(capability)) {
newFront.computeIfAbsent(depBundle, b -> new HashSet<>()).add(bundle); newFront.computeIfAbsent(supplierBundle, b -> new HashSet<>())
reqIter.remove(); .add(toResolve);
it.remove();
} }
} }
if (requirements.isEmpty()) { if (requirements.isEmpty()) {
@ -1024,13 +1033,15 @@ public class BundleHost {
} }
} }
} }
// if requirements remain, some will be resolved by system
// and others will generate helpful errors for the user during activation // If requirements remain, some will be resolved by system and others will generate
// helpful errors for the user during activation
} }
} }
/** /**
* The {@code BundleListener} that notifies {@link BundleHostListener}s of bundle activation changes * The {@code BundleListener} that notifies {@link BundleHostListener}s of bundle activation
* changes.
*/ */
private class MyBundleListener implements BundleListener { private class MyBundleListener implements BundleListener {
private final Bundle systemBundle; private final Bundle systemBundle;
@ -1050,8 +1061,10 @@ public class BundleHost {
if (STDERR_DEBUGGING) { if (STDERR_DEBUGGING) {
String symbolicName = osgiBundle.getSymbolicName(); String symbolicName = osgiBundle.getSymbolicName();
String locationIdentifier = osgiBundle.getLocation(); String locationIdentifier = osgiBundle.getLocation();
System.err.printf("%s %s from %s\n", OSGiUtils.getEventTypeString(event), String message =
String.format("CC: %s %s from %s\n", OSGiUtils.getEventTypeString(event),
symbolicName, locationIdentifier); symbolicName, locationIdentifier);
Msg.debug(this, message);
} }
GhidraBundle bundle; GhidraBundle bundle;
switch (event.getType()) { switch (event.getType()) {

View file

@ -45,7 +45,7 @@ public class BundleMap {
bundlesByLocation.put(bundle.getLocationIdentifier(), bundle); bundlesByLocation.put(bundle.getLocationIdentifier(), bundle);
} }
finally { finally {
lock.writeLock().unlock(); writeLock.unlock();
} }
} }
@ -197,6 +197,7 @@ public class BundleMap {
} }
/** /**
* Returns the currently mapped bundles.
* @return the currently mapped bundles * @return the currently mapped bundles
*/ */
public Collection<GhidraBundle> getGhidraBundles() { public Collection<GhidraBundle> getGhidraBundles() {
@ -210,6 +211,7 @@ public class BundleMap {
} }
/** /**
* Returns the currently mapped bundle files.
* @return the currently mapped bundle files * @return the currently mapped bundle files
*/ */
public Collection<ResourceFile> getBundleFiles() { public Collection<ResourceFile> getBundleFiles() {

View file

@ -45,7 +45,7 @@ import resources.Icons;
import resources.ResourceManager; import resources.ResourceManager;
/** /**
* component for managing OSGi bundle status * Component for managing OSGi bundle status
*/ */
public class BundleStatusComponentProvider extends ComponentProviderAdapter { public class BundleStatusComponentProvider extends ComponentProviderAdapter {
@ -289,7 +289,7 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter {
files.stream().map(ResourceFile::new).collect(Collectors.toUnmodifiableList()); files.stream().map(ResourceFile::new).collect(Collectors.toUnmodifiableList());
Collection<GhidraBundle> bundles = bundleHost.add(resourceFiles, true, false); Collection<GhidraBundle> bundles = bundleHost.add(resourceFiles, true, false);
TaskLauncher.launchNonModal("activating new bundles", (monitor) -> { TaskLauncher.launchNonModal("Activating new bundles", (monitor) -> {
bundleHost.activateAll(bundles, monitor, bundleHost.activateAll(bundles, monitor,
getTool().getService(ConsoleService.class).getStdErr()); getTool().getService(ConsoleService.class).getStdErr());
}); });
@ -339,9 +339,6 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter {
return panel; return panel;
} }
/**
* cleanup this component
*/
public void dispose() { public void dispose() {
filterPanel.dispose(); filterPanel.dispose();
} }
@ -351,9 +348,10 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter {
} }
/** /**
* This is for testing only! during normal execution, statuses are only added through BundleHostListener bundle(s) added events. * This is for testing only! during normal execution, statuses are only added through
* BundleHostListener bundle(s) added events.
* *
* <p>each new bundle will be enabled and writable * <p>Each new bundle will be enabled and writable
* *
* @param bundleFiles the files to use * @param bundleFiles the files to use
*/ */
@ -363,6 +361,10 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter {
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
//=================================================================================================
// Inner Classes
//=================================================================================================
private final class RemoveBundlesTask extends Task { private final class RemoveBundlesTask extends Task {
private final DeactivateAndDisableBundlesTask deactivateBundlesTask; private final DeactivateAndDisableBundlesTask deactivateBundlesTask;
private final List<BundleStatus> statuses; private final List<BundleStatus> statuses;
@ -378,7 +380,7 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter {
public void run(TaskMonitor monitor) throws CancelledException { public void run(TaskMonitor monitor) throws CancelledException {
deactivateBundlesTask.run(monitor); deactivateBundlesTask.run(monitor);
monitor.checkCanceled(); monitor.checkCanceled();
// partition bundles into system (bundles.get(true)) and non-system (bundles.get(false)). // partition bundles into system (bundles.get(true)) / non-system (bundles.get(false))
Map<Boolean, List<GhidraBundle>> bundles = statuses.stream() Map<Boolean, List<GhidraBundle>> bundles = statuses.stream()
.map(bs -> bundleHost.getExistingGhidraBundle(bs.getFile())) .map(bs -> bundleHost.getExistingGhidraBundle(bs.getFile()))
.collect(Collectors.partitioningBy(GhidraBundle::isSystemBundle)); .collect(Collectors.partitioningBy(GhidraBundle::isSystemBundle));
@ -483,8 +485,8 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter {
} }
/* /*
* Activating/deactivating a single bundle doesn't require resolving dependents, * Activating/deactivating a single bundle doesn't require resolving dependents, so this task
* so this task is slightly different from the others. * is slightly different from the others.
*/ */
private class ActivateDeactivateBundleTask extends Task { private class ActivateDeactivateBundleTask extends Task {
private final BundleStatus status; private final BundleStatus status;

View file

@ -22,7 +22,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.*; import javax.swing.JLabel;
import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelEvent;
import org.osgi.framework.Bundle; import org.osgi.framework.Bundle;
@ -32,8 +32,10 @@ import generic.jar.ResourceFile;
import generic.util.Path; import generic.util.Path;
import ghidra.docking.settings.Settings; import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.*; import ghidra.util.Swing;
import ghidra.util.table.column.*; import ghidra.util.SystemUtilities;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
/** /**
* Model for {@link BundleStatus} objects. * Model for {@link BundleStatus} objects.
@ -73,10 +75,10 @@ public class BundleStatusTableModel
} }
private BundleStatus getStatus(GhidraBundle bundle) { private BundleStatus getStatus(GhidraBundle bundle) {
return getStatusFromLoc(bundle.getLocationIdentifier()); return getStatusFromLocation(bundle.getLocationIdentifier());
} }
private BundleStatus getStatusFromLoc(String bundleLoc) { private BundleStatus getStatusFromLocation(String bundleLoc) {
return bundleLocToStatusMap.get(bundleLoc); return bundleLocToStatusMap.get(bundleLoc);
} }
@ -254,7 +256,7 @@ public class BundleStatusTableModel
// wrap the assigned comparator to detect if the order changes // wrap the assigned comparator to detect if the order changes
AtomicBoolean changed = new AtomicBoolean(false); AtomicBoolean changed = new AtomicBoolean(false);
Comparator<BundleStatus> wrapper = new Comparator<BundleStatus>() { Comparator<BundleStatus> wrapper = new Comparator<>() {
Comparator<BundleStatus> comparator = sortingContext.getComparator(); Comparator<BundleStatus> comparator = sortingContext.getComparator();
@Override @Override
@ -367,7 +369,7 @@ public class BundleStatusTableModel
@Override @Override
public void bundleException(GhidraBundleException exception) { public void bundleException(GhidraBundleException exception) {
Swing.runLater(() -> { Swing.runLater(() -> {
BundleStatus status = getStatusFromLoc(exception.getBundleLocation()); BundleStatus status = getStatusFromLocation(exception.getBundleLocation());
if (status != null) { if (status != null) {
status.setSummary(exception.getMessage()); status.setSummary(exception.getMessage());
int rowIndex = getRowIndex(status); int rowIndex = getRowIndex(status);

View file

@ -30,8 +30,20 @@ import generic.jar.ResourceFile;
*/ */
public abstract class GhidraBundle { public abstract class GhidraBundle {
protected final ResourceFile file; /**
* A {@link GhidraBundle} can be
* <ul>
* <li>a Bndtools .bnd script</li>
* <li>an OSGi bundle .jar file</li>
* <li>a directory of Java source</li>
* </ul>
*
*/
enum Type {
BND_SCRIPT, JAR, SOURCE_DIR, INVALID
}
protected final ResourceFile bundleFile; // can be a dir or a jar file
protected final BundleHost bundleHost; protected final BundleHost bundleHost;
protected boolean enabled; protected boolean enabled;
protected boolean systemBundle; protected boolean systemBundle;
@ -39,29 +51,29 @@ public abstract class GhidraBundle {
GhidraBundle(BundleHost bundleHost, ResourceFile bundleFile, boolean enabled, GhidraBundle(BundleHost bundleHost, ResourceFile bundleFile, boolean enabled,
boolean systemBundle) { boolean systemBundle) {
this.bundleHost = bundleHost; this.bundleHost = bundleHost;
this.file = bundleFile; this.bundleFile = bundleFile;
this.enabled = enabled; this.enabled = enabled;
this.systemBundle = systemBundle; this.systemBundle = systemBundle;
} }
/** /**
* clean build artifacts generated during build of this bundle * Clean build artifacts generated during build of this bundle.
* *
* @return true if anything was done * @return true if anything was done
*/ */
abstract boolean clean(); abstract boolean clean();
/** /**
* build OSGi bundle if possible * Build OSGi bundle if needed and if possible.
* *
* @param writer console for build messages to user * @param writer console for build messages to user
* @return true if build happened, false if already built * @return true if build happened, false if already built or could not build
* @throws Exception if the build cannot complete * @throws Exception if the build cannot complete
*/ */
public abstract boolean build(PrintWriter writer) throws Exception; public abstract boolean build(PrintWriter writer) throws Exception;
/** /**
* same as {@link #build(PrintWriter)} with writer = {@link System#err}. * Same as {@link #build(PrintWriter)} with writer = {@link System#err}.
* *
* @return true if build happened, false if already built * @return true if build happened, false if already built
* @throws Exception if the build cannot complete * @throws Exception if the build cannot complete
@ -74,28 +86,43 @@ public abstract class GhidraBundle {
* Return the location identifier of the bundle that this GhidraBundle represents. * Return the location identifier of the bundle that this GhidraBundle represents.
* *
* <p>The location identifier is used by the framework, e.g. it is passed to * <p>The location identifier is used by the framework, e.g. it is passed to
* {@link org.osgi.framework.BundleContext#installBundle} when the bundle is * {@link org.osgi.framework.BundleContext#installBundle} when the bundle is first installed.
* first installed.
* *
* <p>Although the bundle location is a URI, outside of interactions with the framework, * <p>Although the bundle location is a URI, outside of interactions with the framework, the
* the bundle location should remain opaque. * bundle location should remain opaque.
* *
* @return location identifier of this bundle * @return location identifier of this bundle
*/ */
public abstract String getLocationIdentifier(); public abstract String getLocationIdentifier();
/**
* Returns all bundle requirements.
*
* @return the requirements
* @throws GhidraBundleException if there is an exception parsing / loading bundle requirements
*/
public abstract List<BundleRequirement> getAllRequirements() throws GhidraBundleException; public abstract List<BundleRequirement> getAllRequirements() throws GhidraBundleException;
/**
* Returns all bundle capabilities.
*
* @return the capabilities
* @throws GhidraBundleException if there is an exception parsing / loading bundle capabilities
*/
public abstract List<BundleCapability> getAllCapabilities() throws GhidraBundleException; public abstract List<BundleCapability> getAllCapabilities() throws GhidraBundleException;
/** /**
* @return the file where this bundle is loaded from * The file where this bundle is loaded from.
*
* @return the file from where this bundle is loaded
*/ */
public ResourceFile getFile() { public ResourceFile getFile() {
return file; return bundleFile;
} }
/** /**
* True if this bundle is enabled.
*
* @return true if this bundle is enabled * @return true if this bundle is enabled
*/ */
public boolean isEnabled() { public boolean isEnabled() {
@ -103,7 +130,7 @@ public abstract class GhidraBundle {
} }
/** /**
* set the enablement flag for this bundle. * Set the enablement flag for this bundle.
* *
* <p>If a bundle is enabled its contents will be scanned, e.g. for scripts. * <p>If a bundle is enabled its contents will be scanned, e.g. for scripts.
* *
@ -123,7 +150,7 @@ public abstract class GhidraBundle {
} }
/** /**
* Get the type of a GhidraBundle from its file. * Get the type of {@link GhidraBundle} from its file.
* *
* @param file a bundle file * @param file a bundle file
* @return the type * @return the type
@ -163,8 +190,8 @@ public abstract class GhidraBundle {
} }
/** /**
* Get the OSGi bundle represented by this GhidraBundle or null if it isn't in * Get the OSGi bundle represented by this GhidraBundle or null if it isn't in the "installed"
* the "installed" state. * state.
* *
* @return a Bundle or null * @return a Bundle or null
*/ */
@ -173,6 +200,8 @@ public abstract class GhidraBundle {
} }
/** /**
* True if this bundle is active.
*
* @return true if this bundle is active * @return true if this bundle is active
*/ */
public boolean isActive() { public boolean isActive() {
@ -180,17 +209,8 @@ public abstract class GhidraBundle {
return (bundle != null) && bundle.getState() == Bundle.ACTIVE; return (bundle != null) && bundle.getState() == Bundle.ACTIVE;
} }
/** @Override
* A GhidraBundle can be public String toString() {
* <ul> return getOSGiBundle().getSymbolicName();
* <li>a Bndtools .bnd script</li>
* <li>an OSGi bundle .jar file</li>
* <li>a directory of Java source</li>
* </ul>
*
*/
enum Type {
BND_SCRIPT, JAR, SOURCE_DIR, INVALID
} }
} }

View file

@ -67,7 +67,7 @@ public class GhidraJarBundle extends GhidraBundle {
} }
protected ManifestParser createManifestParser() throws GhidraBundleException { protected ManifestParser createManifestParser() throws GhidraBundleException {
try (Jar jar = new Jar(file.getFile(true))) { try (Jar jar = new Jar(bundleFile.getFile(true))) {
Manifest manifest = jar.getManifest(); Manifest manifest = jar.getManifest();
if (manifest == null) { if (manifest == null) {
throw new GhidraBundleException(bundleLocation, "jar bundle with no manifest"); throw new GhidraBundleException(bundleLocation, "jar bundle with no manifest");

View file

@ -35,8 +35,7 @@ import javax.tools.*;
import javax.tools.JavaFileObject.Kind; import javax.tools.JavaFileObject.Kind;
import org.apache.felix.framework.util.manifestparser.ManifestParser; import org.apache.felix.framework.util.manifestparser.ManifestParser;
import org.osgi.framework.Bundle; import org.osgi.framework.*;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants; import org.osgi.framework.Constants;
import org.osgi.framework.wiring.*; import org.osgi.framework.wiring.*;
import org.phidias.compile.BundleJavaManager; import org.phidias.compile.BundleJavaManager;
@ -47,17 +46,16 @@ import generic.io.NullPrintWriter;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.app.script.*; import ghidra.app.script.*;
import ghidra.util.Msg; import ghidra.util.Msg;
import util.CollectionUtils;
import utilities.util.FileUtilities; import utilities.util.FileUtilities;
/** /**
* {@link GhidraSourceBundle} represents a Java source directory that is compiled on build to an OSGi bundle. * Represents a Java source directory that is compiled on build to an OSGi bundle.
* *
* <p>A manifest and {@link BundleActivator} are generated if not already present. * <p>A manifest and {@link BundleActivator} are generated if not already present.
*/ */
/**
*
*/
public class GhidraSourceBundle extends GhidraBundle { public class GhidraSourceBundle extends GhidraBundle {
private static final String INSTRCTION_ACTIVATOR = "org.osgi.framework.BundleActivator";
private static final String GENERATED_ACTIVATOR_CLASSNAME = "GeneratedActivator"; private static final String GENERATED_ACTIVATOR_CLASSNAME = "GeneratedActivator";
private static final String GENERATED_VERSION = "1.0"; private static final String GENERATED_VERSION = "1.0";
@ -70,43 +68,53 @@ public class GhidraSourceBundle extends GhidraBundle {
private static final Predicate<String> IS_CLASS_FILE = private static final Predicate<String> IS_CLASS_FILE =
Pattern.compile("(\\$.*)?\\.class", Pattern.CASE_INSENSITIVE).asMatchPredicate(); Pattern.compile("(\\$.*)?\\.class", Pattern.CASE_INSENSITIVE).asMatchPredicate();
protected interface DiscrepencyCallback {
/** /**
* Invoked when there is a discrepancy between {@code sourceFile} and its * Used to report source and class file deviation
* corresponding class file(s), {@code classFiles} */
protected interface DiscrepancyCallback {
/**
* Invoked when there is a discrepancy between {@code sourceFile} and its corresponding
* class file(s), {@code classFiles}
* *
* @param sourceFile the source file or null to indicate the class files have no corresponding source * @param sourceFile the source file or null to indicate the class files have no
* corresponding source
* @param classFiles corresponding class file(s) * @param classFiles corresponding class file(s)
* @throws Throwable an exception * @throws Throwable an exception
*/ */
void found(ResourceFile sourceFile, Collection<Path> classFiles) throws Throwable; void found(ResourceFile sourceFile, Collection<Path> classFiles) throws Throwable;
} }
// This is a hash; it is used to create a unique directory name for this bundle
private final String symbolicName; private final String symbolicName;
private final Path binaryDir; private final Path binaryDir;
private final String bundleLoc;
/**
* The bundle location id
* @see #getLocationIdentifier()
*/
private final String bundleLocationId;
// These 2 lists are updated to track source file to class file changes; newSources are those
// that need to be compiled; oldBinaries are those that no longer have source files (such as
// when a source file is deleted)
private final List<ResourceFile> newSources = new ArrayList<>(); private final List<ResourceFile> newSources = new ArrayList<>();
private final List<Path> oldBinaries = new ArrayList<>(); private final List<Path> oldBinaries = new ArrayList<>();
private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//// information indexed by source file // information indexed by source file
private final Map<ResourceFile, BuildError> buildErrors = new HashMap<>(); private final Map<ResourceFile, BuildError> buildErrors = new HashMap<>();
private final Map<ResourceFile, List<BundleRequirement>> sourceFileToRequirements = private final Map<ResourceFile, List<BundleRequirement>> sourceFileToRequirements =
new HashMap<>(); new HashMap<>();
private final Map<String, List<ResourceFile>> requirementToSourceFileMap = new HashMap<>(); private final Map<String, List<ResourceFile>> requirementToSourceFileMap = new HashMap<>();
private final Set<String> missedRequirements = new HashSet<>();
private final Set<String> importPackageValues = new HashSet<>(); private final Set<String> importPackageValues = new HashSet<>();
private Set<String> missedRequirements = new HashSet<>();
private long lastCompileAttempt;
/** /**
* Create a new GhidraSourceBundle. * Create a new GhidraSourceBundle.
* *
* @param bundleHost the {@link BundleHost} instance this bundle will belong to * @param bundleHost the instance this bundle will belong to
* @param sourceDirectory the source bundle directory * @param sourceDirectory the source bundle directory
* @param enabled true to start enabled * @param enabled true to start enabled
* @param systemBundle true if this is a Ghidra system bundle * @param systemBundle true if this is a Ghidra system bundle
@ -115,18 +123,18 @@ public class GhidraSourceBundle extends GhidraBundle {
boolean systemBundle) { boolean systemBundle) {
super(bundleHost, sourceDirectory, enabled, systemBundle); super(bundleHost, sourceDirectory, enabled, systemBundle);
this.symbolicName = GhidraSourceBundle.sourceDirHash(getSourceDirectory()); this.symbolicName = sourceDirHash(getSourceDirectory());
this.binaryDir = GhidraSourceBundle.getCompiledBundlesDir().resolve(symbolicName); this.binaryDir = getCompiledBundlesDir().resolve(symbolicName);
this.bundleLocationId =
this.bundleLoc = "reference:file://" + binaryDir.toAbsolutePath().normalize().toString(); "reference:file://" + binaryDir.toAbsolutePath().normalize().toString();
} }
/** /**
* (alias of {@link #getFile} for readability) * (alias of {@link #getFile} for readability)
* @return the source directory this bundle represents * @return the source directory this bundle represents
*/ */
protected final ResourceFile getSourceDirectory() { private final ResourceFile getSourceDirectory() {
return file; return bundleFile;
} }
/** /**
@ -142,8 +150,8 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
/** /**
* When a source bundle doesn't have a manifest, Ghidra computes the bundle's * When a source bundle doesn't have a manifest, Ghidra computes the bundle's symbolic name as
* symbolic name as a hash of the source directory path. * a hash of the source directory path.
* *
* <p>This hash is also used as the final path component of the compile destination: * <p>This hash is also used as the final path component of the compile destination:
* <br/>&nbsp;{@code $USERHOME/.ghidra/.ghidra_<ghidra version>/osgi/compiled-bundles/<sourceDirHash> } * <br/>&nbsp;{@code $USERHOME/.ghidra/.ghidra_<ghidra version>/osgi/compiled-bundles/<sourceDirHash> }
@ -157,18 +165,6 @@ public class GhidraSourceBundle extends GhidraBundle {
return Integer.toHexString(sourceDir.getAbsolutePath().hashCode()); return Integer.toHexString(sourceDir.getAbsolutePath().hashCode());
} }
/**
* for testing only!!!
*
* @param sourceFile a ghidra script file
* @return the directory its class is compiled to
*/
public static Path getBindirFromScriptFile(ResourceFile sourceFile) {
ResourceFile tmpSourceDir = sourceFile.getParentFile();
String tmpSymbolicName = GhidraSourceBundle.sourceDirHash(tmpSourceDir);
return GhidraSourceBundle.getCompiledBundlesDir().resolve(tmpSymbolicName);
}
/** /**
* Return the class name corresponding to a script in this source bundle. * Return the class name corresponding to a script in this source bundle.
* *
@ -188,16 +184,16 @@ public class GhidraSourceBundle extends GhidraBundle {
return relativePath.replace(File.separatorChar, '.'); return relativePath.replace(File.separatorChar, '.');
} }
void clearBuildErrors(ResourceFile sourceFile) { private void clearBuildErrors(ResourceFile sourceFile) {
buildErrors.remove(sourceFile); buildErrors.remove(sourceFile);
} }
/** /**
* append build error * Append the given build error.
* @param sourceFile the file w/ errors * @param sourceFile the file w/ errors
* @param err an error string * @param err an error string
*/ */
protected void buildError(ResourceFile sourceFile, String err) { private void buildError(ResourceFile sourceFile, String err) {
BuildError error = buildErrors.computeIfAbsent(sourceFile, BuildError::new); BuildError error = buildErrors.computeIfAbsent(sourceFile, BuildError::new);
error.append(err); error.append(err);
} }
@ -206,7 +202,7 @@ public class GhidraSourceBundle extends GhidraBundle {
* Get any errors associated with building the given source file. * Get any errors associated with building the given source file.
* *
* @param sourceFile the source file * @param sourceFile the source file
* @return a {@link BuildError} object * @return the build error or null if no errors
*/ */
public BuildError getErrors(ResourceFile sourceFile) { public BuildError getErrors(ResourceFile sourceFile) {
return buildErrors.get(sourceFile); return buildErrors.get(sourceFile);
@ -233,9 +229,10 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
/** /**
* update buildReqs based on \@importpackage tag in java files in the default(unnamed) package * Update build requirements based on {@code @importpackage} tag in java files in the
* default (unnamed) package.
* *
* @throws GhidraBundleException on failure to parse the \@importpackage tag * @throws GhidraBundleException on failure to parse the {@code @importpackage} tag
*/ */
private void updateRequirementsFromMetadata() throws GhidraBundleException { private void updateRequirementsFromMetadata() throws GhidraBundleException {
sourceFileToRequirements.clear(); sourceFileToRequirements.clear();
@ -243,17 +240,23 @@ public class GhidraSourceBundle extends GhidraBundle {
importPackageValues.clear(); importPackageValues.clear();
for (ResourceFile rootSourceFile : getSourceDirectory().listFiles()) { for (ResourceFile rootSourceFile : getSourceDirectory().listFiles()) {
if (rootSourceFile.getName().endsWith(".java")) { if (!rootSourceFile.getName().endsWith(".java")) {
// without GhidraScriptComponentProvider.updateAvailableScriptFilesForDirectory, continue;
// or GhidraScriptComponentProvider.newScript this might be the earliest need for }
// Without GhidraScriptComponentProvider.updateAvailableScriptFilesForDirectory, or
// GhidraScriptComponentProvider.newScript this might be the earliest need for
// ScriptInfo, so allow construction. // ScriptInfo, so allow construction.
// NB: ScriptInfo will update field values if lastModified has changed since last time they were computed // NB: ScriptInfo will update field values if lastModified has changed since last time
// they were computed
String importPackage = parseImportPackageMetadata(rootSourceFile); String importPackage = parseImportPackageMetadata(rootSourceFile);
if (importPackage != null && !importPackage.isEmpty()) { if (importPackage == null || importPackage.isEmpty()) {
importPackageValues.addAll( continue;
ManifestParser.parseDelimitedString(importPackage.strip(), ",")); }
List<String> parts = ManifestParser.parseDelimitedString(importPackage.strip(), ",");
importPackageValues.addAll(parts);
List<BundleRequirement> requirements; List<BundleRequirement> requirements;
try { try {
requirements = OSGiUtils.parseImportPackage(importPackage); requirements = OSGiUtils.parseImportPackage(importPackage);
@ -270,11 +273,9 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
} }
} }
}
}
/** /**
* assumes that {@link #updateRequirementsFromMetadata()} has been called recently * Assumes that {@link #updateRequirementsFromMetadata()} has been called recently
* *
* @return deduped requirements * @return deduped requirements
*/ */
@ -288,11 +289,14 @@ public class GhidraSourceBundle extends GhidraBundle {
return dedupedReqs; return dedupedReqs;
} }
protected ManifestParser createSourceManifestParser() { private ManifestParser createSourceManifestParser() {
ResourceFile manifestFile = getSourceManifestFile(); ResourceFile manifestFile = getSourceManifestFile();
if (manifestFile.exists()) { if (!manifestFile.exists()) {
try (InputStream manifestInputStream = manifestFile.getInputStream()) { return null;
Manifest manifest = new Manifest(manifestInputStream); }
try (InputStream is = manifestFile.getInputStream()) {
Manifest manifest = new Manifest(is);
Attributes mainAttributes = manifest.getMainAttributes(); Attributes mainAttributes = manifest.getMainAttributes();
Map<String, Object> headerMap = mainAttributes.entrySet() Map<String, Object> headerMap = mainAttributes.entrySet()
.stream() .stream()
@ -304,14 +308,12 @@ public class GhidraSourceBundle extends GhidraBundle {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
return null;
}
@Override @Override
public List<BundleRequirement> getAllRequirements() throws GhidraBundleException { public List<BundleRequirement> getAllRequirements() throws GhidraBundleException {
ManifestParser manifestParser = createSourceManifestParser(); ManifestParser parser = createSourceManifestParser();
if (manifestParser != null) { if (parser != null) {
return manifestParser.getRequirements(); return parser.getRequirements();
} }
updateRequirementsFromMetadata(); updateRequirementsFromMetadata();
@ -319,17 +321,17 @@ public class GhidraSourceBundle extends GhidraBundle {
return new ArrayList<>(reqs.values()); return new ArrayList<>(reqs.values());
} }
protected static void findPackageDirs(List<String> packages, ResourceFile directory) { private static void findPackageDirs(List<String> packages, ResourceFile dir) {
for (ResourceFile file : directory
.listFiles(f -> f.isDirectory() || f.getName().endsWith(".java"))) {
if (!file.getName().matches("internal|private")) {
boolean added = false; boolean added = false;
ResourceFile[] files = dir.listFiles(f -> f.isDirectory() || f.getName().endsWith(".java"));
for (ResourceFile file : files) {
if (!file.getName().matches("internal|private")) {
if (file.isDirectory()) { if (file.isDirectory()) {
findPackageDirs(packages, file); findPackageDirs(packages, file);
} }
else if (!added) { else if (!added) {
added = true; added = true;
packages.add(directory.getAbsolutePath()); packages.add(dir.getAbsolutePath());
} }
} }
} }
@ -363,21 +365,23 @@ public class GhidraSourceBundle extends GhidraBundle {
return OSGiUtils.parseExportPackage(sb.substring(1)); return OSGiUtils.parseExportPackage(sb.substring(1));
} }
catch (BundleException e) { catch (BundleException e) {
throw new GhidraBundleException(getLocationIdentifier(), "exports error", e); throw new GhidraBundleException(getLocationIdentifier(), "Exports error", e);
} }
} }
/** /**
* look for new sources, metadata, manifest file. * Look for new sources, metadata, manifest file. This will find files that need to be
* compiled and files that need to be removed.
* *
* <p>if files had errors last time, haven't changed, and no new requirements are available, remove them. * <p>If files had errors last time, haven't changed, and no new requirements are available,
* remove them.
* *
* @param writer for reporting status to user * @param writer for reporting status to user
* @throws IOException while accessing manifest file * @throws IOException while accessing manifest file
* @throws OSGiException while parsing imports * @throws OSGiException while parsing imports
*/ */
void updateFromFilesystem(PrintWriter writer) throws IOException, OSGiException { private void updateFromFilesystem(PrintWriter writer) throws IOException, OSGiException {
// look for new source files
newSources.clear(); newSources.clear();
oldBinaries.clear(); oldBinaries.clear();
@ -395,13 +399,13 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
}); });
// we don't want to rebuild source files that had errors last time and haven't changed, // we don't want to rebuild source files that had errors last time and haven't changed, so
// so remove them from newSources. Also remove old error messages. // remove them from newSources. Also remove old error messages.
Iterator<ResourceFile> newSourceIterator = newSources.iterator(); Iterator<ResourceFile> it = newSources.iterator();
while (newSourceIterator.hasNext()) { while (it.hasNext()) {
ResourceFile newSourceFile = newSourceIterator.next(); ResourceFile newSourceFile = it.next();
if (stillHasErrors(newSourceFile)) { if (stillHasErrors(newSourceFile)) {
newSourceIterator.remove(); it.remove();
} }
else { else {
// any errors are old, so remove them // any errors are old, so remove them
@ -421,46 +425,40 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
private void deleteOldBinaries() throws IOException { private void deleteOldBinaries() throws IOException {
// dedupe and omit files that don't exist // dedup and omit files that don't exist
oldBinaries.sort(null); oldBinaries.sort(null);
Iterable<Path> paths =
() -> oldBinaries.stream().distinct().filter(Files::exists).iterator();
for (Path path : paths) { Iterator<Path> toDelete =
oldBinaries.stream().distinct().filter(Files::exists).sorted().iterator();
for (Path path : CollectionUtils.asIterable(toDelete)) {
Files.delete(path); Files.delete(path);
} }
oldBinaries.clear();
} }
int getBuildErrorCount() { private int getBuildErrorCount() {
return buildErrors.size(); return buildErrors.size();
} }
int getNewSourcesCount() { private int getNewSourcesCount() {
return newSources.size(); return newSources.size();
} }
/** /**
* used just after {@link #build} to get the newly compiled source files * Used just after {@link #build} to get the newly compiled source files.
* @return new source files * @return new source files
*/ */
public List<ResourceFile> getNewSources() { public List<ResourceFile> getNewSources() {
return newSources; return Collections.unmodifiableList(newSources);
}
void compileAttempted() {
lastCompileAttempt = System.currentTimeMillis();
}
long getLastCompileAttempt() {
return lastCompileAttempt;
} }
@Override @Override
public String getLocationIdentifier() { public String getLocationIdentifier() {
return bundleLoc; return bundleLocationId;
} }
ResourceFile getSourceManifestFile() { private ResourceFile getSourceManifestFile() {
return new ResourceFile(getSourceDirectory(), "META-INF" + File.separator + "MANIFEST.MF"); return new ResourceFile(getSourceDirectory(), "META-INF" + File.separator + "MANIFEST.MF");
} }
@ -468,19 +466,20 @@ public class GhidraSourceBundle extends GhidraBundle {
return binaryDir.resolve("META-INF").resolve("MANIFEST.MF"); return binaryDir.resolve("META-INF").resolve("MANIFEST.MF");
} }
boolean hasSourceManifest() { private boolean hasSourceManifest() {
return getSourceManifestFile().exists(); return getSourceManifestFile().exists();
} }
boolean hasNewManifest() { private boolean hasNewManifest() {
ResourceFile sourceManifest = getSourceManifestFile(); ResourceFile sourceManifest = getSourceManifestFile();
Path binaryManifest = getBinaryManifestPath(); Path binaryManifest = getBinaryManifestPath();
return sourceManifest.exists() && (Files.notExists(binaryManifest) || boolean oldOrMissingBinaryManifest = Files.notExists(binaryManifest) ||
sourceManifest.lastModified() > binaryManifest.toFile().lastModified()); sourceManifest.lastModified() > binaryManifest.toFile().lastModified();
return sourceManifest.exists() && oldOrMissingBinaryManifest;
} }
protected static boolean wipeContents(Path path) throws IOException { private static boolean wipeContents(Path path) throws IOException {
if (Files.exists(path)) { if (Files.exists(path)) {
boolean anythingDeleted = false; boolean anythingDeleted = false;
try (Stream<Path> walk = Files.walk(path)) { try (Stream<Path> walk = Files.walk(path)) {
@ -498,12 +497,12 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
/** /**
* if source with a previous requirement error now resolves, add it to newSources. * If source with a previous requirement error now resolves, add it to newSources.
* *
* <p>The reason for the previous build error isn't necessarily a missing requirement, * <p>The reason for the previous build error isn't necessarily a missing requirement, but this
* but this shouldn't be too expensive. * shouldn't be too expensive.
*/ */
private void addSourcesIfResolutionWillPass() { private void addSourcesThatNoLongerHaveMissingRequirements() {
for (ResourceFile sourceFile : buildErrors.keySet()) { for (ResourceFile sourceFile : buildErrors.keySet()) {
List<BundleRequirement> requirements = sourceFileToRequirements.get(sourceFile); List<BundleRequirement> requirements = sourceFileToRequirements.get(sourceFile);
if (requirements != null && !requirements.isEmpty() && if (requirements != null && !requirements.isEmpty() &&
@ -519,10 +518,10 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
/** /**
* if a file that previously built without errors is now missing some requirements, * If a file that previously built without errors is now missing some requirements, rebuild it
* rebuild it to capture errors (if any). * to capture errors (if any).
*/ */
void addSourcesIfResolutionWillFail() { private void addSourcesThatNowHaveMissingRequirements() {
// if previous successes no longer resolve, (cleanup) and try again // if previous successes no longer resolve, (cleanup) and try again
for (Entry<ResourceFile, List<BundleRequirement>> e : sourceFileToRequirements.entrySet()) { for (Entry<ResourceFile, List<BundleRequirement>> e : sourceFileToRequirements.entrySet()) {
ResourceFile sourceFile = e.getKey(); ResourceFile sourceFile = e.getKey();
@ -542,39 +541,42 @@ public class GhidraSourceBundle extends GhidraBundle {
@Override @Override
public boolean build(PrintWriter writer) throws Exception { public boolean build(PrintWriter writer) throws Exception {
if (writer == null) {
writer = new NullPrintWriter();
}
writer = NullPrintWriter.dummyIfNull(writer);
boolean needsCompile = false; boolean needsCompile = false;
if (hasSourceManifest()) { if (hasSourceManifest()) {
sourceFileToRequirements.clear(); sourceFileToRequirements.clear();
requirementToSourceFileMap.clear(); requirementToSourceFileMap.clear();
ArrayList<BundleRequirement> reqs = new ArrayList<>(getAllRequirements()); List<BundleRequirement> reqs = new ArrayList<>(getAllRequirements());
bundleHost.resolve(reqs); bundleHost.resolve(reqs);
HashSet<String> newMissedRequirements = new HashSet<>(); Set<String> newMissedRequirements = new HashSet<>();
for (BundleRequirement req : reqs) { for (BundleRequirement req : reqs) {
newMissedRequirements.add(req.toString()); newMissedRequirements.add(req.toString());
} }
if (hasNewManifest() || !newMissedRequirements.equals(missedRequirements)) { if (hasNewManifest() || !newMissedRequirements.equals(missedRequirements)) {
missedRequirements = newMissedRequirements; missedRequirements.clear();
missedRequirements.addAll(newMissedRequirements);
wipeBinDir(); wipeBinDir();
buildErrors.clear(); buildErrors.clear();
} }
updateFromFilesystem(writer); updateFromFilesystem(writer);
} }
else { else {
updateFromFilesystem(writer);
// this gets all source files from the file system that we know need to be compiled
// based upon missing or outdated class files
updateFromFilesystem(writer);
updateRequirementsFromMetadata(); updateRequirementsFromMetadata();
addSourcesIfResolutionWillPass();
addSourcesIfResolutionWillFail(); // these 2 calls handle source files that need to be compiled based upon changes in
// added or removed bundles / requirements
addSourcesThatNoLongerHaveMissingRequirements();
addSourcesThatNowHaveMissingRequirements();
} }
int buildErrorsLastTime = getBuildErrorCount(); int buildErrorsLastTime = getBuildErrorCount();
int newSourceCount = getNewSourcesCount(); int newSourceCount = getNewSourcesCount();
if (newSourceCount == 0) { if (newSourceCount == 0) {
if (buildErrorsLastTime > 0) { if (buildErrorsLastTime > 0) {
writer.printf("%s hasn't changed, with %d file%s failing in previous build(s):\n", writer.printf("%s hasn't changed, with %d file%s failing in previous build(s):\n",
@ -600,7 +602,7 @@ public class GhidraSourceBundle extends GhidraBundle {
bundleHost.deactivateSynchronously(osgiBundle); bundleHost.deactivateSynchronously(osgiBundle);
} }
// once we've committed to recompile and regenerate generated classes, delete the old stuff // once we've committed to recompile and regenerate classes, delete the old stuff
deleteOldBinaries(); deleteOldBinaries();
String summary = compileToExplodedBundle(writer); String summary = compileToExplodedBundle(writer);
@ -635,16 +637,19 @@ public class GhidraSourceBundle extends GhidraBundle {
private ResourceFile[] correspondingBinaries(ResourceFile source) { private ResourceFile[] correspondingBinaries(ResourceFile source) {
String parentPath = source.getParentFile().getAbsolutePath(); String parentPath = source.getParentFile().getAbsolutePath();
String relPath = parentPath.substring(getSourceDirectory().getAbsolutePath().length()); int sourceDirLength = getSourceDirectory().getAbsolutePath().length();
if (relPath.startsWith(File.separator)) { String relativePath = parentPath.substring(sourceDirLength);
relPath = relPath.substring(1); if (relativePath.startsWith(File.separator)) {
relativePath = relativePath.substring(1);
} }
String className0 = source.getName();
String className = className0.substring(0, className0.length() - 5);// drop ".java" String javaFileName = source.getName();
ResourceFile binarySubdir = new ResourceFile(binaryDir.resolve(relPath).toFile()); String className = javaFileName.substring(0, javaFileName.length() - 5); // drop ".java"
ResourceFile binarySubdir = new ResourceFile(binaryDir.resolve(relativePath).toFile());
if (!binarySubdir.exists() || !binarySubdir.isDirectory()) { if (!binarySubdir.exists() || !binarySubdir.isDirectory()) {
return new ResourceFile[] {}; return new ResourceFile[] {};
} }
return binarySubdir.listFiles(f -> { return binarySubdir.listFiles(f -> {
String fileName = f.getName(); String fileName = f.getName();
return fileName.startsWith(className) && return fileName.startsWith(className) &&
@ -674,23 +679,23 @@ public class GhidraSourceBundle extends GhidraBundle {
* binary_root/com/blah/Blah$Inner$Innerer.class * binary_root/com/blah/Blah$Inner$Innerer.class
* ... * ...
* </pre> * </pre>
* @param callback callback * @param discrepancy the discrepancy callback
*/ */
protected void visitDiscrepancies(DiscrepencyCallback callback) { protected void visitDiscrepancies(DiscrepancyCallback discrepancy) {
try { try {
Deque<ResourceFile> stack = new ArrayDeque<>();
// start in the source directory root
stack.add(getSourceDirectory());
Deque<ResourceFile> stack = new ArrayDeque<>();
ResourceFile sourceDir = getSourceDirectory();
stack.add(sourceDir); // start in the source directory root
while (!stack.isEmpty()) { while (!stack.isEmpty()) {
ResourceFile sourceSubdir = stack.pop(); ResourceFile sourceSubdir = stack.pop();
String relPath = sourceSubdir.getAbsolutePath() String relPath = sourceSubdir.getAbsolutePath()
.substring(getSourceDirectory().getAbsolutePath().length()); .substring(sourceDir.getAbsolutePath().length());
if (relPath.startsWith(File.separator)) { if (relPath.startsWith(File.separator)) {
relPath = relPath.substring(1); relPath = relPath.substring(1);
} }
Path binarySubdir = binaryDir.resolve(relPath);
Path binarySubdir = binaryDir.resolve(relPath);
ClassMapper mapper = new ClassMapper(binarySubdir); ClassMapper mapper = new ClassMapper(binarySubdir);
// for each source file, lookup class files by class name // for each source file, lookup class files by class name
@ -701,13 +706,13 @@ public class GhidraSourceBundle extends GhidraBundle {
else { else {
List<Path> classFiles = mapper.findAndRemove(sourceFile); List<Path> classFiles = mapper.findAndRemove(sourceFile);
if (classFiles != null) { if (classFiles != null) {
callback.found(sourceFile, classFiles); discrepancy.found(sourceFile, classFiles);
} }
} }
} }
// any remaining .class files are missing .java files // any remaining .class files are missing .java files
if (mapper.hasExtraClassFiles()) { if (mapper.hasExtraClassFiles()) {
callback.found(null, mapper.extraClassFiles()); discrepancy.found(null, mapper.extraClassFiles());
} }
} }
} }
@ -716,12 +721,15 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
} }
/* requirements that resolve internally are never "missing", but will only resolve _after_ build/install */ /*
* Requirements that resolve internally are never "missing", but resolve _after_ build/install
*/
private boolean resolveInternally(List<BundleRequirement> requirements) private boolean resolveInternally(List<BundleRequirement> requirements)
throws GhidraBundleException { throws GhidraBundleException {
if (requirements.isEmpty()) { if (requirements.isEmpty()) {
return true; return true;
} }
List<BundleCapability> capabilities = getAllCapabilities(); List<BundleCapability> capabilities = getAllCapabilities();
Iterator<BundleRequirement> requirementIterator = requirements.iterator(); Iterator<BundleRequirement> requirementIterator = requirements.iterator();
boolean anyMissing = false; boolean anyMissing = false;
@ -738,28 +746,38 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
/* /*
* when calling the java compiler programmatically, we map import requests to files with * When calling the java compiler programmatically, we map import requests to files with a
* a custom {@link JavaFileManager}. We wrap the system JavaFileManager with one that * custom {@link JavaFileManager}. We wrap the system JavaFileManager with one that handles
* handles ResourceFiles then wrap that with Phidia, which handles imports based on * ResourceFiles then wrap that with phidias, which handles imports based on bundle
* bundle requirements. * requirements.
*/ */
private BundleJavaManager createBundleJavaManager(PrintWriter writer, Summary summary, private BundleJavaManager createBundleJavaManager(PrintWriter writer, Summary summary,
List<String> options) throws IOException, GhidraBundleException { List<String> options) throws IOException, GhidraBundleException {
final ResourceFileJavaFileManager resourceFileJavaManager = new ResourceFileJavaFileManager(
Collections.singletonList(getSourceDirectory()), buildErrors.keySet());
ResourceFileJavaFileManager resourceFileJavaManager = new ResourceFileJavaFileManager(
Collections.singletonList(getSourceDirectory()), buildErrors.keySet());
BundleJavaManager bundleJavaManager = new MyBundleJavaManager(bundleHost.getHostFramework(), BundleJavaManager bundleJavaManager = new MyBundleJavaManager(bundleHost.getHostFramework(),
resourceFileJavaManager, options); resourceFileJavaManager, options);
// The phidias BundleJavaManager is for compiling from within a bundle -- it makes the // The phidias BundleJavaManager is for compiling from within a bundle -- it makes the
// bundle dependencies available to the compiler classpath. Here, we are compiling in an as-yet // bundle dependencies available to the compiler classpath. Here, we are compiling in an
// non-existing bundle, so we forge the wiring based on @importpackage metadata. // as-yet non-existing bundle, so we forge the wiring based on @importpackage metadata.
// get wires for currently active bundles to satisfy all requirements // get wires for currently active bundles to satisfy all requirements
List<BundleRequirement> requirements = getAllRequirements(); List<BundleRequirement> requirements = getAllRequirements();
List<BundleWiring> bundleWirings = bundleHost.resolve(requirements); List<BundleWiring> bundleWirings = bundleHost.resolve(requirements);
if (!resolveInternally(requirements)) { if (!resolveInternally(requirements)) {
writeErrorUnresolved(writer, summary, requirements);
}
// send the capabilities to phidias
bundleWirings.forEach(bundleJavaManager::addBundleWiring);
return bundleJavaManager;
}
private void writeErrorUnresolved(PrintWriter writer, Summary summary,
List<BundleRequirement> requirements) {
writer.printf("%d import requirement%s remain%s unresolved:\n", requirements.size(), writer.printf("%d import requirement%s remain%s unresolved:\n", requirements.size(),
requirements.size() > 1 ? "s" : "", requirements.size() > 1 ? "" : "s"); requirements.size() > 1 ? "s" : "", requirements.size() > 1 ? "" : "s");
for (BundleRequirement requirement : requirements) { for (BundleRequirement requirement : requirements) {
@ -770,6 +788,7 @@ public class GhidraSourceBundle extends GhidraBundle {
requiringFiles.stream() requiringFiles.stream()
.map(generic.util.Path::toPathString) .map(generic.util.Path::toPathString)
.collect(Collectors.joining(","))); .collect(Collectors.joining(",")));
for (ResourceFile sourceFile : requiringFiles) { for (ResourceFile sourceFile : requiringFiles) {
buildError(sourceFile, buildError(sourceFile,
generic.util.Path.toPathString(sourceFile) + " : failed import " + generic.util.Path.toPathString(sourceFile) + " : failed import " +
@ -782,29 +801,24 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
} }
summary.printf("%d missing package import%s:%s", requirements.size(), String singularity = requirements.size() > 1 ? "s" : "";
requirements.size() > 1 ? "s" : "", String missing = requirements.stream()
requirements.stream()
.flatMap( .flatMap(
r -> OSGiUtils.extractPackageNamesFromFailedResolution(r.toString()) r -> OSGiUtils.extractPackageNamesFromFailedResolution(r.toString()).stream())
.stream())
.distinct() .distinct()
.collect(Collectors.joining(","))); .collect(Collectors.joining(","));
} summary.printf("%d missing package import%s:%s", requirements.size(), singularity, missing);
// send the capabilities to phidias
bundleWirings.forEach(bundleJavaManager::addBundleWiring);
return bundleJavaManager;
} }
/* /*
* Try building sourcefiles.. on success return true. * Try building source files. On success return true.
* *
* If build fails, collect errors, remove files that caused * If build fails, collect errors, remove files that caused
* errors from source files, and return false. * errors from source files, and return false.
*/ */
private boolean tryBuild(PrintWriter writer, BundleJavaManager bundleJavaManager, private boolean tryBuild(PrintWriter writer, BundleJavaManager bundleJavaManager,
List<ResourceFileJavaFileObject> sourceFiles, List<String> options) throws IOException { List<ResourceFileJavaFileObject> sourceFiles, List<String> options) throws IOException {
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>(); DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
JavaCompiler.CompilationTask task = JavaCompiler.CompilationTask task =
compiler.getTask(writer, bundleJavaManager, diagnostics, options, null, sourceFiles); compiler.getTask(writer, bundleJavaManager, diagnostics, options, null, sourceFiles);
@ -812,6 +826,7 @@ public class GhidraSourceBundle extends GhidraBundle {
if (successfulCompilation) { if (successfulCompilation) {
return true; return true;
} }
Set<ResourceFileJavaFileObject> filesWithErrors = new HashSet<>(); Set<ResourceFileJavaFileObject> filesWithErrors = new HashSet<>();
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) { for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
String error = diagnostic.toString() + "\n"; String error = diagnostic.toString() + "\n";
@ -836,9 +851,9 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
/** /**
* generate a manifest (and an activator) * Generate a manifest (and an activator)
* *
* assumes that {@link #updateRequirementsFromMetadata()} has been called recently * <p>Assumes that {@link #updateRequirementsFromMetadata()} has been called recently
*/ */
private String generateManifest(PrintWriter writer, Summary summary, Path binaryManifest) private String generateManifest(PrintWriter writer, Summary summary, Path binaryManifest)
throws OSGiException, IOException { throws OSGiException, IOException {
@ -849,14 +864,14 @@ public class GhidraSourceBundle extends GhidraBundle {
GhidraSourceBundle.sourceDirHash(getSourceDirectory())); GhidraSourceBundle.sourceDirHash(getSourceDirectory()));
analyzer.setProperty("Bundle-Version", GENERATED_VERSION); analyzer.setProperty("Bundle-Version", GENERATED_VERSION);
if (!importPackageValues.isEmpty()) { if (importPackageValues.isEmpty()) {
analyzer.setProperty("Import-Package", "*");
}
else {
// constrain analyzed imports according to what's declared in @importpackage tags // constrain analyzed imports according to what's declared in @importpackage tags
analyzer.setProperty("Import-Package", analyzer.setProperty("Import-Package",
importPackageValues.stream().collect(Collectors.joining(",")) + ",*"); importPackageValues.stream().collect(Collectors.joining(",")) + ",*");
} }
else {
analyzer.setProperty("Import-Package", "*");
}
analyzer.setProperty("Export-Package", "!*.private.*,!*.internal.*,*"); analyzer.setProperty("Export-Package", "!*.private.*,!*.internal.*,*");
@ -866,44 +881,13 @@ public class GhidraSourceBundle extends GhidraBundle {
manifest = analyzer.calcManifest(); manifest = analyzer.calcManifest();
} }
catch (Exception e) { catch (Exception e) {
summary.print("bad manifest"); summary.print("Bad manifest");
throw new OSGiException("failed to calculate manifest by analyzing code", e); throw new OSGiException("Failed to calculate manifest by analyzing code", e);
} }
String activatorClassName = null; if (!addActivatorClass(writer, analyzer, manifest, summary)) {
try {
for (Clazz clazz : analyzer.getClassspace().values()) {
if (clazz.is(QUERY.IMPLEMENTS,
new Instruction("org.osgi.framework.BundleActivator"), analyzer)) {
System.err.printf("found BundleActivator class %s\n", clazz);
activatorClassName = clazz.toString();
}
}
}
catch (Exception e) {
summary.print("failed bnd analysis");
throw new OSGiException("failed to query classes while searching for activator", e);
}
Attributes manifestAttributes = manifest.getMainAttributes();
if (activatorClassName == null) {
activatorClassName = GENERATED_ACTIVATOR_CLASSNAME;
if (!buildDefaultActivator(binaryDir, activatorClassName, writer)) {
summary.print("failed to build generated activator");
return summary.getValue(); return summary.getValue();
} }
// since we add the activator after bndtools built the imports, we should add its imports too
String imps = manifestAttributes.getValue(Constants.IMPORT_PACKAGE);
if (imps == null) {
manifestAttributes.putValue(Constants.IMPORT_PACKAGE,
GhidraBundleActivator.class.getPackageName());
}
else {
manifestAttributes.putValue(Constants.IMPORT_PACKAGE,
imps + "," + GhidraBundleActivator.class.getPackageName());
}
}
manifestAttributes.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName);
// write the manifest // write the manifest
Files.createDirectories(binaryManifest.getParent()); Files.createDirectories(binaryManifest.getParent());
@ -917,6 +901,50 @@ public class GhidraSourceBundle extends GhidraBundle {
return summary.getValue(); return summary.getValue();
} }
private boolean addActivatorClass(PrintWriter writer, Analyzer analyzer, Manifest manifest,
Summary summary) throws OSGiException, IOException {
String activatorClassName = null;
try {
for (Clazz clazz : analyzer.getClassspace().values()) {
if (clazz.is(QUERY.IMPLEMENTS,
new Instruction(INSTRCTION_ACTIVATOR), analyzer)) {
Msg.trace(this, "Found BundleActivator class " + clazz);
activatorClassName = clazz.toString();
}
}
}
catch (Exception e) {
summary.print("Failed bnd analysis");
throw new OSGiException("Failed to query classes while searching for activator", e);
}
Attributes manifestAttributes = manifest.getMainAttributes();
if (activatorClassName != null) {
manifestAttributes.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName);
return true;
}
activatorClassName = GENERATED_ACTIVATOR_CLASSNAME;
if (!buildDefaultActivator(binaryDir, activatorClassName, writer)) {
summary.print("Failed to build generated activator");
return false;
}
// since we add the activator after bndtools built the imports, add its imports too
String imports = manifestAttributes.getValue(Constants.IMPORT_PACKAGE);
String activatorPackageName = GhidraBundleActivator.class.getPackageName();
if (imports == null) {
manifestAttributes.putValue(Constants.IMPORT_PACKAGE, activatorPackageName);
}
else {
manifestAttributes.putValue(Constants.IMPORT_PACKAGE, imports + "," +
activatorPackageName);
}
manifestAttributes.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName);
return true;
}
/** /**
* create and compile a default bundle activator * create and compile a default bundle activator
* *
@ -928,8 +956,8 @@ public class GhidraSourceBundle extends GhidraBundle {
*/ */
private boolean buildDefaultActivator(Path bindir, String activatorClassName, Writer writer) private boolean buildDefaultActivator(Path bindir, String activatorClassName, Writer writer)
throws IOException { throws IOException {
Path activatorSourceFileName = bindir.resolve(activatorClassName + ".java");
Path activatorSourceFileName = bindir.resolve(activatorClassName + ".java");
try (PrintWriter activatorWriter = new PrintWriter( try (PrintWriter activatorWriter = new PrintWriter(
Files.newBufferedWriter(activatorSourceFileName, Charset.forName("UTF-8")))) { Files.newBufferedWriter(activatorSourceFileName, Charset.forName("UTF-8")))) {
activatorWriter.println("import " + GhidraBundleActivator.class.getName() + ";"); activatorWriter.println("import " + GhidraBundleActivator.class.getName() + ";");
@ -959,15 +987,18 @@ public class GhidraSourceBundle extends GhidraBundle {
try (StandardJavaFileManager javaFileManager = try (StandardJavaFileManager javaFileManager =
compiler.getStandardFileManager(null, null, null); compiler.getStandardFileManager(null, null, null);
BundleJavaManager bundleJavaManager = new MyBundleJavaManager( BundleJavaManager bundleManager = new MyBundleJavaManager(
bundleHost.getHostFramework(), javaFileManager, options);) { bundleHost.getHostFramework(), javaFileManager, options);) {
Iterable<? extends JavaFileObject> sourceFiles = Iterable<? extends JavaFileObject> sourceFiles =
javaFileManager.getJavaFileObjectsFromPaths(List.of(activatorSourceFileName)); javaFileManager.getJavaFileObjectsFromPaths(List.of(activatorSourceFileName));
DiagnosticCollector<JavaFileObject> diagnostics = DiagnosticCollector<JavaFileObject> diagnostics =
new DiagnosticCollector<JavaFileObject>(); new DiagnosticCollector<>();
JavaCompiler.CompilationTask task = compiler.getTask(writer, bundleJavaManager, JavaCompiler.CompilationTask task = compiler.getTask(writer, bundleManager,
diagnostics, options, null, sourceFiles); diagnostics, options, null, sourceFiles);
if (!task.call()) { if (task.call()) {
return true;
}
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics
.getDiagnostics()) { .getDiagnostics()) {
writer.write(diagnostic.getSource().toString() + ": " + writer.write(diagnostic.getSource().toString() + ": " +
@ -975,19 +1006,17 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
return false; return false;
} }
return true;
}
} }
/** /**
* compile a source directory to an exploded bundle * Compile a source directory to an exploded bundle.
* *
* @param writer for updating the user during compilation * @param writer for updating the user during compilation
* @return a summary of the work performed
* @throws IOException for source/manifest file reading/generation and binary deletion/creation * @throws IOException for source/manifest file reading/generation and binary deletion/creation
* @throws OSGiException if generation of bundle metadata fails * @throws OSGiException if generation of bundle metadata fails
*/ */
private String compileToExplodedBundle(PrintWriter writer) throws IOException, OSGiException { private String compileToExplodedBundle(PrintWriter writer) throws IOException, OSGiException {
compileAttempted();
Files.createDirectories(binaryDir); Files.createDirectories(binaryDir);
@ -1047,8 +1076,12 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
} }
//=================================================================================================
// Inner Classes
//=================================================================================================
private static class MyBundleJavaManager extends BundleJavaManager { private static class MyBundleJavaManager extends BundleJavaManager {
static URL[] EMPTY_URL_ARRAY = new URL[0]; private static URL[] EMPTY_URL_ARRAY = new URL[0];
MyBundleJavaManager(Bundle bundle, JavaFileManager javaFileManager, List<String> options) MyBundleJavaManager(Bundle bundle, JavaFileManager javaFileManager, List<String> options)
throws IOException { throws IOException {
@ -1056,20 +1089,19 @@ public class GhidraSourceBundle extends GhidraBundle {
} }
/** /**
* since the JavaCompiler tasks can close the class loader returned by this * Since the JavaCompiler tasks can close the class loader returned by this method, make
* method, make sure we're returning a copy. * sure we're returning a copy.
*/ */
@Override @Override
public ClassLoader getClassLoader() { public ClassLoader getClassLoader() {
return new URLClassLoader(EMPTY_URL_ARRAY, super.getClassLoader()); return new URLClassLoader(EMPTY_URL_ARRAY, super.getClassLoader());
} }
} }
private static class Summary { private static class Summary {
static String SEPERATOR = ", "; private static final String SEPERATOR = ", ";
final StringWriter stringWriter = new StringWriter(); private final StringWriter stringWriter = new StringWriter();
final PrintWriter printWriter = new PrintWriter(stringWriter, true); private final PrintWriter printWriter = new PrintWriter(stringWriter, true);
void printf(String format, Object... args) { void printf(String format, Object... args) {
if (stringWriter.getBuffer().length() > 0) { if (stringWriter.getBuffer().length() > 0) {
@ -1096,12 +1128,12 @@ public class GhidraSourceBundle extends GhidraBundle {
* "B" -> [directory/B.class, directory/B$inner.class] * "B" -> [directory/B.class, directory/B$inner.class]
* </pre> * </pre>
* *
* <p>A list of classes are then processed one at a time with {@link ClassMapper#findAndRemove}. * <p>A list of classes are then processed with {@link ClassMapper#findAndRemove}.
* *
* <p>After processing, "extras" are handled with {@link ClassMapper#extraClassFiles}. * <p>After processing, "extras" are handled with {@link ClassMapper#extraClassFiles}.
*/ */
private static class ClassMapper { private static class ClassMapper {
final Map<String, List<Path>> classToClassFilesMap; private final Map<String, List<Path>> classToClassFilesMap;
/** /**
* Map classes in {@code directory} with {@link ClassMapper}. * Map classes in {@code directory} with {@link ClassMapper}.
@ -1129,8 +1161,7 @@ public class GhidraSourceBundle extends GhidraBundle {
if (money >= 0) { if (money >= 0) {
return fileName.substring(0, money); return fileName.substring(0, money);
} }
// drop ".class" return fileName.substring(0, fileName.length() - 6); // drop ".class"
return fileName.substring(0, fileName.length() - 6);
} }
List<Path> findAndRemove(ResourceFile sourceFile) { List<Path> findAndRemove(ResourceFile sourceFile) {
@ -1139,7 +1170,7 @@ public class GhidraSourceBundle extends GhidraBundle {
return null; return null;
} }
className = className.substring(0, className.length() - 5); className = className.substring(0, className.length() - 5); // drop ".java"
long lastModifiedSource = sourceFile.lastModified(); long lastModifiedSource = sourceFile.lastModified();
List<Path> classFiles = classToClassFilesMap.remove(className); List<Path> classFiles = classToClassFilesMap.remove(className);
if (classFiles == null) { if (classFiles == null) {
@ -1158,17 +1189,15 @@ public class GhidraSourceBundle extends GhidraBundle {
return null; return null;
} }
public boolean hasExtraClassFiles() { boolean hasExtraClassFiles() {
return !classToClassFilesMap.isEmpty(); return !classToClassFilesMap.isEmpty();
} }
public Collection<Path> extraClassFiles() { Collection<Path> extraClassFiles() {
return classToClassFilesMap.values() return classToClassFilesMap.values()
.stream() .stream()
.flatMap(l -> l.stream()) .flatMap(l -> l.stream())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }
} }

View file

@ -281,5 +281,4 @@ public class OSGiUtils {
Msg.error(OSGiUtils.class, "Error while collecting packages from jar", e); Msg.error(OSGiUtils.class, "Error while collecting packages from jar", e);
} }
} }
} }

View file

@ -1166,7 +1166,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
} }
} }
class RefreshingBundleHostListener implements BundleHostListener { private class RefreshingBundleHostListener implements BundleHostListener {
@Override @Override
public void bundleBuilt(GhidraBundle bundle, String summary) { public void bundleBuilt(GhidraBundle bundle, String summary) {
@ -1179,8 +1179,8 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter {
GhidraSourceBundle sourceBundle = (GhidraSourceBundle) bundle; GhidraSourceBundle sourceBundle = (GhidraSourceBundle) bundle;
ResourceFile sourceDirectory = sourceBundle.getFile(); ResourceFile sourceDirectory = sourceBundle.getFile();
if (summary == null) { if (summary == null) {
// a null summary means the build didn't change anything, // a null summary means the build didn't change anything, so use any errors from
// so use any errors from the last build // the last build
for (ResourceFile sourceFile : sourceBundle.getAllErrors().keySet()) { for (ResourceFile sourceFile : sourceBundle.getAllErrors().keySet()) {
if (sourceFile.getParentFile().equals(sourceDirectory)) { if (sourceFile.getParentFile().equals(sourceDirectory)) {
ScriptInfo scriptInfo = infoManager.getScriptInfo(sourceFile); ScriptInfo scriptInfo = infoManager.getScriptInfo(sourceFile);

View file

@ -83,7 +83,7 @@ public class GhidraScriptUtil {
} }
/** /**
* initialize state of GhidraScriptUtil with user, system paths, and optional extra system paths. * Initialize state of GhidraScriptUtil with user, system, and optional extra system paths.
* *
* @param aBundleHost the host to use * @param aBundleHost the host to use
* @param extraSystemPaths additional system paths for this run, can be null * @param extraSystemPaths additional system paths for this run, can be null
@ -106,7 +106,7 @@ public class GhidraScriptUtil {
*/ */
public static void dispose() { public static void dispose() {
if (bundleHost != null) { if (bundleHost != null) {
bundleHost.dispose(); bundleHost.stopFramework();
bundleHost = null; bundleHost = null;
} }
providers = null; providers = null;

View file

@ -121,8 +121,8 @@ public class JavaScriptProvider extends GhidraScriptProvider {
throw new ClassNotFoundException( throw new ClassNotFoundException(
"Failed to find source bundle containing script: " + sourceFile.toString()); "Failed to find source bundle containing script: " + sourceFile.toString());
} }
bundleHost.activateAll(Collections.singletonList(bundle), TaskMonitor.DUMMY, writer);
bundleHost.activateAll(Collections.singletonList(bundle), TaskMonitor.DUMMY, writer);
String classname = bundle.classNameForScript(sourceFile); String classname = bundle.classNameForScript(sourceFile);
Class<?> clazz = bundle.getOSGiBundle().loadClass(classname); // throws ClassNotFoundException Class<?> clazz = bundle.getOSGiBundle().loadClass(classname); // throws ClassNotFoundException
return clazz; return clazz;

View file

@ -87,7 +87,7 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest {
@After @After
public void tearDown() { public void tearDown() {
bundleHost.dispose(); bundleHost.stopFramework();
capturingBundleHostListener = null; capturingBundleHostListener = null;
bundleHost = null; bundleHost = null;

View file

@ -435,11 +435,11 @@ public class BundleStatusManagerTest extends AbstractGhidraScriptMgrPluginTest {
} }
void awaitActivation() throws InterruptedException { void awaitActivation() throws InterruptedException {
assertTrue(activationLatch.await(5000, TimeUnit.MILLISECONDS)); assertTrue(activationLatch.await(30000, TimeUnit.MILLISECONDS));
} }
void awaitDisablement() throws InterruptedException { void awaitDisablement() throws InterruptedException {
assertTrue(disablementLatch.await(5000, TimeUnit.MILLISECONDS)); assertTrue(disablementLatch.await(30000, TimeUnit.MILLISECONDS));
} }
} }

View file

@ -18,6 +18,7 @@ package ghidra.app.plugin.core.script;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.io.*; import java.io.*;
import java.nio.file.Path;
import org.apache.logging.log4j.*; import org.apache.logging.log4j.*;
import org.apache.logging.log4j.core.config.Configurator; import org.apache.logging.log4j.core.config.Configurator;
@ -150,7 +151,7 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes
// remove all class files from the user script bin dir // remove all class files from the user script bin dir
File userScriptsBinDir = File userScriptsBinDir =
GhidraSourceBundle.getBindirFromScriptFile(new ResourceFile(newScriptFile)).toFile(); getBinDirFromScriptFile(new ResourceFile(newScriptFile)).toFile();
File[] userScriptBinDirFiles; File[] userScriptBinDirFiles;
if (userScriptsBinDir.exists()) { if (userScriptsBinDir.exists()) {
userScriptBinDirFiles = userScriptsBinDir.listFiles(classFileFilter); userScriptBinDirFiles = userScriptsBinDir.listFiles(classFileFilter);
@ -195,7 +196,7 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes
// verify that the generated class file is placed in the default scripting home/bin // verify that the generated class file is placed in the default scripting home/bin
File userScriptsBinDir = File userScriptsBinDir =
GhidraSourceBundle.getBindirFromScriptFile(systemScriptFile).toFile(); getBinDirFromScriptFile(systemScriptFile).toFile();
String className = scriptName.replace(".java", ".class"); String className = scriptName.replace(".java", ".class");
File expectedClassFile = new File(userScriptsBinDir, className); File expectedClassFile = new File(userScriptsBinDir, className);
@ -232,7 +233,7 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes
waitForScriptCompletion(scriptID, 20000); waitForScriptCompletion(scriptID, 20000);
// verify a bin dir was created and that the class file is in it // verify a bin dir was created and that the class file is in it
File binDir = GhidraSourceBundle.getBindirFromScriptFile(newScriptFile).toFile(); File binDir = getBinDirFromScriptFile(newScriptFile).toFile();
assertTrue("bin output dir not created", binDir.exists()); assertTrue("bin output dir not created", binDir.exists());
File scriptClassFile = new File(binDir, rawScriptName + ".class"); File scriptClassFile = new File(binDir, rawScriptName + ".class");
@ -493,4 +494,11 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes
assertContainsText("The field of the script still has state--the script was not recreated", assertContainsText("The field of the script still has state--the script was not recreated",
"*2*", output); "*2*", output);
} }
private Path getBinDirFromScriptFile(ResourceFile sourceFile) {
ResourceFile tmpSourceDir = sourceFile.getParentFile();
String tmpSymbolicName = GhidraSourceBundle.sourceDirHash(tmpSourceDir);
return GhidraSourceBundle.getCompiledBundlesDir().resolve(tmpSymbolicName);
}
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,14 +23,12 @@ import java.io.*;
* *
* *
*/ */
@Deprecated(forRemoval = true, since = "10.2") // This is not used
public class JavaCompiler { public class JavaCompiler {
private IOThread cmdOut; private IOThread cmdOut;
private IOThread cmdErr; private IOThread cmdErr;
/**
* Compile a java file.
*/
public void compile(File javaFile) { public void compile(File javaFile) {
String name = javaFile.getName(); String name = javaFile.getName();
String className = name.substring(0, name.indexOf(".")) + ".class"; String className = name.substring(0, name.indexOf(".")) + ".class";
@ -60,8 +57,8 @@ public class JavaCompiler {
argV[5] = javaFile.getAbsolutePath(); argV[5] = javaFile.getAbsolutePath();
try { try {
Process p = Runtime.getRuntime().exec(argV); Process p = Runtime.getRuntime().exec(argV);
for (int i=0; i<argV.length; i++) { for (String element : argV) {
System.out.print(argV[i] + " "); System.out.print(element + " ");
} }
System.out.println(); System.out.println();
@ -74,18 +71,13 @@ public class JavaCompiler {
cmdOut.join(); cmdOut.join();
cmdErr.join(); cmdErr.join();
} catch (Exception e) { }
catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
/**
* Set up threads to read from stdin and stderr and output that
* to stdout.
* @param stdin
* @param stderr
*/
private void setupIO(InputStream stdin, InputStream stderr) { private void setupIO(InputStream stdin, InputStream stderr) {
cmdOut = new IOThread(stdin); // cmdOut = new IOThread(stdin); //
cmdErr = new IOThread(stderr); cmdErr = new IOThread(stderr);

View file

@ -19,6 +19,10 @@ import java.io.PrintWriter;
public class NullPrintWriter extends PrintWriter { public class NullPrintWriter extends PrintWriter {
public static PrintWriter dummyIfNull(PrintWriter pw) {
return pw == null ? new NullPrintWriter() : pw;
}
public NullPrintWriter() { public NullPrintWriter() {
super(new NullWriter()); super(new NullWriter());
} }

View file

@ -20,11 +20,9 @@ import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.*;
import java.util.Properties;
import ghidra.util.HTMLUtilities; import ghidra.util.HTMLUtilities;
import ghidra.util.SystemUtilities;
public class FileLocker { public class FileLocker {
@ -91,28 +89,16 @@ public class FileLocker {
Properties properties = new Properties(); Properties properties = new Properties();
InputStream is = null; try (InputStream is = new FileInputStream(lockFile)) {
try {
is = new FileInputStream(lockFile);
properties.load(is); properties.load(is);
return properties; return properties;
} }
catch (FileNotFoundException e) { catch (FileNotFoundException e) {
// should never happen // should not happen
} }
catch (IOException e) { catch (IOException e) {
// ignore // ignore
} }
finally {
if (is != null) {
try {
is.close();
}
catch (IOException e) {
// we tried!
}
}
}
return null; return null;
} }
@ -178,25 +164,13 @@ public class FileLocker {
private boolean storeProperties(Properties properties) { private boolean storeProperties(Properties properties) {
OutputStream os = null; try (OutputStream os = new FileOutputStream(lockFile)) {
try {
os = new FileOutputStream(lockFile);
properties.store(os, "Ghidra Lock File"); properties.store(os, "Ghidra Lock File");
return true; return true;
} }
catch (IOException e) { catch (IOException e) {
return false; return false;
} }
finally {
if (os != null) {
try {
os.close();
}
catch (IOException e) {
// don't care; we tried
}
}
}
} }
private boolean isLockOwner() { private boolean isLockOwner() {
@ -212,7 +186,7 @@ public class FileLocker {
for (String key : PROPERTY_KEYS) { for (String key : PROPERTY_KEYS) {
String originalProperty = createdLockProperties.getProperty(key); String originalProperty = createdLockProperties.getProperty(key);
String currentProperty = currentLockProperties.getProperty(key); String currentProperty = currentLockProperties.getProperty(key);
if (!SystemUtilities.isEqual(originalProperty, currentProperty)) { if (!Objects.equals(originalProperty, currentProperty)) {
return false; return false;
} }
} }

View file

@ -1,6 +1,5 @@
/* ### /* ###
* IP: GHIDRA * IP: GHIDRA
* REVIEWED: YES
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,12 +15,12 @@
*/ */
package ghidra.framework.project; package ghidra.framework.project;
import java.io.File;
import generic.util.FileLocker; import generic.util.FileLocker;
import generic.util.LockFactory; import generic.util.LockFactory;
import ghidra.framework.model.ProjectLocator; import ghidra.framework.model.ProjectLocator;
import java.io.File;
/** /**
* A simple delegate for creating and using locks in Ghidra. * A simple delegate for creating and using locks in Ghidra.
*/ */
@ -29,7 +28,7 @@ class ProjectLock {
private final File lockFile; private final File lockFile;
FileLocker locker; private FileLocker locker;
public ProjectLock(ProjectLocator projectLocator) { public ProjectLock(ProjectLocator projectLocator) {
this.lockFile = projectLocator.getProjectLockFile(); this.lockFile = projectLocator.getProjectLockFile();

View file

@ -880,7 +880,8 @@ public final class FileUtilities {
* *
* @param f1 the parent file * @param f1 the parent file
* @param f2 the child file * @param f2 the child file
* @return the portion of the second file that trails the full path of the first file. * @return the portion of the second file that trails the full path of the first file; null as
* described above
* @throws IOException if there is an error canonicalizing the path * @throws IOException if there is an error canonicalizing the path
*/ */
public static String relativizePath(File f1, File f2) throws IOException { public static String relativizePath(File f1, File f2) throws IOException {
@ -903,17 +904,18 @@ public final class FileUtilities {
} }
/** /**
* Return the relative path string of one resource file in another. If * Return the relative path string of one resource file in another. If no path can be
* no path can be constructed or the files are the same, then null is returned. * constructed or the files are the same, then null is returned.
* *
* Note: unlike {@link #relativizePath(File, File)}, this function does not resolve symbolic links. * Note: unlike {@link #relativizePath(File, File)}, this function does not resolve symbolic
* links.
* *
* <P>For example, given, in this order, two files with these paths * <P>For example, given, in this order, two files with these paths
* <code>/a/b</code> and <code>/a/b/c</code>, this method will return 'c'. * <code>/a/b</code> and <code>/a/b/c</code>, this method will return 'c'.
* *
* @param f1 the parent resource file * @param f1 the parent resource file
* @param f2 the child resource file * @param f2 the child resource file
* @return the relative path of {@code f2} in {@code f1} * @return the relative path of {@code f2} in {@code f1}; null if f1 is not a parent of f2
*/ */
public static String relativizePath(ResourceFile f1, ResourceFile f2) { public static String relativizePath(ResourceFile f1, ResourceFile f2) {
StringBuilder sb = new StringBuilder(f2.getName()); StringBuilder sb = new StringBuilder(f2.getName());