diff --git a/Ghidra/Features/Base/src/main/help/help/topics/BundleManager/BundleManager.htm b/Ghidra/Features/Base/src/main/help/help/topics/BundleManager/BundleManager.htm index a137710a81..389e5097e4 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/BundleManager/BundleManager.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/BundleManager/BundleManager.htm @@ -45,7 +45,7 @@

Source bundles

When a directory is added to the Bundle Manager, it is treated as a - source bundle and of its Java contents will be compiled to
+ source bundle and its Java contents will be compiled to
    <user home>/.ghidra/.ghidra-<version>/osgi/compiled-bundles/<hash>,
where <hash> is a hash of the source bundle location.

exploded bundles

diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BuildError.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BuildError.java new file mode 100644 index 0000000000..8db1a5dcb7 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BuildError.java @@ -0,0 +1,60 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.osgi; + +import generic.jar.ResourceFile; + +/** + * An error produced during {@link GhidraBundle#build()} with a time stamp + */ +public class BuildError { + // the lastModified time of the source causing this error + private final long lastModified; + + private final StringBuilder message = new StringBuilder(); + + /** + * Construct an object to record error message produced for {@code sourceFile} + * @param sourceFile the file causing this error + */ + public BuildError(ResourceFile sourceFile) { + lastModified = sourceFile.lastModified(); + } + + /** + * Append {@code str} to the current error message + * + * @param str the string to append + */ + public void append(String str) { + message.append(str); + } + + /** + * @return the error message + */ + String getMessage() { + return message.toString(); + } + + /** + * @return the last modified time of the source for this build error + */ + public long getLastModified() { + return lastModified; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java index 582daa83c0..2bd2b01c3a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleHost.java @@ -24,7 +24,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.felix.framework.FrameworkFactory; -import org.apache.felix.framework.Logger; import org.apache.felix.framework.util.FelixConstants; import org.osgi.framework.*; import org.osgi.framework.launch.Framework; @@ -45,19 +44,23 @@ import ghidra.util.task.*; *

* note: {@link GhidraBundle}, its implementations, and this class constitute * a bridge between OSGi's {@link Bundle} and Ghidra. - * - unqualified, "bundle" will mean {@link GhidraBundle} - * - use of OSGi types, including {@link Bundle} and {@link Framework}, should be package scoped (not public) - * - OSGi bundle lifecycle is simplified to "active" and "inactive" (OSGi's "uninstalled" state) + * */ public class BundleHost { protected static final boolean STDERR_DEBUGGING = false; - private static final String saveStateTagPath = "BundleHost_PATH"; - private static final String saveStateTagEnabled = "BundleHost_ENABLE"; - private static final String saveStateTagActive = "BundleHost_ACTIVE"; - private static final String saveStateTagSystem = "BundleHost_SYSTEM"; + 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_ACTIVE = "BundleHost_ACTIVE"; + private static final String SAVE_STATE_TAG_SYSTEM = "BundleHost_SYSTEM"; - HashMap bundlePathToBundleMap = new HashMap<>(); - HashMap bundleLocationToBundleMap = new HashMap<>(); + Map fileToBundleMap = new HashMap<>(); + Map bundleLocationToBundleMap = new HashMap<>(); BundleContext frameworkBundleContext; Framework felixFramework; @@ -69,20 +72,6 @@ public class BundleHost { // } - private static GhidraBundle newGhidraBundle(BundleHost bundleHost, ResourceFile bundlePath, - boolean enabled, boolean systemBundle) { - switch (GhidraBundle.getType(bundlePath)) { - case SourceDir: - return new GhidraSourceBundle(bundleHost, bundlePath, enabled, systemBundle); - case Jar: - return new GhidraJarBundle(bundleHost, bundlePath, enabled, systemBundle); - case BndScript: - default: - break; - } - return new GhidraPlaceholderBundle(bundleHost, bundlePath, enabled, systemBundle); - } - /** * stop the framework. */ @@ -91,35 +80,18 @@ public class BundleHost { } /** - * If there is currently a bundle managed with path {@code bundlePath}, return its {@link GhidraBundle}, - * otherwise return {@code null}. - * - * @param bundlePath the bundlePath of the sought bundle - * @return a {@link GhidraBundle} or {@code null} - */ - public GhidraBundle getExistingGhidraBundle(ResourceFile bundlePath) { - GhidraBundle bundle = bundlePathToBundleMap.get(bundlePath); - if (bundle == null) { - Msg.showError(this, null, "ghidra bundle cache", - "getExistingGhidraBundle expected a GhidraBundle created at " + bundlePath + - " but none was found"); - } - return bundle; - } - - /** - * If a {@link GhidraBundle} hasn't already been added for {@bundlePath}, add it now as a + * If a {@link GhidraBundle} hasn't already been added for {@bundleFile}, add it now as a * non-system bundle. * - * Enable the bundle. + *

Enable the bundle. * - * @param bundlePath the path to the bundle to (add and) enable + * @param bundleFile the bundle file to (add and) enable * @return false if the bundle was already enabled */ - public boolean enablePath(ResourceFile bundlePath) { - GhidraBundle bundle = bundlePathToBundleMap.get(bundlePath); + public boolean enable(ResourceFile bundleFile) { + GhidraBundle bundle = fileToBundleMap.get(bundleFile); if (bundle == null) { - bundle = add(bundlePath, true, false); + bundle = add(bundleFile, true, false); return true; } return enable(bundle); @@ -155,18 +127,49 @@ public class BundleHost { return false; } + /** + * If there is currently a bundle managed with file {@code bundleFile}, + * return its {@link GhidraBundle}, otherwise return {@code null}. + * + * @param bundleFile the bundleFile of the sought bundle + * @return a {@link GhidraBundle} or {@code null} + */ + public GhidraBundle getExistingGhidraBundle(ResourceFile bundleFile) { + GhidraBundle bundle = fileToBundleMap.get(bundleFile); + if (bundle == null) { + Msg.showError(this, null, "ghidra bundle cache", + "getExistingGhidraBundle expected a GhidraBundle created at " + bundleFile + + " but none was found"); + } + return bundle; + } + + private static GhidraBundle createGhidraBundle(BundleHost bundleHost, ResourceFile bundleFile, + boolean enabled, boolean systemBundle) { + switch (GhidraBundle.getType(bundleFile)) { + case SOURCE_DIR: + return new GhidraSourceBundle(bundleHost, bundleFile, enabled, systemBundle); + case JAR: + return new GhidraJarBundle(bundleHost, bundleFile, enabled, systemBundle); + case BND_SCRIPT: + default: + break; + } + return new GhidraPlaceholderBundle(bundleHost, bundleFile, enabled, systemBundle); + } + /** * Create a new GhidraBundle and add to the list of managed bundles * - * @param bundlePath the bundle's path + * @param bundleFile the bundle file * @param enabled if the new bundle should be enabled * @param systemBundle if the new bundle is a system bundle * @return a new GhidraBundle */ - public GhidraBundle add(ResourceFile bundlePath, boolean enabled, boolean systemBundle) { - GhidraBundle bundle = newGhidraBundle(this, bundlePath, enabled, systemBundle); - bundlePathToBundleMap.put(bundlePath, bundle); - bundleLocationToBundleMap.put(bundle.getBundleLocation(), bundle); + public GhidraBundle add(ResourceFile bundleFile, boolean enabled, boolean systemBundle) { + GhidraBundle bundle = createGhidraBundle(this, bundleFile, enabled, systemBundle); + fileToBundleMap.put(bundleFile, bundle); + bundleLocationToBundleMap.put(bundle.getLocationIdentifier(), bundle); fireBundleAdded(bundle); return bundle; } @@ -175,19 +178,20 @@ public class BundleHost { * Create new GhidraBundles and add to the list of managed bundles. All GhidraBundles created * with the same {@code enabled} and {@code systemBundle} values. * - * @param bundlePaths a list of bundle paths + * @param bundleFiles a list of bundle files * @param enabled if the new bundle should be enabled * @param systemBundle if the new bundle is a system bundle */ - public void add(List bundlePaths, boolean enabled, boolean systemBundle) { - Map newBundleMap = bundlePaths.stream() + public void add(List bundleFiles, boolean enabled, boolean systemBundle) { + Map newBundleMap = bundleFiles.stream() .collect(Collectors.toUnmodifiableMap(Function.identity(), - bundlePath -> newGhidraBundle(BundleHost.this, bundlePath, enabled, systemBundle))); - bundlePathToBundleMap.putAll(newBundleMap); + bundleFile -> createGhidraBundle(BundleHost.this, bundleFile, enabled, + systemBundle))); + fileToBundleMap.putAll(newBundleMap); bundleLocationToBundleMap.putAll(newBundleMap.values() .stream() - .collect( - Collectors.toUnmodifiableMap(GhidraBundle::getBundleLocation, Function.identity()))); + .collect(Collectors.toUnmodifiableMap(GhidraBundle::getLocationIdentifier, + Function.identity()))); fireBundlesAdded(newBundleMap.values()); } @@ -196,10 +200,10 @@ public class BundleHost { * * @param bundles the bundles to add */ - public void add(List bundles) { + private void add(List bundles) { for (GhidraBundle bundle : bundles) { - bundlePathToBundleMap.put(bundle.getPath(), bundle); - bundleLocationToBundleMap.put(bundle.getBundleLocation(), bundle); + fileToBundleMap.put(bundle.getFile(), bundle); + bundleLocationToBundleMap.put(bundle.getLocationIdentifier(), bundle); } fireBundlesAdded(bundles); } @@ -207,11 +211,11 @@ public class BundleHost { /** * Remove a bundle from the list of managed bundles. * - * @param bundlePath the path of the bundle to remove + * @param bundleFile the file of the bundle to remove */ - public void removeBundlePath(ResourceFile bundlePath) { - GhidraBundle bundle = bundlePathToBundleMap.remove(bundlePath); - bundleLocationToBundleMap.remove(bundle.getBundleLocation()); + public void remove(ResourceFile bundleFile) { + GhidraBundle bundle = fileToBundleMap.remove(bundleFile); + bundleLocationToBundleMap.remove(bundle.getLocationIdentifier()); fireBundleRemoved(bundle); } @@ -220,9 +224,9 @@ public class BundleHost { * * @param bundleLocation the location id of the bundle to remove */ - public void removeBundleLoc(String bundleLocation) { + public void remove(String bundleLocation) { GhidraBundle bundle = bundleLocationToBundleMap.remove(bundleLocation); - bundlePathToBundleMap.remove(bundle.getPath()); + fileToBundleMap.remove(bundle.getFile()); fireBundleRemoved(bundle); } @@ -232,8 +236,8 @@ public class BundleHost { * @param bundle the bundle to remove */ public void remove(GhidraBundle bundle) { - bundlePathToBundleMap.remove(bundle.getPath()); - bundleLocationToBundleMap.remove(bundle.getBundleLocation()); + fileToBundleMap.remove(bundle.getFile()); + bundleLocationToBundleMap.remove(bundle.getLocationIdentifier()); fireBundleRemoved(bundle); } @@ -244,8 +248,8 @@ public class BundleHost { */ public void remove(Collection bundles) { for (GhidraBundle bundle : bundles) { - bundlePathToBundleMap.remove(bundle.getPath()); - bundleLocationToBundleMap.remove(bundle.getBundleLocation()); + fileToBundleMap.remove(bundle.getFile()); + bundleLocationToBundleMap.remove(bundle.getLocationIdentifier()); } fireBundlesRemoved(bundles); } @@ -257,17 +261,16 @@ public class BundleHost { /** * Try to install a bundle. * - * * @param bundle the bundle to install * @return the OSGi bundle returned by the framework - * @throws GhidraBundleException when install fails + * @throws GhidraBundleException if install fails */ public Bundle install(GhidraBundle bundle) throws GhidraBundleException { try { - return frameworkBundleContext.installBundle(bundle.getBundleLocation()); + return frameworkBundleContext.installBundle(bundle.getLocationIdentifier()); } catch (BundleException e) { - throw new GhidraBundleException(bundle.getBundleLocation(), + throw new GhidraBundleException(bundle.getLocationIdentifier(), "installing from bundle location", e); } } @@ -296,16 +299,16 @@ public class BundleHost { * @return all the bundles */ public Collection getGhidraBundles() { - return bundlePathToBundleMap.values(); + return fileToBundleMap.values(); } /** - * return paths of currently managed bundles + * return the list of currently managed bundle files * - * @return all the bundle paths + * @return all the bundle files */ - public Collection getBundlePaths() { - return bundlePathToBundleMap.keySet(); + public Collection getBundleFiles() { + return fileToBundleMap.keySet(); } void dumpLoadedBundles() { @@ -319,28 +322,29 @@ public class BundleHost { /** * Attempt to resolve a list of BundleRequirements with active Bundle capabilities. * - * @param reqs list of requirements -- satisfied requirements are removed as capabiliites are found - * @return the list of BundeWiring objects correpsonding to matching capabilities + * @param requirements list of requirements -- satisfied requirements are removed as + * capabilities are found + * @return list of {@link BundleWiring} objects corresponding to matching capabilities */ - public List resolve(List reqs) { + public List resolve(List requirements) { // enumerate active bundles, looking for capabilities meeting our requirements List bundleWirings = new ArrayList<>(); for (Bundle bundle : frameworkBundleContext.getBundles()) { if (bundle.getState() == Bundle.ACTIVE) { - BundleWiring bw = bundle.adapt(BundleWiring.class); + BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); boolean keeper = false; - for (BundleCapability cap : bw.getCapabilities(null)) { - Iterator it = reqs.iterator(); - while (it.hasNext()) { - BundleRequirement req = it.next(); - if (req.matches(cap)) { - it.remove(); + for (BundleCapability capability : bundleWiring.getCapabilities(null)) { + Iterator requirementsIterator = requirements.iterator(); + while (requirementsIterator.hasNext()) { + BundleRequirement requirement = requirementsIterator.next(); + if (requirement.matches(capability)) { + requirementsIterator.remove(); keeper = true; } } } if (keeper) { - bundleWirings.add(bw); + bundleWirings.add(bundleWiring); } } } @@ -351,7 +355,7 @@ public class BundleHost { * Attempt to resolve {@code requirements} against the currently active bundles. * * @param requirements a list of {@link BundleRequirement} objects - * @return true if all of the requiremetns can be resolved + * @return true if all of the requirements can be resolved */ public boolean canResolveAll(Collection requirements) { LinkedList tmpRequirements = new LinkedList<>(requirements); @@ -359,27 +363,6 @@ public class BundleHost { return tmpRequirements.isEmpty(); } - private class FelixStderrLogger extends Logger { - @Override - protected void doLog(int level, String message, Throwable throwable) { - System.err.printf("felixlogger: %s %s\n", message, throwable); - } - - @Override - protected void doLogOut(int level, String message, Throwable throwable) { - System.err.printf("felixlogger: %s %s\n", message, throwable); - } - - @SuppressWarnings("rawtypes") - - @Override - protected void doLog(final Bundle bundle, final ServiceReference sr, final int level, - final String message, final Throwable throwable) { - System.err.printf("felixlogger: %s %s %s\n", bundle, message, throwable); - } - - } - protected String buildExtraSystemPackages() { Set packages = new HashSet<>(); OSGiUtils.getPackagesFromClasspath(packages); @@ -397,7 +380,8 @@ public class BundleHost { } /** - * A directory for use by Felix as a cache + * A directory for use by the OSGi framework as a cache + * * @return the directory */ protected static Path getCacheDir() { @@ -432,7 +416,7 @@ public class BundleHost { config.put(FelixConstants.LOG_LEVEL_PROP, "1"); if (STDERR_DEBUGGING) { config.put(FelixConstants.LOG_LEVEL_PROP, "999"); - config.put(FelixConstants.LOG_LOGGER_PROP, new FelixStderrLogger()); + // config.put(FelixConstants.LOG_LOGGER_PROP, new org.apache.felix.framework.Logger() {...}); } FrameworkFactory factory = new FrameworkFactory(); @@ -465,54 +449,6 @@ public class BundleHost { }); } - /** - * add the {@code BundleListener} that notifies listeners of bundle activation changes - */ - protected void addBundleListener() { - final Bundle systemBundle = frameworkBundleContext.getBundle(); - frameworkBundleContext.addBundleListener(new BundleListener() { - @Override - public void bundleChanged(BundleEvent event) { - Bundle osgiBundle = event.getBundle(); - - // ignore events on the system bundle - if (osgiBundle == systemBundle) { - return; - } - if (STDERR_DEBUGGING) { - String n = osgiBundle.getSymbolicName(); - String l = osgiBundle.getLocation(); - System.err.printf("%s %s from %s\n", OSGiUtils.getEventTypeString(event), n, l); - } - GhidraBundle bundle; - switch (event.getType()) { - case BundleEvent.STARTED: - bundle = bundleLocationToBundleMap.get(osgiBundle.getLocation()); - if (bundle != null) { - fireBundleActivationChange(bundle, true); - } - else { - Msg.error(this, String.format("not a GhidraBundle: %s\n", - osgiBundle.getLocation())); - } - break; - case BundleEvent.UNINSTALLED: - bundle = bundleLocationToBundleMap.get(osgiBundle.getLocation()); - if (bundle != null) { - fireBundleActivationChange(bundle, false); - } - else { - Msg.error(this, String.format("not a GhidraBundle: %s\n", - osgiBundle.getLocation())); - } - break; - default: - break; - } - } - }); - } - /** * start the framework * @@ -534,7 +470,8 @@ public class BundleHost { addDebuggingListeners(); } - addBundleListener(); + frameworkBundleContext + .addBundleListener(new MyBundleListener(frameworkBundleContext.getBundle())); try { felixFramework.start(); @@ -551,10 +488,16 @@ public class BundleHost { if (felixFramework != null) { try { felixFramework.stop(); - felixFramework.waitForStop(5000); + // 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); + if (event.getType() == FrameworkEvent.WAIT_TIMEDOUT) { + Msg.error(this, "Stopping OSGi framework timed out after 5 seconds."); + } felixFramework = null; } catch (BundleException | InterruptedException e) { + Msg.error(this, "Failed to stop OSGi framework."); e.printStackTrace(); } } @@ -578,8 +521,8 @@ public class BundleHost { } private static boolean anyMatch(Bundle bundle, int... bundleStates) { - Integer s = bundle.getState(); - return IntStream.of(bundleStates).anyMatch(s::equals); + Integer bundleState = bundle.getState(); + return IntStream.of(bundleStates).anyMatch(bundleState::equals); } private static void waitFor(Bundle bundle, int... bundleStates) throws InterruptedException { @@ -607,9 +550,10 @@ public class BundleHost { bundle.start(); } catch (BundleException e) { - GhidraBundleException gbe = new GhidraBundleException(bundle, "activating bundle", e); - fireBundleException(gbe); - throw gbe; + GhidraBundleException bundleException = + new GhidraBundleException(bundle, "activating bundle", e); + fireBundleException(bundleException); + throw bundleException; } waitFor(bundle, Bundle.ACTIVE); } @@ -690,7 +634,7 @@ public class BundleHost { protected void activateAll(Collection bundles, TaskMonitor monitor, PrintWriter console) { List bundlesRemaining = new ArrayList<>(bundles); - + monitor.setMaximum(bundlesRemaining.size()); while (!bundlesRemaining.isEmpty() && !monitor.isCancelled()) { List resolvableBundles = bundlesRemaining.stream() @@ -704,14 +648,14 @@ public class BundleHost { else { bundlesRemaining.removeAll(resolvableBundles); } - + for (GhidraBundle bundle : resolvableBundles) { if (monitor.isCancelled()) { break; } try { bundle.build(console); - activateSynchronously(bundle.getBundleLocation()); + activateSynchronously(bundle.getLocationIdentifier()); } catch (Exception e) { e.printStackTrace(console); @@ -820,31 +764,31 @@ public class BundleHost { /** * Restore the list of managed bundles from {@code saveState} and each bundle's state. * - * Bundles that had been active are reactivated. + *

Bundles that had been active are reactivated. * - * note: This is done once on startup, AFTER system bundles have been added. + *

note: This is done once on startup after system bundles have been added. * * @param saveState the state object * @param tool the tool */ public void restoreManagedBundleState(SaveState saveState, PluginTool tool) { - String[] bundlePaths = saveState.getStrings(saveStateTagPath, new String[0]); + String[] bundleFiles = saveState.getStrings(SAVE_STATE_TAG_FILE, new String[0]); boolean[] bundleIsEnabled = - saveState.getBooleans(saveStateTagEnabled, new boolean[bundlePaths.length]); + saveState.getBooleans(SAVE_STATE_TAG_ENABLE, new boolean[bundleFiles.length]); boolean[] bundleIsActive = - saveState.getBooleans(saveStateTagActive, new boolean[bundlePaths.length]); + saveState.getBooleans(SAVE_STATE_TAG_ACTIVE, new boolean[bundleFiles.length]); boolean[] bundleIsSystem = - saveState.getBooleans(saveStateTagSystem, new boolean[bundlePaths.length]); + saveState.getBooleans(SAVE_STATE_TAG_SYSTEM, new boolean[bundleFiles.length]); List newBundles = new ArrayList<>(); List bundlesToActivate = new ArrayList<>(); - for (int i = 0; i < bundlePaths.length; i++) { - ResourceFile bundlePath = generic.util.Path.fromPathString(bundlePaths[i]); + for (int i = 0; i < bundleFiles.length; i++) { + ResourceFile bundleFile = generic.util.Path.fromPathString(bundleFiles[i]); boolean isEnabled = bundleIsEnabled[i]; boolean isActive = bundleIsActive[i]; boolean isSystem = bundleIsSystem[i]; - GhidraBundle bundle = bundlePathToBundleMap.get(bundlePath); + GhidraBundle bundle = fileToBundleMap.get(bundleFile); if (bundle != null) { if (isEnabled != bundle.isEnabled()) { bundle.setEnabled(isEnabled); @@ -852,7 +796,7 @@ public class BundleHost { } if (isSystem != bundle.isSystemBundle()) { bundle.systemBundle = isSystem; - Msg.error(this, String.format("%s went from %system to %system", bundlePath, + Msg.error(this, String.format("%s went from %system to %system", bundleFile, isSystem ? "not " : "", isSystem ? "" : "not ")); } } @@ -860,7 +804,8 @@ public class BundleHost { // stored system bundles that weren't already initialized must be old, drop 'm. } else { - newBundles.add(bundle = newGhidraBundle(this, bundlePath, isEnabled, isSystem)); + bundle = createGhidraBundle(this, bundleFile, isEnabled, isSystem); + newBundles.add(bundle); } if (bundle != null && isActive) { bundlesToActivate.add(bundle); @@ -876,25 +821,76 @@ public class BundleHost { * @param saveState the state object */ public void saveManagedBundleState(SaveState saveState) { - int numBundles = bundlePathToBundleMap.size(); - String[] bundlePaths = new String[numBundles]; + int numBundles = fileToBundleMap.size(); + String[] bundleFiles = new String[numBundles]; boolean[] bundleIsEnabled = new boolean[numBundles]; boolean[] bundleIsActive = new boolean[numBundles]; boolean[] bundleIsSystem = new boolean[numBundles]; int index = 0; - for (GhidraBundle bundle : bundlePathToBundleMap.values()) { - bundlePaths[index] = generic.util.Path.toPathString(bundle.getPath()); + for (GhidraBundle bundle : fileToBundleMap.values()) { + bundleFiles[index] = generic.util.Path.toPathString(bundle.getFile()); bundleIsEnabled[index] = bundle.isEnabled(); bundleIsActive[index] = bundle.isActive(); bundleIsSystem[index] = bundle.isSystemBundle(); ++index; } - saveState.putStrings(saveStateTagPath, bundlePaths); - saveState.putBooleans(saveStateTagEnabled, bundleIsEnabled); - saveState.putBooleans(saveStateTagActive, bundleIsActive); - saveState.putBooleans(saveStateTagSystem, bundleIsSystem); + saveState.putStrings(SAVE_STATE_TAG_FILE, bundleFiles); + saveState.putBooleans(SAVE_STATE_TAG_ENABLE, bundleIsEnabled); + saveState.putBooleans(SAVE_STATE_TAG_ACTIVE, bundleIsActive); + saveState.putBooleans(SAVE_STATE_TAG_SYSTEM, bundleIsSystem); } + /** + * The {@code BundleListener} that notifies {@link BundleHostListener}s of bundle activation changes + */ + private class MyBundleListener implements BundleListener { + private final Bundle systemBundle; + + private MyBundleListener(Bundle systemBundle) { + this.systemBundle = systemBundle; + } + + @Override + public void bundleChanged(BundleEvent event) { + Bundle osgiBundle = event.getBundle(); + + // ignore events on the system bundle + if (osgiBundle == systemBundle) { + return; + } + if (STDERR_DEBUGGING) { + String symbolicName = osgiBundle.getSymbolicName(); + String locationIdentifier = osgiBundle.getLocation(); + System.err.printf("%s %s from %s\n", OSGiUtils.getEventTypeString(event), + symbolicName, locationIdentifier); + } + GhidraBundle bundle; + switch (event.getType()) { + case BundleEvent.STARTED: + bundle = bundleLocationToBundleMap.get(osgiBundle.getLocation()); + if (bundle != null) { + fireBundleActivationChange(bundle, true); + } + else { + Msg.error(this, + String.format("not a GhidraBundle: %s\n", osgiBundle.getLocation())); + } + break; + case BundleEvent.UNINSTALLED: + bundle = bundleLocationToBundleMap.get(osgiBundle.getLocation()); + if (bundle != null) { + fireBundleActivationChange(bundle, false); + } + else { + Msg.error(this, + String.format("not a GhidraBundle: %s\n", osgiBundle.getLocation())); + } + break; + default: + break; + } + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatus.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatus.java index d2e46d988d..c6432276f7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatus.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatus.java @@ -19,34 +19,38 @@ import generic.jar.ResourceFile; import generic.util.Path; /** - * The BundleStatus class represents the runtime state and user preferences for OSGi bundles in Ghidra. + * The BundleStatus class represents the runtime state and user preferences for bundles. */ public class BundleStatus implements Comparable { - final Path path; - final GhidraBundle.Type type; - final String bundleLocation; + private final GhidraBundle.Type type; + private final String location; - boolean active = false; - boolean busy = false; + private final ResourceFile file; + private final boolean readOnly; + private boolean enabled; + private boolean active = false; + private boolean busy = false; - String summary; + private String summary; - BundleStatus(ResourceFile path, boolean enabled, boolean readonly, String bundleLoc) { - this.path = new Path(path, enabled, false, readonly); - type = GhidraBundle.getType(getPath()); - this.bundleLocation = bundleLoc; + BundleStatus(ResourceFile bundleFile, boolean enabled, boolean readOnly, String bundleLoc) { + this.file = bundleFile; + type = GhidraBundle.getType(getFile()); + this.location = bundleLoc; + this.enabled = enabled; + this.readOnly = readOnly; } @Override public int compareTo(BundleStatus o) { - return path.compareTo(o != null ? o.path : null); + return Path.toPathString(file).compareTo(o != null ? Path.toPathString(o.file) : null); } /** * @return true if the bundle is enabled */ public boolean isEnabled() { - return path.isEnabled(); + return enabled; } /** @@ -55,14 +59,14 @@ public class BundleStatus implements Comparable { * @param isEnabled true to set status to enabled */ public void setEnabled(boolean isEnabled) { - path.setEnabled(isEnabled); + this.enabled = isEnabled; } /** * @return true if the bundle is read only */ public boolean isReadOnly() { - return path.isReadOnly(); + return readOnly; } /** @@ -107,31 +111,31 @@ public class BundleStatus implements Comparable { } /** - * @return the bundle's path + * @return the bundle file */ - public ResourceFile getPath() { - return path.getPath(); + public ResourceFile getFile() { + return file; } /** - * @return true if the bundle's path exists + * @return true if the bundle file exists */ - public boolean pathExists() { - return path.exists(); + public boolean fileExists() { + return file.exists(); } /** - * @return the bundle's path as a string, using $USER and $GHIDRA_HOME when appropriate + * @return the bundle file path, using $USER and $GHIDRA_HOME when appropriate */ public String getPathAsString() { - return path.getPathAsString(); + return Path.toPathString(file); } /** * @return the bundle's location identifier */ - public String getBundleLocation() { - return bundleLocation; + public String getLocationIdentifier() { + return location; } void setBusy(boolean isBusy) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java index 9691c3fc7e..6e737d628b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusComponentProvider.java @@ -25,8 +25,7 @@ import javax.swing.*; import javax.swing.event.TableModelEvent; import javax.swing.table.TableColumn; -import docking.ActionContext; -import docking.action.*; +import docking.action.builder.ActionBuilder; import docking.util.AnimationUtils; import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooserMode; @@ -79,32 +78,7 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { this.bundleHost = bundleHost; this.bundleStatusTableModel = new BundleStatusTableModel(this, bundleHost); - bundleStatusTableModel.addListener(new BundleStatusChangeRequestListener() { - @Override - public void bundleEnablementChangeRequest(BundleStatus status, boolean enabled) { - GhidraBundle gb = bundleHost.getExistingGhidraBundle(status.getPath()); - if (gb instanceof GhidraPlaceholderBundle) { - return; - } - if (enabled) { - bundleHost.enable(gb); - } - else { - if (status.isActive()) { - startActivateDeactiveTask(status, false); - } - bundleHost.disable(gb); - } - } - - @Override - public void bundleActivationChangeRequest(BundleStatus status, boolean newValue) { - if (status.isEnabled()) { - startActivateDeactiveTask(status, newValue); - } - } - - }); + bundleStatusTableModel.addListener(new MyBundleStatusChangeRequestListener()); this.filter = new GhidraFileFilter() { @Override @@ -113,8 +87,8 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { } @Override - public boolean accept(File path, GhidraFileChooserModel model) { - return GhidraBundle.getType(path) != GhidraBundle.Type.INVALID; + public boolean accept(File file, GhidraFileChooserModel model) { + return GhidraBundle.getType(file) != GhidraBundle.Type.INVALID; } }; this.fileChooser = null; @@ -178,7 +152,7 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { BundleStatus status = (BundleStatus) data.getRowObject(); - Component x = super.getTableCellRendererComponent(data); + Component component = super.getTableCellRendererComponent(data); if (status.isBusy()) { cb.setVisible(false); cb.setEnabled(false); @@ -190,7 +164,7 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { cb.setEnabled(true); setText(""); } - return x; + return component; } }); @@ -199,69 +173,40 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { bundleStatusTable.getColumnModel().getColumn(bundleStatusTableModel.typeColumn.index); FontMetrics fontmetrics = panel.getFontMetrics(panel.getFont()); column.setMaxWidth(10 + - SwingUtilities.computeStringWidth(fontmetrics, GhidraBundle.Type.SourceDir.toString())); + SwingUtilities.computeStringWidth(fontmetrics, GhidraBundle.Type.SOURCE_DIR.toString())); column = bundleStatusTable.getColumnModel().getColumn(bundleStatusTableModel.pathColumn.index); column.setCellRenderer(new GTableCellRenderer() { @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { - ResourceFile path = (ResourceFile) data.getValue(); - JLabel c = (JLabel) super.getTableCellRendererComponent(data); - c.setText(Path.toPathString(path)); - GhidraBundle gb = bundleHost.getExistingGhidraBundle(path); - if (gb == null || gb instanceof GhidraPlaceholderBundle || !path.exists()) { - c.setForeground(Color.RED); + ResourceFile file = (ResourceFile) data.getValue(); + JLabel label = (JLabel) super.getTableCellRendererComponent(data); + label.setText(Path.toPathString(file)); + GhidraBundle bundle = bundleHost.getExistingGhidraBundle(file); + if (bundle == null || bundle instanceof GhidraPlaceholderBundle || !file.exists()) { + label.setForeground(Color.RED); } - return c; + return label; } }); } private void addBundlesAction(String actionName, String description, Icon icon, Runnable runnable) { - DockingAction action = new DockingAction(actionName, this.getName()) { - @Override - public void actionPerformed(ActionContext context) { - runnable.run(); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return bundleStatusTable.getSelectedRows().length > 0; - } - }; - action.setPopupMenuData(new MenuData(new String[] { description }, icon, BUNDLE_GROUP)); - action.setToolBarData(new ToolBarData(icon, BUNDLE_GROUP)); - action.setDescription(description); - action.setEnabled(false); - getTool().addLocalAction(this, action); - - } - - private void addBundleListAction(String actionName, String name, String description, Icon icon, - Runnable runnable) { - DockingAction action = new DockingAction(actionName, this.getName()) { - @Override - public void actionPerformed(ActionContext context) { - runnable.run(); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return true; - } - }; - action.setPopupMenuData(new MenuData(new String[] { name }, icon, BUNDLE_LIST_GROUP)); - action.setToolBarData(new ToolBarData(icon, BUNDLE_LIST_GROUP)); - action.setDescription(description); - action.setEnabled(true); - getTool().addLocalAction(this, action); + new ActionBuilder(actionName, this.getName()).popupMenuPath(description) + .popupMenuIcon(icon) + .popupMenuGroup(BUNDLE_GROUP) + .toolBarIcon(icon) + .toolBarGroup(BUNDLE_GROUP) + .description(description) + .enabled(false) + .enabledWhen(context -> bundleStatusTable.getSelectedRows().length > 0) + .onAction(context -> runnable.run()) + .buildAndInstallLocal(this); } private void createActions() { - DockingAction action; - addBundlesAction("ActivateBundles", "Activate bundle(s)", ResourceManager.loadImage("images/media-playback-start.png"), this::doActivateBundles); @@ -271,47 +216,26 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { addBundlesAction("CleanBundles", "Clean bundle(s)", ResourceManager.loadImage("images/erase16.png"), this::doClean); - // - action = new DockingAction("AddBundles", this.getName()) { - @Override - public void actionPerformed(ActionContext context) { - showAddBundlesFileChooser(); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return true; - } - - }; Icon icon = ResourceManager.loadImage("images/Plus.png"); - action.setPopupMenuData( - new MenuData(new String[] { "Add bundle(s)" }, icon, BUNDLE_LIST_GROUP)); - action.setToolBarData(new ToolBarData(icon, BUNDLE_LIST_GROUP)); - action.setDescription("Display file chooser to add bundles to list"); - action.setEnabled(true); - getTool().addLocalAction(this, action); + new ActionBuilder("AddBundles", this.getName()).popupMenuPath("Add Bundle(s)") + .popupMenuIcon(icon) + .popupMenuGroup(BUNDLE_LIST_GROUP) + .toolBarIcon(icon) + .toolBarGroup(BUNDLE_LIST_GROUP) + .description("Display file chooser to add bundles to list") + .onAction(c -> showAddBundlesFileChooser()) + .buildAndInstallLocal(this); - // icon = ResourceManager.loadImage("images/edit-delete.png"); - action = new DockingAction("RemoveBundles", this.getName()) { - @Override - public void actionPerformed(ActionContext context) { - doRemoveBundles(); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - return bundleStatusTable.getSelectedRows().length > 0; - } - - }; - action.setPopupMenuData( - new MenuData(new String[] { "Remove bundle(s)" }, icon, BUNDLE_LIST_GROUP)); - action.setToolBarData(new ToolBarData(icon, BUNDLE_LIST_GROUP)); - action.setDescription("Remove selected bundle(s) from the list"); - action.setEnabled(true); - getTool().addLocalAction(this, action); + new ActionBuilder("RemoveBundles", this.getName()).popupMenuPath("Remove bundle(s)") + .popupMenuIcon(icon) + .popupMenuGroup(BUNDLE_LIST_GROUP) + .toolBarIcon(icon) + .toolBarGroup(BUNDLE_LIST_GROUP) + .description("Remove selected bundle(s) from the list") + .enabledWhen(c -> bundleStatusTable.getSelectedRows().length > 0) + .onAction(c -> doRemoveBundles()) + .buildAndInstallLocal(this); } /** @@ -330,10 +254,10 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { private void doClean() { int[] selectedModelRows = getSelectedModelRows(); boolean anythingCleaned = false; - for (BundleStatus bs : bundleStatusTableModel.getRowObjects(selectedModelRows)) { - anythingCleaned |= bundleHost.getExistingGhidraBundle(bs.getPath()).clean(); - if (!bs.getSummary().isEmpty()) { - bs.setSummary(""); + for (BundleStatus status : bundleStatusTableModel.getRowObjects(selectedModelRows)) { + anythingCleaned |= bundleHost.getExistingGhidraBundle(status.getFile()).clean(); + if (!status.getSummary().isEmpty()) { + status.setSummary(""); anythingCleaned |= true; } } @@ -350,25 +274,25 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { } doDeactivateBundles(); + // partition bundles into system (bundles.get(true)) and non-system (bundles.get(false)). Map> bundles = bundleStatusTableModel.getRowObjects(selectedModelRows) .stream() - .map(bs -> bundleHost.getExistingGhidraBundle(bs.getPath())) - .collect(Collectors.partitioningBy(gb -> gb.isSystemBundle())); + .map(bs -> bundleHost.getExistingGhidraBundle(bs.getFile())) + .collect(Collectors.partitioningBy(GhidraBundle::isSystemBundle)); + List systemBundles = bundles.get(true); if (!systemBundles.isEmpty()) { - StringBuilder sb = new StringBuilder(); - for (GhidraBundle gb : systemBundles) { - bundleHost.disable(gb); - sb.append(gb.getPath() + "\n"); + StringBuilder stringBuilder = new StringBuilder(); + for (GhidraBundle bundle : systemBundles) { + bundleHost.disable(bundle); + stringBuilder.append(bundle.getFile() + "\n"); } Msg.showWarn(this, this.getComponent(), "Unabled to remove", - "System bundles cannot be removed:\n" + sb.toString()); - + "System bundles cannot be removed:\n" + stringBuilder.toString()); } bundleHost.remove(bundles.get(false)); - } private void showAddBundlesFileChooser() { @@ -393,15 +317,15 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { } String lastSelected = Preferences.getProperty(PREFENCE_LAST_SELECTED_BUNDLE); if (lastSelected != null) { - File f = new File(lastSelected); - fileChooser.setSelectedFile(f); + File lastSelectedFile = new File(lastSelected); + fileChooser.setSelectedFile(lastSelectedFile); } } else { String lastSelected = Preferences.getProperty(PREFENCE_LAST_SELECTED_BUNDLE); if (lastSelected != null) { - File f = new File(lastSelected); - fileChooser.setSelectedFile(f); + File lastSelectedFile = new File(lastSelected); + fileChooser.setSelectedFile(lastSelectedFile); } fileChooser.rescanCurrentDirectory(); } @@ -417,104 +341,24 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { } protected void doActivateBundles() { - int[] selectedModelRows = getSelectedModelRows(); - - new TaskLauncher(new Task("activating", true, true, false) { - @Override - public void run(TaskMonitor monitor) throws CancelledException { - // suppress RowObjectSelectionManager repairs until after we're done - bundleStatusTable.chill(); - - List statuses = - bundleStatusTableModel.getRowObjects(selectedModelRows) - .stream() - .filter(bs -> !bs.isActive()) - .collect(Collectors.toUnmodifiableList()); - - List gbs = new ArrayList<>(); - for (BundleStatus bs : statuses) { - GhidraBundle gb = bundleHost.getExistingGhidraBundle(bs.getPath()); - if (!(gb instanceof GhidraPlaceholderBundle)) { - bs.setBusy(true); - bundleHost.enable(gb); - gbs.add(gb); - } - } - notifyTableDataChanged(); - - bundleHost.activateAll(gbs, monitor, - getTool().getService(ConsoleService.class).getStdErr()); - - boolean anybusy = false; - for (BundleStatus bs : statuses) { - if (bs.isBusy()) { - anybusy = true; - bs.setBusy(false); - } - } - if (anybusy) { - notifyTableDataChanged(); - } - - bundleStatusTable.thaw(); - } - }, getComponent(), 1000); + new TaskLauncher( + new ActivateBundlesTask("activating", true, true, false, getSelectedModelRows()), + getComponent(), 1000); } protected void doDeactivateBundles() { - ConsoleService console = getTool().getService(ConsoleService.class); - int[] selectedModelRows = getSelectedModelRows(); - - new TaskLauncher(new Task("deactivating", true, true, false) { - @Override - public void run(TaskMonitor monitor) throws CancelledException { - List gbs = bundleStatusTableModel.getRowObjects(selectedModelRows) - .stream() - .filter(bs -> bs.isActive()) - .map(bs -> bundleHost.getExistingGhidraBundle(bs.getPath())) - .collect(Collectors.toList()); - - monitor.setMaximum(gbs.size()); - for (GhidraBundle gb : gbs) { - try { - bundleHost.deactivateSynchronously(gb.getBundleLocation()); - } - catch (GhidraBundleException | InterruptedException e) { - e.printStackTrace(console.getStdErr()); - } - monitor.incrementProgress(1); - } - } - }, getComponent(), 1000); + new TaskLauncher( + new DeactivateBundlesTask("deactivating", true, true, false, getSelectedModelRows()), + getComponent(), 1000); } - protected void startActivateDeactiveTask(BundleStatus status, boolean activate) { + protected void doActivateDeactivateBundle(BundleStatus status, boolean activate) { status.setBusy(true); notifyTableRowChanged(status); - ConsoleService console = getTool().getService(ConsoleService.class); - - new TaskLauncher(new Task((activate ? "Activating" : "Deactivating ") + " bundle...") { - @Override - public void run(TaskMonitor monitor) throws CancelledException { - try { - GhidraBundle gb = bundleHost.getExistingGhidraBundle(status.getPath()); - if (activate) { - gb.build(console.getStdErr()); - bundleHost.activateSynchronously(gb.getBundleLocation()); - } - else { // deactivate - bundleHost.deactivateSynchronously(gb.getBundleLocation()); - } - } - catch (Exception e) { - e.printStackTrace(console.getStdErr()); - } - finally { - status.setBusy(false); - notifyTableRowChanged(status); - } - } - }, null, 1000); + new TaskLauncher( + new ActivateDeactivateBundleTask( + (activate ? "Activating" : "Deactivating ") + " bundle...", status, activate), + null, 1000); } private void notifyTableRowChanged(BundleStatus status) { @@ -554,14 +398,159 @@ public class BundleStatusComponentProvider extends ComponentProviderAdapter { /** * This is for testing only! during normal execution, statuses are only added through BundleHostListener bundle(s) added events. * - * each path is marked editable and non-readonly + *

each new bundle will be enabled and writable * - * @param bundlePaths the paths to use + * @param bundleFiles the files to use */ - public void setPathsForTesting(List bundlePaths) { - bundleStatusTableModel.setModelData(bundlePaths.stream() + public void setBundleFilesForTesting(List bundleFiles) { + bundleStatusTableModel.setModelData(bundleFiles.stream() .map(f -> new BundleStatus(f, true, false, null)) .collect(Collectors.toList())); } + private class ActivateBundlesTask extends Task { + private final int[] selectedModelRows; + + private ActivateBundlesTask(String title, boolean canCancel, boolean hasProgress, + boolean isModal, int[] selectedModelRows) { + super(title, canCancel, hasProgress, isModal); + this.selectedModelRows = selectedModelRows; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + // suppress RowObjectSelectionManager repairs until after we're done + bundleStatusTable.chill(); + + List statuses = bundleStatusTableModel.getRowObjects(selectedModelRows) + .stream() + .filter(bs -> !bs.isActive()) + .collect(Collectors.toUnmodifiableList()); + + List bundles = new ArrayList<>(); + for (BundleStatus status : statuses) { + GhidraBundle bundle = bundleHost.getExistingGhidraBundle(status.getFile()); + if (!(bundle instanceof GhidraPlaceholderBundle)) { + status.setBusy(true); + bundleHost.enable(bundle); + bundles.add(bundle); + } + } + notifyTableDataChanged(); + + bundleHost.activateAll(bundles, monitor, + getTool().getService(ConsoleService.class).getStdErr()); + + boolean anybusy = false; + for (BundleStatus status : statuses) { + if (status.isBusy()) { + anybusy = true; + status.setBusy(false); + } + } + if (anybusy) { + notifyTableDataChanged(); + } + + bundleStatusTable.thaw(); + } + } + + private class DeactivateBundlesTask extends Task { + private final int[] selectedModelRows; + + private DeactivateBundlesTask(String title, boolean canCancel, boolean hasProgress, + boolean isModal, int[] selectedModelRows) { + super(title, canCancel, hasProgress, isModal); + this.selectedModelRows = selectedModelRows; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + List bundles = bundleStatusTableModel.getRowObjects(selectedModelRows) + .stream() + .filter(bs -> bs.isActive()) + .map(bs -> bundleHost.getExistingGhidraBundle(bs.getFile())) + .collect(Collectors.toList()); + + monitor.setMaximum(bundles.size()); + for (GhidraBundle bundle : bundles) { + try { + bundleHost.deactivateSynchronously(bundle.getLocationIdentifier()); + } + catch (GhidraBundleException | InterruptedException e) { + ConsoleService console = getTool().getService(ConsoleService.class); + e.printStackTrace(console.getStdErr()); + } + monitor.incrementProgress(1); + } + } + } + + /* + * Activating/deactivating a single bundle doesn't require resolving dependents, + * so this task is slightly different from the others. + */ + private class ActivateDeactivateBundleTask extends Task { + private final BundleStatus status; + private final boolean activate; + + private ActivateDeactivateBundleTask(String title, BundleStatus status, boolean activate) { + super(title); + this.status = status; + this.activate = activate; + } + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + ConsoleService console = getTool().getService(ConsoleService.class); + try { + GhidraBundle bundle = bundleHost.getExistingGhidraBundle(status.getFile()); + if (activate) { + bundle.build(console.getStdErr()); + bundleHost.activateSynchronously(bundle.getLocationIdentifier()); + } + else { // deactivate + bundleHost.deactivateSynchronously(bundle.getLocationIdentifier()); + } + } + catch (Exception e) { + e.printStackTrace(console.getStdErr()); + } + finally { + status.setBusy(false); + notifyTableRowChanged(status); + } + } + } + + /** + * Listener that responds to change requests from the {@link BundleStatusTableModel}. + */ + private class MyBundleStatusChangeRequestListener implements BundleStatusChangeRequestListener { + @Override + public void bundleEnablementChangeRequest(BundleStatus status, boolean enabled) { + GhidraBundle bundle = bundleHost.getExistingGhidraBundle(status.getFile()); + if (bundle instanceof GhidraPlaceholderBundle) { + return; + } + if (enabled) { + bundleHost.enable(bundle); + } + else { + if (status.isActive()) { + doActivateDeactivateBundle(status, false); + } + bundleHost.disable(bundle); + } + } + + @Override + public void bundleActivationChangeRequest(BundleStatus status, boolean newValue) { + if (status.isEnabled()) { + doActivateDeactivateBundle(status, newValue); + } + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java index da06e93306..565b3ec4b1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/BundleStatusTableModel.java @@ -16,46 +16,28 @@ package ghidra.app.plugin.core.osgi; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; +import javax.swing.SwingUtilities; +import javax.swing.event.TableModelEvent; + import docking.widgets.table.AbstractSortedTableModel; import docking.widgets.table.TableSortingContext; import generic.jar.ResourceFile; import ghidra.util.Msg; +import ghidra.util.SystemUtilities; +/** + * Model for {@link BundleStatus} objects. + */ public class BundleStatusTableModel extends AbstractSortedTableModel { List columns = new ArrayList<>(); - class Column { - final Class clazz; - final int index; - final String name; - - Column(String name, Class clazz) { - this.name = name; - this.index = columns.size(); - columns.add(this); - this.clazz = clazz; - } - - boolean editable(BundleStatus status) { - return false; - } - - Object getValue(BundleStatus status) { - return null; - } - - void setValue(BundleStatus status, Object aValue) { - throw new RuntimeException(name + " is not editable!"); - } - - } - Column enabledColumn = new Column("Enabled", Boolean.class) { @Override boolean editable(BundleStatus status) { - return status.pathExists(); + return status.fileExists(); } @Override @@ -71,7 +53,7 @@ public class BundleStatusTableModel extends AbstractSortedTableModel bundleLocToStatusMap = new HashMap<>(); private BundleHostListener bundleHostListener; - private ArrayList bundleStatusListeners = new ArrayList<>(); + private List bundleStatusListeners = + new CopyOnWriteArrayList<>(); private List statuses; - protected class MyBundleHostListener implements BundleHostListener { - @Override - public void bundleBuilt(GhidraBundle bundle, String summary) { - BundleStatus status = getStatus(bundle); - status.setSummary(summary); - int row = getRowIndex(status); - fireTableRowsUpdated(row, row); - } - - @Override - public void bundleActivationChange(GhidraBundle bundle, boolean newActivation) { - BundleStatus status = getStatus(bundle); - int row = getRowIndex(status); - status.setBusy(false); - if (newActivation) { - status.setActive(true); - } - else { - status.setActive(false); - } - fireTableRowsUpdated(row, row); - } - - @Override - public void bundleAdded(GhidraBundle bundle) { - addNewStatus(bundle); - } - - @Override - public void bundlesAdded(Collection bundles) { - int index = statuses.size(); - for (GhidraBundle bundle : bundles) { - addNewStatusNoFire(bundle); - } - fireTableRowsInserted(index, bundles.size() - 1); - } - - @Override - public void bundleRemoved(GhidraBundle bundle) { - BundleStatus status = getStatus(bundle); - removeStatus(status); - } - - @Override - public void bundlesRemoved(Collection bundles) { - List toRemove = bundles.stream() - .map(BundleStatusTableModel.this::getStatus) - .collect(Collectors.toUnmodifiableList()); - removeStatuses(toRemove); - } - - @Override - public void bundleEnablementChange(GhidraBundle bundle, boolean newEnablement) { - BundleStatus status = getStatus(bundle); - status.setEnabled(newEnablement); - int row = getRowIndex(status); - fireTableRowsUpdated(row, row); - } - - @Override - public void bundleException(GhidraBundleException exception) { - BundleStatus status = getStatusFromLoc(exception.getBundleLocation()); - status.setSummary(exception.getMessage()); - int row = getRowIndex(status); - fireTableRowsUpdated(row, row); - } - } - BundleStatusTableModel(BundleStatusComponentProvider provider, BundleHost bundleHost) { super(); this.provider = provider; @@ -195,7 +110,8 @@ public class BundleStatusTableModel extends AbstractSortedTableModel BundleStatusTableModel.super.fireTableChanged(e1)); + } + private void addNewStatusNoFire(GhidraBundle bundle) { - BundleStatus status = new BundleStatus(bundle.getPath(), bundle.isEnabled(), - bundle.isSystemBundle(), bundle.getBundleLocation()); + BundleStatus status = new BundleStatus(bundle.getFile(), bundle.isEnabled(), + bundle.isSystemBundle(), bundle.getLocationIdentifier()); if (statuses.contains(status)) { throw new RuntimeException( - "Bundle status manager already contains " + bundle.getPath().toString()); + "Bundle status manager already contains " + bundle.getFile().toString()); } status.setActive(bundle.isActive()); - bundleLocToStatusMap.put(status.getBundleLocation(), status); + bundleLocToStatusMap.put(status.getLocationIdentifier(), status); statuses.add(status); } @@ -247,10 +173,10 @@ public class BundleStatusTableModel extends AbstractSortedTableModel getColumnClass(int columnIndex) { + public Class getColumnClass(int columnIndex) { return getColumn(columnIndex).clazz; } @@ -341,15 +267,13 @@ public class BundleStatusTableModel extends AbstractSortedTableModelWhen the user requests a change to the status of a bundle, each listener is called. * * @param listener the listener to add */ public void addListener(BundleStatusChangeRequestListener listener) { - synchronized (bundleStatusListeners) { - if (!bundleStatusListeners.contains(listener)) { - bundleStatusListeners.add(listener); - } + if (!bundleStatusListeners.contains(listener)) { + bundleStatusListeners.add(listener); } } @@ -359,24 +283,18 @@ public class BundleStatusTableModel extends AbstractSortedTableModel data, TableSortingContext sortingContext) { + // notify accesses listeners, must be done on swing thread + SystemUtilities.assertThisIsTheSwingThread("Must be called on the Swing thread"); if (sortingContext.isUnsorted()) { // this is the 'no sort' state @@ -409,22 +329,23 @@ public class BundleStatusTableModel extends AbstractSortedTableModel proxy = new Comparator() { - Comparator p = sortingContext.getComparator(); + // wrap the assigned comparator to detect if the order changes + boolean[] changed = { false }; + Comparator wrapper = new Comparator() { + Comparator comparator = sortingContext.getComparator(); @Override public int compare(BundleStatus o1, BundleStatus o2) { - int v = p.compare(o1, o2); - if (v < 0) { - change[0] = true; + int result = comparator.compare(o1, o2); + if (result < 0) { + changed[0] = true; } - return v; + return result; } }; - Collections.sort(data, proxy); + Collections.sort(data, wrapper); sortCompleted(sortingContext); - if (change[0]) { + if (changed[0]) { notifyModelSorted(false); } } @@ -435,8 +356,106 @@ public class BundleStatusTableModel extends AbstractSortedTableModel bundles) { + int index = statuses.size(); + for (GhidraBundle bundle : bundles) { + addNewStatusNoFire(bundle); + } + fireTableRowsInserted(index, bundles.size() - 1); + } + + @Override + public void bundleRemoved(GhidraBundle bundle) { + BundleStatus status = getStatus(bundle); + removeStatus(status); + } + + @Override + public void bundlesRemoved(Collection bundles) { + List toRemove = bundles.stream() + .map(BundleStatusTableModel.this::getStatus) + .collect(Collectors.toUnmodifiableList()); + removeStatuses(toRemove); + } + + @Override + public void bundleEnablementChange(GhidraBundle bundle, boolean newEnablement) { + BundleStatus status = getStatus(bundle); + status.setEnabled(newEnablement); + int row = getRowIndex(status); + fireTableRowsUpdated(row, row); + } + + @Override + public void bundleException(GhidraBundleException exception) { + BundleStatus status = getStatusFromLoc(exception.getBundleLocation()); + status.setSummary(exception.getMessage()); + int row = getRowIndex(status); + fireTableRowsUpdated(row, row); + } + } + + class Column { + final Class clazz; + final int index; + final String name; + + Column(String name, Class clazz) { + this.name = name; + this.index = columns.size(); + columns.add(this); + this.clazz = clazz; + } + + boolean editable(BundleStatus status) { + return false; + } + + Object getValue(BundleStatus status) { + return null; + } + + void setValue(BundleStatus status, Object aValue) { + throw new RuntimeException(name + " is not editable!"); + } + + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java index 496bb71717..7cfee06ccd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundle.java @@ -29,15 +29,16 @@ import generic.jar.ResourceFile; */ public abstract class GhidraBundle { - protected final ResourceFile path; + protected final ResourceFile file; + protected final BundleHost bundleHost; protected boolean enabled; protected boolean systemBundle; - GhidraBundle(BundleHost bundleHost, ResourceFile bundlePath, boolean enabled, + GhidraBundle(BundleHost bundleHost, ResourceFile bundleFile, boolean enabled, boolean systemBundle) { this.bundleHost = bundleHost; - this.path = bundlePath; + this.file = bundleFile; this.enabled = enabled; this.systemBundle = systemBundle; } @@ -59,29 +60,36 @@ public abstract class GhidraBundle { 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 + * @throws Exception if the build cannot complete */ - @SuppressWarnings("javadoc") public boolean build() throws Exception { return build(new PrintWriter(System.err)); } /** * Return the location identifier of the bundle that this GhidraBundle represents. - * The location identifier is passed to {@link org.osgi.framework.BundleContext#installBundle} when this - * bundle is installed. + * + *

The location identifier is used by the framework, e.g. it is passed to + * {@link org.osgi.framework.BundleContext#installBundle} when the bundle is + * first installed. + * + *

Although the bundle location is a URI, outside of interactions with the framework, + * the bundle location should remain opaque. * * @return location identifier of this bundle */ - public abstract String getBundleLocation(); + public abstract String getLocationIdentifier(); abstract List getAllRequirements(); /** - * @return this bundle's path + * @return the file where this bundle is loaded from */ - public ResourceFile getPath() { - return path; + public ResourceFile getFile() { + return file; } /** @@ -94,7 +102,7 @@ public abstract class GhidraBundle { /** * set the enablement flag for this bundle. * - * If a bundle is enabled its contents will be scanned, e.g. for scripts. + *

If a bundle is enabled its contents will be scanned, e.g. for scripts. * * @param enabled new state */ @@ -112,81 +120,74 @@ public abstract class GhidraBundle { } /** - * A GhidraBundle can be - *

    - *
  • a Bndtools .bnd script
  • - *
  • an OSGi bundle .jar file
  • - *
  • a directory of Java source
  • - * - * - */ - enum Type { - BndScript, Jar, SourceDir, INVALID - } - - /** - * a string error with a time stamp - */ - public static class BuildFailure { - long when = -1; - StringBuilder message = new StringBuilder(); - } - - /** - * Get the type of a GhidraBundle from its path. + * Get the type of a GhidraBundle from its file. * - * @param path a resource path + * @param file a bundle file * @return the type */ - static GhidraBundle.Type getType(ResourceFile path) { - if (path.isDirectory()) { - return GhidraBundle.Type.SourceDir; + static GhidraBundle.Type getType(ResourceFile file) { + if (file.isDirectory()) { + return GhidraBundle.Type.SOURCE_DIR; } - String n = path.getName().toLowerCase(); - if (n.endsWith(".bnd")) { - return GhidraBundle.Type.BndScript; + String fileName = file.getName().toLowerCase(); + if (fileName.endsWith(".bnd")) { + return GhidraBundle.Type.BND_SCRIPT; } - if (n.endsWith(".jar")) { - return GhidraBundle.Type.Jar; + if (fileName.endsWith(".jar")) { + return GhidraBundle.Type.JAR; } return GhidraBundle.Type.INVALID; } /** - * Get the type of a GhidraBundle from its path. + * Get the type of a GhidraBundle from its file. * - * @param path a file system path + * @param file a bundle file * @return the type */ - public static GhidraBundle.Type getType(File path) { - if (path.isDirectory()) { - return GhidraBundle.Type.SourceDir; + public static GhidraBundle.Type getType(File file) { + if (file.isDirectory()) { + return GhidraBundle.Type.SOURCE_DIR; } - String n = path.getName().toLowerCase(); - if (n.endsWith(".bnd")) { - return GhidraBundle.Type.BndScript; + String fileName = file.getName().toLowerCase(); + if (fileName.endsWith(".bnd")) { + return GhidraBundle.Type.BND_SCRIPT; } - if (n.endsWith(".jar")) { - return GhidraBundle.Type.Jar; + if (fileName.endsWith(".jar")) { + return GhidraBundle.Type.JAR; } return GhidraBundle.Type.INVALID; } /** - * Get the OSGi bundle respresented by this GhidraBundle or null + * Get the OSGi bundle represented by this GhidraBundle or null if it isn't in + * the "installed" state. * * @return a Bundle or null */ public Bundle getOSGiBundle() { - return bundleHost.getOSGiBundle(getBundleLocation()); + return bundleHost.getOSGiBundle(getLocationIdentifier()); } /** * @return true if this bundle is active */ public boolean isActive() { - Bundle b = getOSGiBundle(); - return (b != null) && b.getState() == Bundle.ACTIVE; + Bundle bundle = getOSGiBundle(); + return (bundle != null) && bundle.getState() == Bundle.ACTIVE; + } + + /** + * A GhidraBundle can be + *
      + *
    • a Bndtools .bnd script
    • + *
    • an OSGi bundle .jar file
    • + *
    • a directory of Java source
    • + *
    + * + */ + enum Type { + BND_SCRIPT, JAR, SOURCE_DIR, INVALID } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundleException.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundleException.java index 141547252d..f36521f83f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundleException.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraBundleException.java @@ -20,12 +20,15 @@ import java.util.stream.Collectors; import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; +/** + * {@link GhidraBundleException}s store the context associated with exceptions thrown during bundle operations. + */ public class GhidraBundleException extends OSGiException { private final Bundle bundle; private final String bundleLocation; /** - * {@link GhidraBundleException}s store the context associated with exceptions thrown during bundle operations. + * Construct a new exception originating with {@code bundle}. * * @param bundle the bundle (if available) * @param msg a contextual message @@ -38,7 +41,7 @@ public class GhidraBundleException extends OSGiException { } /** - * {@link GhidraBundleException}s store the context associated with exceptions thrown during bundle operations. + * Construct a new exception originating with the bundle having location identifier {@code bundleLocation}. * * @param bundleLocation the bundle location identifier (since no bundle is available) * @param msg a contextual message @@ -71,73 +74,56 @@ public class GhidraBundleException extends OSGiException { return ""; } if (e instanceof BundleException) { - BundleException be = (BundleException) e; - switch (be.getType()) { + BundleException bundleException = (BundleException) e; + switch (bundleException.getType()) { default: return "No exception type"; case BundleException.UNSPECIFIED: return "UNSPECIFIED"; - /** - * The operation was unsupported. This type can be used anywhere a - * BundleException can be thrown. - */ + // The operation was unsupported. This type can be used anywhere a BundleException can be thrown. case BundleException.UNSUPPORTED_OPERATION: return "UNSUPPORTED_OPERATION"; - /** - * The operation was invalid. - */ + // The operation was invalid. case BundleException.INVALID_OPERATION: return "INVALID_OPERATION"; - /** - * The bundle manifest was in error. - */ + // The bundle manifest was in error. case BundleException.MANIFEST_ERROR: return "MANIFEST_ERROR"; - /** - * The bundle was not resolved. - */ + // The bundle was not resolved. case BundleException.RESOLVE_ERROR: { - String message = be.getMessage(); + String message = bundleException.getMessage(); if (message.startsWith("Unable to acquire global lock")) { return message; } // parse the package constraints from filters in the BundleRequirement string - String packages = OSGiUtils.extractPackageNamesFromFailedResolution(be.getMessage()) - .stream() - .distinct() - .collect(Collectors.joining("\n")); + String packages = + OSGiUtils.extractPackageNamesFromFailedResolution(bundleException.getMessage()) + .stream() + .distinct() + .collect(Collectors.joining("\n")); return "RESOLVE_ERROR with reference to packages:\n" + packages; } - /** - * The bundle activator was in error. - */ + // The bundle activator was in error. case BundleException.ACTIVATOR_ERROR: return "ACTIVATOR_ERROR"; - /** - * The operation failed due to insufficient permissions. - */ + // The operation failed due to insufficient permissions. case BundleException.SECURITY_ERROR: return "SECURITY_ERROR"; - /** - * The operation failed to complete the requested lifecycle state change. - */ + // The operation failed to complete the requested lifecycle state change. case BundleException.STATECHANGE_ERROR: return "STATECHANGE_ERROR"; - /** - * The bundle could not be resolved due to an error with the - * Bundle-NativeCode header. - */ + // The bundle could not be resolved due to an error with the Bundle-NativeCode header. case BundleException.NATIVECODE_ERROR: return "NATIVECODE_ERROR"; - /** + /* * The install or update operation failed because another already installed * bundle has the same symbolic name and version. This exception type will * only occur if the framework is configured to only allow a single bundle @@ -148,25 +134,16 @@ public class GhidraBundleException extends OSGiException { case BundleException.DUPLICATE_BUNDLE_ERROR: return "DUPLICATE_BUNDLE_ERROR"; - /** - * The start transient operation failed because the start level of the - * bundle is greater than the current framework start level - */ + // The start transient operation failed because the start level of the bundle + // is greater than the current framework start level case BundleException.START_TRANSIENT_ERROR: return "START_TRANSIENT_ERROR"; - /** - * The framework received an error while reading the input stream for a - * bundle. - */ + // The framework received an error while reading the input stream for a bundle. case BundleException.READ_ERROR: return "READ_ERROR"; - /** - * A framework hook rejected the operation. - * - * @since 1.6 - */ + // A framework hook rejected the operation. case BundleException.REJECTED_BY_HOOK: return "REJECTED_BY_HOOK"; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java index eec75b2251..a1b9be400d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraJarBundle.java @@ -36,14 +36,14 @@ public class GhidraJarBundle extends GhidraBundle { * {@link GhidraJarBundle} wraps an ordinary OSGi bundle .jar. * * @param bundleHost the {@link BundleHost} instance this bundle will belong to - * @param path the jar file's path + * @param file the jar file * @param enabled true to start enabled * @param systemBundle true if this is a Ghidra system bundle */ - public GhidraJarBundle(BundleHost bundleHost, ResourceFile path, boolean enabled, + public GhidraJarBundle(BundleHost bundleHost, ResourceFile file, boolean enabled, boolean systemBundle) { - super(bundleHost, path, enabled, systemBundle); - this.bundleLocation = "file://" + path.getAbsolutePath().toString(); + super(bundleHost, file, enabled, systemBundle); + this.bundleLocation = "file://" + file.getAbsolutePath().toString(); } @Override @@ -57,19 +57,17 @@ public class GhidraJarBundle extends GhidraBundle { } @Override - public String getBundleLocation() { + public String getLocationIdentifier() { return bundleLocation; } @Override public List getAllRequirements() { - Jar jar; - try { - jar = new Jar(path.getFile(true)); - Manifest m = jar.getManifest(); - String imps = m.getMainAttributes().getValue(Constants.IMPORT_PACKAGE); - if (imps != null) { - return OSGiUtils.parseImports(imps); + try (Jar jar = new Jar(file.getFile(true))) { + Manifest manifest = jar.getManifest(); + String importPackageString = manifest.getMainAttributes().getValue(Constants.IMPORT_PACKAGE); + if (importPackageString != null) { + return OSGiUtils.parseImportPackage(importPackageString); } return Collections.emptyList(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraPlaceholderBundle.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraPlaceholderBundle.java index 28b027c0e7..0f9d6aad02 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraPlaceholderBundle.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraPlaceholderBundle.java @@ -28,9 +28,9 @@ import generic.jar.ResourceFile; */ public class GhidraPlaceholderBundle extends GhidraBundle { - GhidraPlaceholderBundle(BundleHost bundleHost, ResourceFile bundlePath, boolean isEnabled, + GhidraPlaceholderBundle(BundleHost bundleHost, ResourceFile bundleFile, boolean isEnabled, boolean isSystemBundle) { - super(bundleHost, bundlePath, isEnabled, isSystemBundle); + super(bundleHost, bundleFile, isEnabled, isSystemBundle); } @Override @@ -44,8 +44,8 @@ public class GhidraPlaceholderBundle extends GhidraBundle { } @Override - public String getBundleLocation() { - return "invalid://" + getPath(); + public String getLocationIdentifier() { + return "invalid://" + getFile(); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java index d3277b0ff0..f9b3afd25b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/GhidraSourceBundle.java @@ -48,11 +48,14 @@ import ghidra.util.Msg; /** * {@link GhidraSourceBundle} represents a Java source directory that is compiled on build to an OSGi bundle. * - * A manifest and BundleActivator are generated if not already present. + *

    A manifest and {@link BundleActivator} are generated if not already present. + */ +/** + * */ public class GhidraSourceBundle extends GhidraBundle { private static final String GENERATED_ACTIVATOR_CLASSNAME = "GeneratedActivator"; - private static final Predicate isClassFile = + private static final Predicate IS_CLASS_FILE = Pattern.compile("(\\$.*)?\\.class", Pattern.CASE_INSENSITIVE).asMatchPredicate(); protected interface DiscrepencyCallback { @@ -61,27 +64,27 @@ public class GhidraSourceBundle extends GhidraBundle { * corresponding class file(s), {@code classFiles} * * @param sourceFile the source file or null to indicate the class files have no corresponding source - * @param classFiles corresponding class files(s) + * @param classFiles corresponding class file(s) * @throws Throwable an exception */ void found(ResourceFile sourceFile, Collection classFiles) throws Throwable; } private final String symbolicName; - private final Path binDir; + private final Path binaryDir; private final String bundleLoc; private final List newSources = new ArrayList<>(); - private final List oldBin = new ArrayList<>(); + private final List oldBinaries = new ArrayList<>(); private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //// information indexed by source file - private final HashMap buildErrors = new HashMap<>(); - private final HashMap> sourceFileToRequirements = + private final Map buildErrors = new HashMap<>(); + private final Map> sourceFileToRequirements = new HashMap<>(); - private final HashMap> requirementToSourceFileMap = new HashMap<>(); + private final Map> requirementToSourceFileMap = new HashMap<>(); private long lastCompileAttempt; @@ -97,10 +100,18 @@ public class GhidraSourceBundle extends GhidraBundle { boolean systemBundle) { super(bundleHost, sourceDirectory, enabled, systemBundle); - this.symbolicName = GhidraSourceBundle.sourceDirHash(path); - this.binDir = GhidraSourceBundle.getCompiledBundlesDir().resolve(symbolicName); + this.symbolicName = GhidraSourceBundle.sourceDirHash(getSourceDirectory()); + this.binaryDir = GhidraSourceBundle.getCompiledBundlesDir().resolve(symbolicName); - this.bundleLoc = "reference:file://" + binDir.toAbsolutePath().normalize().toString(); + this.bundleLoc = "reference:file://" + binaryDir.toAbsolutePath().normalize().toString(); + } + + /** + * (alias of {@link #getFile} for readability) + * @return the source directory this bundle represents + */ + protected final ResourceFile getSourceDirectory() { + return file; } /** @@ -119,8 +130,8 @@ public class GhidraSourceBundle extends GhidraBundle { * When a source bundle doesn't have a manifest, Ghidra computes the bundle's * symbolic name as a hash of the source directory path. * - * The hash is is also used as the final path component of the compile destination:
    - *  {@code $USERHOME/.ghidra/.ghidra_/osgi/compiled-bundles/ } + *

    This hash is also used as the final path component of the compile destination: + *
     {@code $USERHOME/.ghidra/.ghidra_/osgi/compiled-bundles/ } * * @param sourceDir the source directory * @return a string hash of the source directory path @@ -144,17 +155,19 @@ public class GhidraSourceBundle extends GhidraBundle { } /** - * Returen the class name corresponding to a script in this source bundle. + * Return the class name corresponding to a script in this source bundle. * * @param sourceFile a source file from this bundle * @return the class name */ public String classNameForScript(ResourceFile sourceFile) { - String p; try { - p = sourceFile.getCanonicalPath(); - p = p.substring(1 + path.getCanonicalPath().length(), p.length() - 5);// relative path less ".java" - return p.replace(File.separatorChar, '.'); + String path = sourceFile.getCanonicalPath(); + + // get the relative path and chop ".java" from the end + path = path.substring(1 + getSourceDirectory().getCanonicalPath().length(), + path.length() - 5); + return path.replace(File.separatorChar, '.'); } catch (IOException e) { e.printStackTrace(); @@ -171,63 +184,63 @@ public class GhidraSourceBundle extends GhidraBundle { * @param sourceFile the file w/ errors * @param err an error string */ - void buildError(ResourceFile sourceFile, String err) { - GhidraBundle.BuildFailure f = - buildErrors.computeIfAbsent(sourceFile, x -> new GhidraBundle.BuildFailure()); - f.when = sourceFile.lastModified(); - f.message.append(err); + protected void buildError(ResourceFile sourceFile, String err) { + BuildError error = buildErrors.computeIfAbsent(sourceFile, BuildError::new); + error.append(err); } /** * get any errors associated with building the given source file. * * @param sourceFile the source file - * @return a {@link GhidraBundle.BuildFailure} object + * @return a {@link BuildError} object */ - public GhidraBundle.BuildFailure getErrors(ResourceFile sourceFile) { + public BuildError getErrors(ResourceFile sourceFile) { return buildErrors.get(sourceFile); } private String getPreviousBuildErrors() { return buildErrors.values() .stream() - .map(e -> e.message.toString()) + .map(BuildError::getMessage) .collect(Collectors.joining()); } - private String parseImports(ResourceFile javaSource) { + private String parseImportPackageMetadata(ResourceFile javaSource) { return GhidraScriptUtil.newScriptInfo(javaSource).getImportPackage(); } /** - * update buildReqs based on \@importpackages tag in java files in the default(unnamed) package + * update buildReqs based on \@importpackage tag in java files in the default(unnamed) package * - * @throws GhidraBundleException on failure to parse the \@importpackages tag + * @throws GhidraBundleException on failure to parse the \@importpackage tag */ private void updateRequirementsFromMetadata() throws GhidraBundleException { sourceFileToRequirements.clear(); requirementToSourceFileMap.clear(); - for (ResourceFile file : path.listFiles()) { - if (file.getName().endsWith(".java")) { - // without GhidraScriptComponentProvider.updateAvailableScriptFilesForDirectory, or GhidraScriptComponentProvider.newScript - // this might be the earliest need for ScriptInfo, so allow construction. + for (ResourceFile rootSourceFile : getSourceDirectory().listFiles()) { + if (rootSourceFile.getName().endsWith(".java")) { + // without GhidraScriptComponentProvider.updateAvailableScriptFilesForDirectory, + // or GhidraScriptComponentProvider.newScript this might be the earliest need for + // ScriptInfo, so allow construction. // NB: ScriptInfo will update field values if lastModified has changed since last time they were computed - String imps = parseImports(file); - if (imps != null && !imps.isEmpty()) { + String importPackage = parseImportPackageMetadata(rootSourceFile); + if (importPackage != null && !importPackage.isEmpty()) { List requirements; try { - requirements = OSGiUtils.parseImports(imps); + requirements = OSGiUtils.parseImportPackage(importPackage); } catch (BundleException e) { - throw new GhidraBundleException(getBundleLocation(), "parsing manifest", e); + throw new GhidraBundleException(getLocationIdentifier(), "parsing manifest", + e); } - sourceFileToRequirements.put(file, requirements); + sourceFileToRequirements.put(rootSourceFile, requirements); for (BundleRequirement requirement : requirements) { requirementToSourceFileMap .computeIfAbsent(requirement.toString(), x -> new ArrayList<>()) - .add(file); + .add(rootSourceFile); } } } @@ -254,12 +267,12 @@ public class GhidraSourceBundle extends GhidraBundle { } Map reqs = getComputedReqs(); // insert requirements from a source manifest - ResourceFile manifest = getSourceManifestPath(); - if (manifest.exists()) { + ResourceFile manifestFile = getSourceManifestFile(); + if (manifestFile.exists()) { try { - Manifest m = new Manifest(manifest.getInputStream()); - String imports = m.getMainAttributes().getValue("Import-Package"); - for (BundleRequirement r : OSGiUtils.parseImports(imports)) { + Manifest manifest = new Manifest(manifestFile.getInputStream()); + String importPackage = manifest.getMainAttributes().getValue("Import-Package"); + for (BundleRequirement r : OSGiUtils.parseImportPackage(importPackage)) { reqs.putIfAbsent(r.toString(), r); } } @@ -273,54 +286,68 @@ public class GhidraSourceBundle extends GhidraBundle { /** * look for new sources, metadata, manifest file. * - * if files had errors last time, haven't changed, and no new requirements are available, remove them. + *

    if files had errors last time, haven't changed, and no new requirements are available, remove them. * * @param writer for reporting status to user * @throws IOException while accessing manifest file * @throws OSGiException while parsing imports */ - void updateNewSourceOldBinFromFilesystem(PrintWriter writer) throws IOException, OSGiException { + void updateFromFilesystem(PrintWriter writer) throws IOException, OSGiException { // look for new source files newSources.clear(); - oldBin.clear(); + oldBinaries.clear(); - visitDiscrepencies((sf, bfs) -> { - if (sf != null) { - newSources.add(sf); + visitDiscrepancies((sourceFile, classFiles) -> { + // sourceFile is either newer than its corresponding class files, + // or there are no corresponding class files (meaning it's new), + // or sourceFile=null and classFiles had no corresponding source + if (sourceFile != null) { + // these will be (re)compiled + newSources.add(sourceFile); } - if (bfs != null) { - oldBin.addAll(bfs); + if (classFiles != null) { + // these will be deleted + oldBinaries.addAll(classFiles); } }); - // don't rebuild source files that failed last time and haven't changed - Iterator it = newSources.iterator(); - while (it.hasNext()) { - ResourceFile sf = it.next(); - GhidraBundle.BuildFailure f = buildErrors.get(sf); - if (f != null) { - if (f.when == sf.lastModified()) { - it.remove(); - continue; - } - // it's either new or worth trying again - buildErrors.remove(sf); + // we don't want to rebuild source files that had errors last time and haven't changed, + // so remove them from newSources. Also remove old error messages. + Iterator newSourceIterator = newSources.iterator(); + while (newSourceIterator.hasNext()) { + ResourceFile newSourceFile = newSourceIterator.next(); + if (stillHasErrors(newSourceFile)) { + newSourceIterator.remove(); + } + else { + // any errors are old, so remove them + buildErrors.remove(newSourceFile); } } } + private boolean stillHasErrors(ResourceFile newSourceFile) { + BuildError error = buildErrors.get(newSourceFile); + if (error != null) { + if (error.getLastModified() == newSourceFile.lastModified()) { + return true; + } + } + return false; + } + private void deleteOldBinaries() throws IOException { // dedupe and omit files that don't exist - oldBin.sort(null); - Iterable paths = () -> oldBin.stream().distinct().filter(Files::exists).iterator(); + oldBinaries.sort(null); + Iterable paths = + () -> oldBinaries.stream().distinct().filter(Files::exists).iterator(); - for (Path bf : paths) { - Files.delete(bf); + for (Path path : paths) { + Files.delete(path); } - // oldBin.clear(); } - int getFailingSourcesCount() { + int getBuildErrorCount() { return buildErrors.size(); } @@ -345,20 +372,24 @@ public class GhidraSourceBundle extends GhidraBundle { } @Override - public String getBundleLocation() { + public String getLocationIdentifier() { return bundleLoc; } - ResourceFile getSourceManifestPath() { - return new ResourceFile(path, "META-INF" + File.separator + "MANIFEST.MF"); + ResourceFile getSourceManifestFile() { + return new ResourceFile(getSourceDirectory(), "META-INF" + File.separator + "MANIFEST.MF"); } - boolean hasNewManifest() throws IOException { - ResourceFile smf = getSourceManifestPath(); - Path dmf = binDir.resolve("META-INF").resolve("MANIFEST.MF"); + private Path getBinaryManifestPath() { + return binaryDir.resolve("META-INF").resolve("MANIFEST.MF"); + } - return smf.exists() && (Files.notExists(dmf) || - smf.lastModified() > Files.getLastModifiedTime(dmf).toMillis()); + boolean hasNewManifest() { + ResourceFile sourceManfiest = getSourceManifestFile(); + Path binaryManifest = getBinaryManifestPath(); + + return sourceManfiest.exists() && (Files.notExists(binaryManifest) || + sourceManfiest.lastModified() > binaryManifest.toFile().lastModified()); } protected static boolean wipeContents(Path path) throws IOException { @@ -374,7 +405,50 @@ public class GhidraSourceBundle extends GhidraBundle { } private boolean wipeBinDir() throws IOException { - return wipeContents(binDir); + return wipeContents(binaryDir); + } + + /** + * if source with a previous build error now resolves, add it to newSources. + * + *

    The reason for the previous build error isn't necessarily a missing requirement, + * but this shouldn't be too expensive. + */ + private void buildIfPreviosErrorNowResolves() { + for (ResourceFile sourceFile : buildErrors.keySet()) { + List requirements = sourceFileToRequirements.get(sourceFile); + if (requirements != null && !requirements.isEmpty() && + bundleHost.canResolveAll(requirements)) { + if (!newSources.contains(sourceFile)) { + newSources.add(sourceFile); + } + for (ResourceFile oldbin : correspondingBinaries(sourceFile)) { + oldbin.delete(); + } + } + } + } + + /** + * if a file that previously built without errors is now missing some requirements, + * rebuild it to capture errors (if any). + */ + void buildIfRequirementsChanged() { + // if previous successes no longer resolve, (cleanup) and try again + for (Entry> e : sourceFileToRequirements.entrySet()) { + ResourceFile sourceFile = e.getKey(); + List requirements = e.getValue(); + if (requirements != null && !requirements.isEmpty() && + !buildErrors.containsKey(sourceFile) && !bundleHost.canResolveAll(requirements)) { + if (!newSources.contains(sourceFile)) { + newSources.add(sourceFile); + } + + Arrays.stream(correspondingBinaries(sourceFile)) + .map(rf -> rf.getFile(false).toPath()) + .forEach(oldBinaries::add); + } + } } @Override @@ -391,44 +465,20 @@ public class GhidraSourceBundle extends GhidraBundle { wipeBinDir(); } - updateNewSourceOldBinFromFilesystem(writer); + updateFromFilesystem(writer); updateRequirementsFromMetadata(); - // if previous failures now resolve, try again - for (ResourceFile sf : buildErrors.keySet()) { - List reqs = sourceFileToRequirements.get(sf); - if (reqs != null && !reqs.isEmpty() && bundleHost.canResolveAll(reqs)) { - if (!newSources.contains(sf)) { - newSources.add(sf); - } - for (ResourceFile oldbin : correspondingBinaries(sf)) { - oldbin.delete(); - } - } - } - // if previous successes no longer resolve, (cleanup) and try again - for (Entry> e : sourceFileToRequirements.entrySet()) { - ResourceFile sf = e.getKey(); - List reqs = e.getValue(); - if (reqs != null && !reqs.isEmpty() && !buildErrors.containsKey(sf) && - !bundleHost.canResolveAll(reqs)) { - if (!newSources.contains(sf)) { - newSources.add(sf); - } + buildIfPreviosErrorNowResolves(); + buildIfRequirementsChanged(); - Arrays.stream(correspondingBinaries(sf)) - .map(rf -> rf.getFile(false).toPath()) - .forEach(oldBin::add); - } - } - - int failuresLastTime = getFailingSourcesCount(); + int buildErrorsLastTime = getBuildErrorCount(); int newSourceCount = getNewSourcesCount(); if (newSourceCount == 0) { - if (failuresLastTime > 0) { + if (buildErrorsLastTime > 0) { writer.printf("%s hasn't changed, with %d file%s failing in previous build(s):\n", - path.toString(), failuresLastTime, failuresLastTime > 1 ? "s" : ""); + getSourceDirectory().toString(), buildErrorsLastTime, + buildErrorsLastTime > 1 ? "s" : ""); writer.printf("%s\n", getPreviousBuildErrors()); } } @@ -437,15 +487,15 @@ public class GhidraSourceBundle extends GhidraBundle { } // finally, we should be able to handle empty directories - if (!binDir.toFile().exists()) { + if (!binaryDir.toFile().exists()) { needsCompile = true; } if (needsCompile) { // if there is a bundle at our locations, uninstall it - Bundle b = bundleHost.getOSGiBundle(getBundleLocation()); - if (b != null) { - bundleHost.deactivateSynchronously(b); + Bundle osgiBundle = bundleHost.getOSGiBundle(getLocationIdentifier()); + if (osgiBundle != null) { + bundleHost.deactivateSynchronously(osgiBundle); } // once we've committed to recompile and regenerate generated classes, delete the old stuff @@ -467,9 +517,9 @@ public class GhidraSourceBundle extends GhidraBundle { } try { - Bundle b = getOSGiBundle(); - if (b != null) { - bundleHost.deactivateSynchronously(b); + Bundle bundle = getOSGiBundle(); + if (bundle != null) { + bundleHost.deactivateSynchronously(bundle); } return anythingChanged | wipeBinDir(); } @@ -482,29 +532,32 @@ public class GhidraSourceBundle extends GhidraBundle { private ResourceFile[] correspondingBinaries(ResourceFile source) { String parentPath = source.getParentFile().getAbsolutePath(); - String relpath = parentPath.substring(path.getAbsolutePath().length()); - if (relpath.startsWith(File.separator)) { - relpath = relpath.substring(1); + String relPath = parentPath.substring(getSourceDirectory().getAbsolutePath().length()); + if (relPath.startsWith(File.separator)) { + relPath = relPath.substring(1); } - String n0 = source.getName(); - final String n = n0.substring(0, n0.length() - 5);// trim .java - ResourceFile bp = new ResourceFile(binDir.resolve(relpath).toFile()); - if (!bp.exists() || !bp.isDirectory()) { + String className0 = source.getName(); + String className = className0.substring(0, className0.length() - 5);// drop ".java" + ResourceFile binarySubdir = new ResourceFile(binaryDir.resolve(relPath).toFile()); + if (!binarySubdir.exists() || !binarySubdir.isDirectory()) { return new ResourceFile[] {}; } - return bp.listFiles(f -> { - String nn = f.getName(); - return nn.startsWith(n) && isClassFile.test(nn.substring(n.length())); + return binarySubdir.listFiles(f -> { + String fileName = f.getName(); + return fileName.startsWith(className) && + IS_CLASS_FILE.test(fileName.substring(className.length())); }); } /** + * visit discrepancies between java source and corresponding class files. + * *

     	 * walk resources to find:
     	 *  - source files that are newer than their corresponding binary
     	 *		reports (source file, list of corresponding binaries)
     	 * 	- source files with no corresponding binary
    -	 *		reports (source file, )
    +	 *		reports (source file, empty list)
     	 *  - binary files with no corresponding source
     	 *  	reports (null, list of binary files)
     	 *  
    @@ -520,208 +573,138 @@ public class GhidraSourceBundle extends GhidraBundle {
     	 * 
    * @param cb callback */ - protected void visitDiscrepencies(DiscrepencyCallback cb) { + protected void visitDiscrepancies(DiscrepencyCallback cb) { try { Deque stack = new ArrayDeque<>(); - stack.add(path); + // start in the source directory root + stack.add(getSourceDirectory()); + while (!stack.isEmpty()) { - ResourceFile sd = stack.pop(); - String relpath = sd.getAbsolutePath().substring(path.getAbsolutePath().length()); - if (relpath.startsWith(File.separator)) { - relpath = relpath.substring(1); + ResourceFile sourceSubdir = stack.pop(); + String relPath = sourceSubdir.getAbsolutePath() + .substring(getSourceDirectory().getAbsolutePath().length()); + if (relPath.startsWith(File.separator)) { + relPath = relPath.substring(1); } - Path bd = binDir.resolve(relpath); + Path binarySubdir = binaryDir.resolve(relPath); - // index the class files in the corresponding directory by basename - Map> binfiles = Files.exists(bd) ? Files.list(bd) - .filter(x -> Files.isRegularFile(x) && - x.getFileName().toString().endsWith(".class")) - .collect(groupingBy(x -> { - String s = x.getFileName().toString(); - int money = s.indexOf('$'); - if (money >= 0) { - return s.substring(0, money); - } - return s.substring(0, s.length() - 6); - })) : Collections.emptyMap(); + ClassMapper mapper = new ClassMapper(binarySubdir); - for (ResourceFile sf : sd.listFiles()) { - if (sf.isDirectory()) { - stack.push(sf); + // for each source file, lookup class files by class name + for (ResourceFile sourceFile : sourceSubdir.listFiles()) { + if (sourceFile.isDirectory()) { + stack.push(sourceFile); } else { - String n = sf.getName(); - if (n.endsWith(".java")) { - long sourceLastModified = sf.lastModified(); - List bfs = binfiles.remove(n.substring(0, n.length() - 5)); - long binaryLastModified = (bfs == null || bfs.isEmpty()) ? -1 - : bfs.stream() - .mapToLong(bf -> bf.toFile().lastModified()) - .min() - .getAsLong(); - // if source is newer than the oldest binary, report - if (sourceLastModified > binaryLastModified) { - cb.found(sf, bfs); - } + List classFiles = mapper.findAndRemove(sourceFile); + if (classFiles != null) { + cb.found(sourceFile, classFiles); } } } // any remaining .class files are missing .java files - if (!binfiles.isEmpty()) { - cb.found(null, - binfiles.values() - .stream() - .flatMap(l -> l.stream()) - .collect(Collectors.toList())); + if (mapper.hasExtraClassFiles()) { + cb.found(null, mapper.extraClassFiles()); } } } - catch (Throwable t) { - t.printStackTrace(); + catch (Throwable e) { + e.printStackTrace(); } } - private static class Summary { - static String SEP = ", "; - final StringWriter sw = new StringWriter(); - final PrintWriter pw = new PrintWriter(sw, true); - - void printf(String format, Object... args) { - if (sw.getBuffer().length() > 0) { - pw.write(SEP); - } - pw.printf(format, args); - } - - void print(String arg) { - pw.print(arg); - } - - String getValue() { - pw.flush(); - return sw.getBuffer().toString(); - } - - } - - /** - * compile a source directory to an exploded bundle - * - * @param writer for updating the user during compilation - * @throws IOException for source/manifest file reading/generation and binary deletion/creation - * @throws OSGiException if generation of bundle metadata fails + /* + * when calling the java compiler programmatically, we map import requests to files with + * a custom {@link JavaFileManager}. We wrap the system JavaFileManager with one that + * handles ResourceFiles then wrap that with Phidia, which handles imports based on + * bundle requirements. */ - private String compileToExplodedBundle(PrintWriter writer) throws IOException, OSGiException { - compileAttempted(); - - Files.createDirectories(binDir); - - Summary summary = new Summary(); - - List options = new ArrayList<>(); - options.add("-g"); - options.add("-d"); - options.add(binDir.toString()); - options.add("-sourcepath"); - options.add(path.toString()); - options.add("-classpath"); - options.add(System.getProperty("java.class.path") + File.pathSeparator + binDir.toString()); - options.add("-proc:none"); - - final ResourceFileJavaFileManager resourceFileJavaManager = - new ResourceFileJavaFileManager(Collections.singletonList(path), buildErrors.keySet()); + private BundleJavaManager createBundleJavaManager(PrintWriter writer, Summary summary, + List options) throws IOException { + final ResourceFileJavaFileManager resourceFileJavaManager = new ResourceFileJavaFileManager( + Collections.singletonList(getSourceDirectory()), buildErrors.keySet()); BundleJavaManager bundleJavaManager = new BundleJavaManager(bundleHost.getHostFramework(), resourceFileJavaManager, options); + // 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 - // non-existing bundle, so we forge the wiring based on @importpackages metadata. + // non-existing bundle, so we forge the wiring based on @importpackage metadata. - // XXX skip this if there's a source manifest, emit warnings about @importpackages + // XXX skip this if there's a source manifest, emit warnings about @importpackage // get wires for currently active bundles to satisfy all requirements - List reqs = getAllRequirements(); - List bundleWirings = bundleHost.resolve(reqs); + List requirements = getAllRequirements(); + List bundleWirings = bundleHost.resolve(requirements); - if (!reqs.isEmpty()) { - writer.printf("%d import requirement%s remain%s unresolved:\n", reqs.size(), - reqs.size() > 1 ? "s" : "", reqs.size() > 1 ? "" : "s"); - for (BundleRequirement req : reqs) { - writer.printf(" %s\n", req.toString()); + if (!requirements.isEmpty()) { + writer.printf("%d import requirement%s remain%s unresolved:\n", requirements.size(), + requirements.size() > 1 ? "s" : "", requirements.size() > 1 ? "" : "s"); + for (BundleRequirement requirement : requirements) { + writer.printf(" %s\n", requirement.toString()); } - summary.printf("%d missing @import%s:%s", reqs.size(), reqs.size() > 1 ? "s" : "", - reqs.stream() - .flatMap(r -> OSGiUtils.extractPackageNamesFromFailedResolution(r.toString()).stream()) + summary.printf("%d missing @importpackage%s:%s", requirements.size(), + requirements.size() > 1 ? "s" : "", + requirements.stream() + .flatMap(r -> OSGiUtils.extractPackageNamesFromFailedResolution(r.toString()) + .stream()) .distinct() .collect(Collectors.joining(","))); } // send the capabilities to phidias bundleWirings.forEach(bundleJavaManager::addBundleWiring); + return bundleJavaManager; + } - final List sourceFiles = newSources.stream() - .map(sf -> new ResourceFileJavaFileObject(sf.getParentFile(), sf, Kind.SOURCE)) - .collect(Collectors.toList()); + /* + * Try building sourcefiles.. on success return true. + * + * If build fails, collect errors, remove files that caused + * errors from source files, and return false. + */ + private boolean tryBuild(PrintWriter writer, BundleJavaManager bundleJavaManager, + List sourceFiles, List options) throws IOException { + DiagnosticCollector diagnostics = new DiagnosticCollector(); + JavaCompiler.CompilationTask task = + compiler.getTask(writer, bundleJavaManager, diagnostics, options, null, sourceFiles); - Path dmf = binDir.resolve("META-INF").resolve("MANIFEST.MF"); - if (Files.exists(dmf)) { - Files.delete(dmf); + Boolean successfulCompilation = task.call(); + if (successfulCompilation) { + return true; } - - // try to compile, if we fail, avoid offenders and try again - while (!sourceFiles.isEmpty()) { - DiagnosticCollector diagnostics = - new DiagnosticCollector(); - JavaCompiler.CompilationTask task = compiler.getTask(writer, bundleJavaManager, - diagnostics, options, null, sourceFiles); - // task.setProcessors // for annotation processing / code generation - - Boolean successfulCompilation = task.call(); - if (successfulCompilation) { - break; + Set filesWithErrors = new HashSet<>(); + for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { + String error = diagnostic.toString() + "\n"; + writer.write(error); + ResourceFileJavaFileObject sourceFileObject = + (ResourceFileJavaFileObject) diagnostic.getSource(); + ResourceFile sourceFile = sourceFileObject.getFile(); + buildError(sourceFile, error); // remember all errors for this file + filesWithErrors.add(sourceFileObject); + } + for (ResourceFileJavaFileObject sourceFileObject : filesWithErrors) { + if (sourceFiles.remove(sourceFileObject)) { + writer.printf("skipping %s\n", sourceFileObject.getFile().toString()); } - Set hadErrors = new HashSet<>(); - for (Diagnostic d : diagnostics.getDiagnostics()) { - String err = d.toString() + "\n"; - writer.write(err); - ResourceFileJavaFileObject sf = (ResourceFileJavaFileObject) d.getSource(); - ResourceFile rf = sf.getFile(); - buildError(rf, err); // remember all errors for this file - hadErrors.add(sf); - } - for (ResourceFileJavaFileObject sf : hadErrors) { - if (sourceFiles.remove(sf)) { - writer.printf("skipping %s\n", sf.getFile().toString()); - } - else { - throw new IOException( - "compilation error loop condition for " + sf.getFile().toString()); - } + else { + // we can't tolerate infinite loops here, so bail + throw new IOException("compilation error loop condition for " + + sourceFileObject.getFile().toString()); } + } + return false; + } - } - // mark the successful compilations - for (ResourceFileJavaFileObject sf : sourceFiles) { - ResourceFile rf = sf.getFile(); - buildSuccess(rf); - } - // buildErrors is now up to date, set status - if (getFailingSourcesCount() > 0) { - summary.printf("%d failing source files", getFailingSourcesCount()); - } - - ResourceFile smf = getSourceManifestPath(); - if (smf.exists()) { - Files.createDirectories(dmf.getParent()); - Files.copy(smf.getInputStream(), dmf, StandardCopyOption.REPLACE_EXISTING); - return summary.getValue(); - } - + /* analyze the current binary dir and generate a manifest and an activator if one isn't found */ + private String generateManifest(PrintWriter writer, Summary summary, Path binaryManifest) + throws OSGiException, IOException { // no manifest, so create one with bndtools Analyzer analyzer = new Analyzer(); - analyzer.setJar(new Jar(binDir.toFile())); // give bnd the contents - analyzer.setProperty("Bundle-SymbolicName", GhidraSourceBundle.sourceDirHash(path)); + analyzer.setJar(new Jar(binaryDir.toFile())); // give bnd the contents + analyzer.setProperty("Bundle-SymbolicName", + GhidraSourceBundle.sourceDirHash(getSourceDirectory())); analyzer.setProperty("Bundle-Version", "1.0"); - // XXX we must constrain analyzed imports according to constraints declared in @importpackages tags + // XXX we must constrain analyzed imports according to constraints declared in @importpackage tags analyzer.setProperty("Import-Package", "*"); analyzer.setProperty("Export-Package", "!*.private.*,!*.internal.*,*"); // analyzer.setBundleActivator(s); @@ -735,7 +718,6 @@ public class GhidraSourceBundle extends GhidraBundle { summary.print("bad manifest"); throw new OSGiException("failed to calculate manifest by analyzing code", e); } - Attributes ma = manifest.getMainAttributes(); String activatorClassName = null; try { @@ -751,28 +733,30 @@ public class GhidraSourceBundle extends GhidraBundle { 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(binDir, activatorClassName, writer)) { + if (!buildDefaultActivator(binaryDir, activatorClassName, writer)) { summary.print("failed to build generated activator"); return summary.getValue(); } // since we add the activator after bndtools built the imports, we should add its imports too - String imps = ma.getValue(Constants.IMPORT_PACKAGE); + String imps = manifestAttributes.getValue(Constants.IMPORT_PACKAGE); if (imps == null) { - ma.putValue(Constants.IMPORT_PACKAGE, + manifestAttributes.putValue(Constants.IMPORT_PACKAGE, GhidraBundleActivator.class.getPackageName()); } else { - ma.putValue(Constants.IMPORT_PACKAGE, + manifestAttributes.putValue(Constants.IMPORT_PACKAGE, imps + "," + GhidraBundleActivator.class.getPackageName()); } } - ma.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName); + manifestAttributes.putValue(Constants.BUNDLE_ACTIVATOR, activatorClassName); // write the manifest - Files.createDirectories(dmf.getParent()); - try (OutputStream out = Files.newOutputStream(dmf)) { + Files.createDirectories(binaryManifest.getParent()); + try (OutputStream out = Files.newOutputStream(binaryManifest)) { manifest.write(out); } } @@ -840,4 +824,172 @@ public class GhidraSourceBundle extends GhidraBundle { return true; } + /** + * compile a source directory to an exploded bundle + * + * @param writer for updating the user during compilation + * @throws IOException for source/manifest file reading/generation and binary deletion/creation + * @throws OSGiException if generation of bundle metadata fails + */ + private String compileToExplodedBundle(PrintWriter writer) throws IOException, OSGiException { + compileAttempted(); + + Files.createDirectories(binaryDir); + + Summary summary = new Summary(); + + List options = new ArrayList<>(); + options.add("-g"); + options.add("-d"); + options.add(binaryDir.toString()); + options.add("-sourcepath"); + options.add(getSourceDirectory().toString()); + options.add("-classpath"); + options + .add(System.getProperty("java.class.path") + File.pathSeparator + binaryDir.toString()); + options.add("-proc:none"); + + BundleJavaManager bundleJavaManager = createBundleJavaManager(writer, summary, options); + + final List sourceFiles = newSources.stream() + .map(sf -> new ResourceFileJavaFileObject(sf.getParentFile(), sf, Kind.SOURCE)) + .collect(Collectors.toList()); + + Path binaryManifest = getBinaryManifestPath(); + if (Files.exists(binaryManifest)) { + Files.delete(binaryManifest); + } + + // try to compile, if we fail, avoid offenders and try again + while (!sourceFiles.isEmpty()) { + if (tryBuild(writer, bundleJavaManager, sourceFiles, options)) { + break; + } + } + + // mark the successful compilations + for (ResourceFileJavaFileObject sourceFile : sourceFiles) { + buildSuccess(sourceFile.getFile()); + } + // buildErrors is now up to date, set status + if (getBuildErrorCount() > 0) { + int count = getBuildErrorCount(); + summary.printf("%d source file%s with errors", count, count > 1 ? "s" : ""); + } + + ResourceFile sourceManifest = getSourceManifestFile(); + if (sourceManifest.exists()) { + Files.createDirectories(binaryManifest.getParent()); + Files.copy(sourceManifest.getInputStream(), binaryManifest, + StandardCopyOption.REPLACE_EXISTING); + return summary.getValue(); + } + + return generateManifest(writer, summary, binaryManifest); + } + + protected static class Compilation { + PrintWriter writer; + Summary summary; + List options; + BundleJavaManager bundleJavaManager; + + List sourceFiles; + + } + + private static class Summary { + static String SEPERATOR = ", "; + final StringWriter stringWriter = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(stringWriter, true); + + void printf(String format, Object... args) { + if (stringWriter.getBuffer().length() > 0) { + printWriter.write(SEPERATOR); + } + printWriter.printf(format, args); + } + + void print(String arg) { + printWriter.print(arg); + } + + String getValue() { + printWriter.flush(); + return stringWriter.getBuffer().toString(); + } + } + + /** + * Index *.class files in a directory by class name, e.g. + * + *
    +	 *    "A" -> [directory/A.class]
    +	 *    "B" -> [directory/B.class, directory/B$inner.class]
    +	 * 
    + * + *

    A list of classes are then processed one at a time with {@link ClassMapper#findAndRemove}. + * + *

    After processing, "extras" are handled with {@link ClassMapper#extraClassFiles}. + */ + private static class ClassMapper { + final Map> classToClassFilesMap; + + /** + * Map classes in {@code directory} with {@link ClassMapper}. + * + * @param directory the directory + * @throws IOException if there's a problem listing files + */ + ClassMapper(Path directory) throws IOException { + classToClassFilesMap = Files.exists(directory) ? Files.list(directory) + .filter( + f -> Files.isRegularFile(f) && f.getFileName().toString().endsWith(".class")) + .collect(groupingBy(f -> { + String fileName = f.getFileName().toString(); + // if f is the class file of an inner class, use the class name + int money = fileName.indexOf('$'); + if (money >= 0) { + return fileName.substring(0, money); + } + // drop ".class" + return fileName.substring(0, fileName.length() - 6); + })) : Collections.emptyMap(); + } + + List findAndRemove(ResourceFile sourceFile) { + String className = sourceFile.getName(); + if (className.endsWith(".java")) { + className = className.substring(0, className.length() - 5); + long lastModifiedSource = sourceFile.lastModified(); + List classFiles = classToClassFilesMap.remove(className); + if (classFiles == null) { + classFiles = Collections.emptyList(); + } + long lastModifiedClassFile = classFiles.isEmpty() ? -1 + : classFiles.stream() + .mapToLong(p -> p.toFile().lastModified()) + .min() + .getAsLong(); + // if source is newer than the oldest binary, report + if (lastModifiedSource > lastModifiedClassFile) { + return classFiles; + } + } + return null; + } + + public boolean hasExtraClassFiles() { + return !classToClassFilesMap.isEmpty(); + } + + public Collection extraClassFiles() { + return classToClassFilesMap.values() + .stream() + .flatMap(l -> l.stream()) + .collect(Collectors.toList()); + } + + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/LessFreneticGTable.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/LessFreneticGTable.java index f0e66aa25a..8f32417f24 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/LessFreneticGTable.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/LessFreneticGTable.java @@ -22,8 +22,11 @@ import javax.swing.table.TableModel; import docking.widgets.table.*; /** - * RowObjectSelectionManager attempts to repair selections in a filtered table - * before and after filter events. The additional selection events, however, cause focus changes we don't want. + * {@link RowObjectSelectionManager} attempts to repair selections in a filtered table + * before and after filter events. The additional selection events, however, cause focus changes we don't want. + * + *

    if the behavior is a bug in RowObjectSelectionManager, and it's fixed, this + * class can go away and it's use in {@link BundleStatusComponentProvider} replaced with GTable. */ class LessFreneticGTable extends GTable { boolean chilled = false; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiException.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiException.java index cba3ae1d74..756e23d848 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiException.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiException.java @@ -17,9 +17,12 @@ package ghidra.app.plugin.core.osgi; import ghidra.util.exception.UsrException; +/** + * Wrapper for exceptions originating with an OSGi operation. + */ public class OSGiException extends UsrException { /** - * Wrapper for exceptions originating with an OSGi operation. + * Create an exception with given {@code message} and {@code cause}. * * @param message a contextual message * @param cause the original exception @@ -29,7 +32,7 @@ public class OSGiException extends UsrException { } /** - * Wrapper for exceptions originating with an OSGi operation. + * Create an exception with given {@code message}. * * @param message a contextual message */ diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java index 08e09d3e2e..f22ed26246 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/osgi/OSGiUtils.java @@ -77,17 +77,17 @@ public class OSGiUtils { /** * parse Import-Package string from a bundle manifest * - * @param imports Import-Package value + * @param importPackageString Import-Package value * @return deduced requirements or null if there was an error * @throws BundleException on parse failure */ - static List parseImports(String imports) throws BundleException { + static List parseImportPackage(String importPackageString) + throws BundleException { // parse it with Felix's ManifestParser to a list of BundleRequirement objects Map headerMap = new HashMap<>(); - headerMap.put(Constants.IMPORT_PACKAGE, imports); - ManifestParser mp; - mp = new ManifestParser(null, null, null, headerMap); - return mp.getRequirements(); + headerMap.put(Constants.IMPORT_PACKAGE, importPackageString); + ManifestParser manifestParser = new ManifestParser(null, null, null, headerMap); + return manifestParser.getRequirements(); } // from https://dzone.com/articles/locate-jar-classpath-given @@ -102,10 +102,10 @@ public class OSGiUtils { location = loader.getResource(classLocation); } if (location != null) { - Pattern p = Pattern.compile("^.*:(.*)!.*$"); - Matcher m = p.matcher(location.toString()); - if (m.find()) { - return m.group(1); + Pattern pattern = Pattern.compile("^.*:(.*)!.*$"); + Matcher matcher = pattern.matcher(location.toString()); + if (matcher.find()) { + return matcher.group(1); } return null; // not loaded from jar? } @@ -132,13 +132,14 @@ public class OSGiUtils { .map(Path::normalize); } - static void collectPackagesFromDirectory(Path dirPath, Set s) { + static void collectPackagesFromDirectory(Path dirPath, Set packages) { try { - Files.walk(dirPath).filter(p -> p.toString().endsWith(".class")).forEach(p -> { - String n = dirPath.relativize(p).toString(); - int lastSlash = n.lastIndexOf(File.separatorChar); - s.add(lastSlash > 0 ? n.substring(0, lastSlash).replace(File.separatorChar, '.') - : ""); + Files.walk(dirPath).filter(p -> p.toString().endsWith(".class")).forEach(path -> { + String relativePath = dirPath.relativize(path).toString(); + int lastSlash = relativePath.lastIndexOf(File.separatorChar); + packages + .add(lastSlash > 0 ? relativePath.substring(0, lastSlash).replace(File.separatorChar, '.') + : ""); }); } @@ -147,13 +148,13 @@ public class OSGiUtils { } } - static void collectPackagesFromJar(Path jarPath, Set s) { + static void collectPackagesFromJar(Path jarPath, Set packages) { try { try (JarFile j = new JarFile(jarPath.toFile())) { - j.stream().filter(je -> je.getName().endsWith(".class")).forEach(je -> { - String n = je.getName(); - int lastSlash = n.lastIndexOf('/'); - s.add(lastSlash > 0 ? n.substring(0, lastSlash).replace('/', '.') : ""); + j.stream().filter(entry -> entry.getName().endsWith(".class")).forEach(jarEntry -> { + String entryName = jarEntry.getName(); + int lastSlash = entryName.lastIndexOf('/'); + packages.add(lastSlash > 0 ? entryName.substring(0, lastSlash).replace('/', '.') : ""); }); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java index bb4cb6e8fb..fd7ca5b497 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptActionManager.java @@ -21,6 +21,7 @@ import java.io.*; import java.net.*; import java.nio.file.Files; import java.util.*; +import java.util.function.Predicate; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -30,6 +31,7 @@ import javax.swing.KeyStroke; import docking.ActionContext; import docking.DockingUtils; import docking.action.*; +import docking.action.builder.ActionBuilder; import docking.actions.KeyBindingUtils; import docking.tool.ToolConstants; import docking.widgets.table.GTable; @@ -61,7 +63,6 @@ class GhidraScriptActionManager { private DockingAction globalRunLastAction; private DockingAction renameAction; private DockingAction keyBindingAction; - private DockingAction helpAction; private Map actionMap = new HashMap<>(); GhidraScriptActionManager(GhidraScriptComponentProvider provider, GhidraScriptMgrPlugin plugin, @@ -78,7 +79,7 @@ class GhidraScriptActionManager { } void restoreUserDefinedKeybindings(SaveState saveState) { - Collection dirs = provider.getBundleHost().getBundlePaths(); + Collection dirs = provider.getBundleHost().getBundleFiles(); String[] names = saveState.getNames(); for (String name : names) { @@ -172,54 +173,32 @@ class GhidraScriptActionManager { globalRunLastAction.firePropertyChanged(DockingActionIf.DESCRIPTION_PROPERTY, "", newDesc); } - private DockingAction createScriptAction(String name, String menuEntry, - String actionDescription, Icon icon, String toolBarGroup, Runnable runnable) { - DockingAction action = new DockingAction(name, plugin.getName()) { - @Override - public void actionPerformed(ActionContext context) { - runnable.run(); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - Object contextObject = context.getContextObject(); - return contextObject instanceof ResourceFile; - } - }; - action.setPopupMenuData(new MenuData(new String[] { menuEntry }, icon)); - action.setToolBarData(new ToolBarData(icon, toolBarGroup)); - - action.setDescription(actionDescription); - action.setEnabled(false); - - plugin.getTool().addLocalAction(provider, action); - - return action; + private DockingAction createScriptAction(String name, String menuEntry, String description, + Icon icon, String toolBarGroup, Runnable runnable) { + return new ActionBuilder(name, plugin.getName()).popupMenuPath(menuEntry) + .popupMenuIcon(icon) + .toolBarIcon(icon) + .toolBarGroup(toolBarGroup) + .description(description) + .enabled(false) + .enabledWhen(context -> context.getContextObject() instanceof ResourceFile) + .onAction(context -> runnable.run()) + .buildAndInstallLocal(provider); } - private DockingAction createScriptTableAction(String name, String actionDescription, Icon icon, + private DockingAction createScriptTableAction(String name, String description, Icon icon, Runnable runnable) { - DockingAction action = new DockingAction(name, plugin.getName()) { - @Override - public void actionPerformed(ActionContext context) { - runnable.run(); - } - - @Override - public boolean isAddToPopup(ActionContext context) { + return new ActionBuilder(name, plugin.getName()).popupMenuPath(name) + .popupMenuIcon(icon) + .toolBarIcon(icon) + .toolBarGroup(null) + .description(description) + .enabledWhen(context -> { Object contextObject = context.getContextObject(); return (contextObject instanceof GTable) || (contextObject instanceof ResourceFile); - } - }; - action.setPopupMenuData(new MenuData(new String[] { name }, icon)); - action.setToolBarData(new ToolBarData(icon, null)); - - action.setDescription(actionDescription); - action.setEnabled(true); - - plugin.getTool().addLocalAction(provider, action); - - return action; + }) + .onAction(context -> runnable.run()) + .buildAndInstallLocal(provider); } private void createActions() { @@ -253,60 +232,57 @@ class GhidraScriptActionManager { newAction = createScriptTableAction("New", "Create New Script", ResourceManager.loadImage("images/script_add.png"), provider::newScript); - createScriptTableAction("Refresh", "Refresh Script List", - Icons.REFRESH_ICON, provider::refresh); + createScriptTableAction("Refresh", "Refresh Script List", Icons.REFRESH_ICON, + provider::refresh); showBundleStatusAction = createScriptTableAction("Script Directories", "Manage Script Directories", ResourceManager.loadImage("images/text_list_bullets.png"), provider::showBundleStatusComponent); - helpAction = new DockingAction("Ghidra API Help", plugin.getName()) { - @Override - public void actionPerformed(ActionContext context) { - showGhidraScriptJavadoc(); - } - - @Override - public boolean isEnabledForContext(ActionContext context) { - Object contextObject = context.getContextObject(); - return (contextObject instanceof GTable) || (contextObject instanceof ResourceFile); - } - - @Override - public boolean isAddToPopup(ActionContext context) { - Object contextObject = context.getContextObject(); - return (contextObject instanceof GTable) || (contextObject instanceof ResourceFile); - } + Icon icon = ResourceManager.loadImage("images/red-cross.png"); + Predicate test = context -> { + Object contextObject = context.getContextObject(); + return (contextObject instanceof GTable) || (contextObject instanceof ResourceFile); }; - helpAction.setPopupMenuData(new MenuData(new String[] { "Ghidra API Help" }, - ResourceManager.loadImage("images/red-cross.png"), null)); - helpAction.setToolBarData( - new ToolBarData(ResourceManager.loadImage("images/red-cross.png"), null)); + new ActionBuilder("Ghidra API Help", plugin.getName()).popupMenuPath("Ghidra API Help") + .popupMenuIcon(icon) + .popupWhen(test) + .toolBarIcon(icon) + .toolBarGroup(null) + .description("Help") + .helpLocation(new HelpLocation(plugin.getName(), "Help")) + .enabledWhen(test) + .onAction(context -> showGhidraScriptJavadoc()) + .buildAndInstallLocal(provider); - helpAction.setDescription("Help"); - helpAction.setEnabled(true); - helpAction.setHelpLocation(new HelpLocation(plugin.getName(), "Help")); - plugin.getTool().addLocalAction(provider, helpAction); - - DockingAction globalHelpAction = new DockingAction("Ghidra API Help", plugin.getName()) { + // XXX In order to override a method of the new DockingAction and use the builder, we + // need to override the build method of the ActionBuilder. When the ActionBuilder is + // updated, this code can be cleaned up. + new ActionBuilder("Ghidra API Help", plugin.getName()) { @Override - public void actionPerformed(ActionContext context) { - showGhidraScriptJavadoc(); - } + public DockingAction build() { + validate(); + DockingAction action = new DockingAction(name, owner, keyBindingType) { + @Override + public void actionPerformed(ActionContext context) { + actionCallback.accept(context); + } - @Override - public boolean shouldAddToWindow(boolean isMainWindow, Set> contextTypes) { - return true; + @Override + public boolean shouldAddToWindow(boolean isMainWindow, + Set> contextTypes) { + return true; + } + }; + decorateAction(action); + return action; } - }; - globalHelpAction.setEnabled(true); - globalHelpAction.setHelpLocation(new HelpLocation("Misc", "Welcome_to_Ghidra_Help")); - globalHelpAction.setMenuBarData( - new MenuData(new String[] { ToolConstants.MENU_HELP, "Ghidra API Help" }, null, - ToolConstants.HELP_CONTENTS_MENU_GROUP)); - plugin.getTool().addAction(globalHelpAction); - + }.menuGroup(ToolConstants.HELP_CONTENTS_MENU_GROUP) + .menuPath(ToolConstants.MENU_HELP, "Ghidra API Help") + .helpLocation(new HelpLocation("Misc", "Welcome_to_Ghidra_Help")) + .onAction(context -> showGhidraScriptJavadoc()) + .buildAndInstall(plugin.getTool()); } private void showGhidraScriptJavadoc() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java index 60765808a7..c163edab6a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptComponentProvider.java @@ -458,7 +458,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { .stream() .filter(GhidraSourceBundle.class::isInstance) .filter(GhidraBundle::isEnabled) - .map(GhidraBundle::getPath) + .map(GhidraBundle::getFile) .collect(Collectors.toList()); } @@ -471,7 +471,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { .filter(GhidraSourceBundle.class::isInstance) .filter(Predicate.not(GhidraBundle::isSystemBundle)) .filter(GhidraBundle::isEnabled) - .map(GhidraBundle::getPath) + .map(GhidraBundle::getFile) .collect(Collectors.toList()); } @@ -530,7 +530,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { } void enableScriptDirectory(ResourceFile scriptDir) { - bundleHost.enablePath(scriptDir); + bundleHost.enable(scriptDir); Msg.showInfo(this, getComponent(), "Script Path Added/Enabled", "The directory has been automatically enabled for use:\n" + scriptDir.getAbsolutePath()); @@ -586,7 +586,7 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { } void runScript(String scriptName, TaskListener listener) { - for (ResourceFile dir : bundleHost.getBundlePaths()) { + for (ResourceFile dir : bundleHost.getBundleFiles()) { if (dir.isDirectory()) { ResourceFile scriptSource = new ResourceFile(dir, scriptName); if (scriptSource.exists()) { @@ -711,55 +711,6 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { bundleStatusComponentProvider.setVisible(true); } - class RefreshingBundleHostListener implements BundleHostListener { - - @Override - public void bundleBuilt(GhidraBundle bundle, String summary) { - if (bundle instanceof GhidraSourceBundle) { - GhidraSourceBundle sourceBundle = (GhidraSourceBundle) bundle; - for (ResourceFile sourceFile : sourceBundle.getNewSources()) { - if (infoManager.containsMetadata(sourceFile)) { - ScriptInfo scriptInfo = infoManager.getExistingScriptInfo(sourceFile); - GhidraBundle.BuildFailure e = sourceBundle.getErrors(sourceFile); - scriptInfo.setCompileErrors(e != null); - } - } - tableModel.fireTableDataChanged(); - } - } - - @Override - public void bundleEnablementChange(GhidraBundle bundle, boolean newEnablment) { - if (bundle instanceof GhidraSourceBundle) { - refresh(); - } - } - - @Override - public void bundleAdded(GhidraBundle bundle) { - plugin.getTool().setConfigChanged(true); - refresh(); - } - - @Override - public void bundlesAdded(Collection bundles) { - plugin.getTool().setConfigChanged(true); - refresh(); - } - - @Override - public void bundleRemoved(GhidraBundle bundle) { - plugin.getTool().setConfigChanged(true); - refresh(); - } - - @Override - public void bundlesRemoved(Collection bundles) { - plugin.getTool().setConfigChanged(true); - refresh(); - } - } - void refresh() { hasBeenRefreshed = true; @@ -778,8 +729,8 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { private void updateAvailableScriptFilesForAllPaths() { List scriptsToRemove = tableModel.getScripts(); List scriptAccumulator = new ArrayList<>(); - for (ResourceFile bundlePath : getScriptDirectories()) { - updateAvailableScriptFilesForDirectory(scriptsToRemove, scriptAccumulator, bundlePath); + for (ResourceFile bundleFile : getScriptDirectories()) { + updateAvailableScriptFilesForDirectory(scriptsToRemove, scriptAccumulator, bundleFile); } // note: do this after the loop to prevent a flurry of table model update events @@ -1135,21 +1086,6 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { return taskListener; } - private class ScriptTaskListener implements TaskListener { - @Override - public void taskCancelled(Task task) { - taskCompleted(task); - } - - @Override - public void taskCompleted(Task task) { - Rectangle visibleRect = scriptTable.getVisibleRect(); - scriptTable.repaint(visibleRect); - } - } - - /********************************************************************/ - @Override public void componentShown() { if (!hasBeenRefreshed) { @@ -1199,6 +1135,69 @@ public class GhidraScriptComponentProvider extends ComponentProviderAdapter { } } + /** passed to runScript, repaints scriptTable when a script completes */ + private class ScriptTaskListener implements TaskListener { + @Override + public void taskCancelled(Task task) { + taskCompleted(task); + } + + @Override + public void taskCompleted(Task task) { + Rectangle visibleRect = scriptTable.getVisibleRect(); + scriptTable.repaint(visibleRect); + } + } + + class RefreshingBundleHostListener implements BundleHostListener { + + @Override + public void bundleBuilt(GhidraBundle bundle, String summary) { + if (bundle instanceof GhidraSourceBundle) { + GhidraSourceBundle sourceBundle = (GhidraSourceBundle) bundle; + for (ResourceFile sourceFile : sourceBundle.getNewSources()) { + if (infoManager.containsMetadata(sourceFile)) { + ScriptInfo scriptInfo = infoManager.getExistingScriptInfo(sourceFile); + BuildError e = sourceBundle.getErrors(sourceFile); + scriptInfo.setCompileErrors(e != null); + } + } + tableModel.fireTableDataChanged(); + } + } + + @Override + public void bundleEnablementChange(GhidraBundle bundle, boolean newEnablment) { + if (bundle instanceof GhidraSourceBundle) { + refresh(); + } + } + + @Override + public void bundleAdded(GhidraBundle bundle) { + plugin.getTool().setConfigChanged(true); + refresh(); + } + + @Override + public void bundlesAdded(Collection bundles) { + plugin.getTool().setConfigChanged(true); + refresh(); + } + + @Override + public void bundleRemoved(GhidraBundle bundle) { + plugin.getTool().setConfigChanged(true); + refresh(); + } + + @Override + public void bundlesRemoved(Collection bundles) { + plugin.getTool().setConfigChanged(true); + refresh(); + } + } + /** Table filter that uses the state of the tree to further filter */ private class ScriptTableSecondaryFilter implements TableFilter { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java index f5334e3bc6..6c39be52a5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptEditorComponentProvider.java @@ -133,8 +133,8 @@ public class GhidraScriptEditorComponentProvider extends ComponentProvider { } } - private boolean isReadOnly(ResourceFile scriptSourceFile1) { - return GhidraScriptUtil.isSystemScriptPath(scriptSourceFile1); + private static boolean isReadOnly(ResourceFile scriptSourceFile) { + return GhidraScriptUtil.isSystemScript(scriptSourceFile); } // private boolean isSystemScript() { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java index cdf84677ca..0b73e94ac6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin.java @@ -48,7 +48,8 @@ import ghidra.util.task.TaskListener; ) //@formatter:on public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScriptService { - private static int loaded = 0; + /** number of GhidraScriptMgrPlugin references to the BundleHost owned by {@link GhidraScriptUtil} */ + private static int referenceCount = 0; private final GhidraScriptComponentProvider provider; @@ -61,14 +62,16 @@ public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScript */ public GhidraScriptMgrPlugin(PluginTool tool) { super(tool, true, true, true); - if (loaded == 0) { + // Each tool starts a new script manager plugin, but we only ever want one bundle host. + // We store the one BundleHost in GhidraScriptUtil and keep a count of references to it. + if (referenceCount == 0) { bundleHost = new BundleHost(); GhidraScriptUtil.initialize(bundleHost, null); } else { bundleHost = GhidraScriptUtil.getBundleHost(); } - loaded += 1; + referenceCount += 1; provider = new GhidraScriptComponentProvider(this, bundleHost); } @@ -77,8 +80,8 @@ public class GhidraScriptMgrPlugin extends ProgramPlugin implements GhidraScript protected void dispose() { super.dispose(); provider.dispose(); - loaded -= 1; - if (loaded == 0) { + referenceCount -= 1; + if (referenceCount == 0) { GhidraScriptUtil.dispose(); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java index a32f2731a5..66eec871b7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java @@ -98,9 +98,9 @@ import ghidra.util.task.TaskMonitor; * *

    Ghidra Script State

    *
    - *

    - * All scripts, when run, will be handed the current state in the form of class - * instance variable. These variables are: + * + *

    All scripts, when run, will be handed the current state in the form of class instance + * variable. These variables are: *

      *
    1. currentProgram: the active program
    2. *
    3. currentAddress: the address of the current cursor location in the tool
    4. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptInfoManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptInfoManager.java index fe6ef5c804..e3d0974cbf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptInfoManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptInfoManager.java @@ -79,7 +79,7 @@ public class GhidraScriptInfoManager { * Returns the script info object for the specified script file, * construct a new one if necessary. * - * Only call this method if you expect to be creating ScriptInfo objects. + *

      Only call this method if you expect to be creating ScriptInfo objects. * Prefer getExistingScriptInfo instead. * * @param scriptFile the script file @@ -122,10 +122,9 @@ public class GhidraScriptInfoManager { public ScriptInfo getExistingScriptInfo(ResourceFile script) { ScriptInfo info = scriptFileToInfoMap.get(script); if (info == null) { - String s = (script.exists() ? "" : "non") + "existing script" + script.toString() + + String error = (script.exists() ? "" : "non") + "existing script" + script.toString() + " is missing expected ScriptInfo"; - System.err.println(s); - Msg.showError(GhidraScriptInfoManager.class, null, "ScriptInfo lookup", s); + Msg.showError(GhidraScriptInfoManager.class, null, "ScriptInfo lookup", error); } return info; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProperties.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProperties.java index eef2cb8191..7d15b22e72 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProperties.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptProperties.java @@ -26,7 +26,7 @@ import ghidra.util.Msg; * Handles processing for .properties files associated with a GhidraScript (.properties file and * script should share the same basename). * - * This should only be called/used by the GhidraScript class. + *

      This should only be called/used by the GhidraScript class. */ public class GhidraScriptProperties { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java index 6c1e24768d..e4ad037e58 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import generic.jar.ResourceFile; import ghidra.app.plugin.core.osgi.BundleHost; import ghidra.app.plugin.core.osgi.OSGiException; +import ghidra.app.plugin.core.script.GhidraScriptMgrPlugin; import ghidra.framework.Application; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; @@ -37,6 +38,9 @@ public class GhidraScriptUtil { */ public static String USER_SCRIPTS_DIR = buildUserScriptsDirectory(); + /** + * this instance is Ghidra's singleton, a reference is held here and in {@link GhidraScriptMgrPlugin} + */ private static BundleHost bundleHost; private static final String SCRIPTS_SUBDIR_NAME = "ghidra_scripts"; @@ -87,7 +91,7 @@ public class GhidraScriptUtil { } bundleHost.add(getUserScriptDirectory(), true, false); - bundleHost.add(getSystemScriptPaths(), true, true); + bundleHost.add(getSystemScriptDirectories(), true, true); } /** @@ -106,13 +110,19 @@ public class GhidraScriptUtil { * @return a list of the current script directories */ public static List getScriptSourceDirectories() { - return bundleHost.getBundlePaths() + return bundleHost.getBundleFiles() .stream() .filter(ResourceFile::isDirectory) .collect(Collectors.toList()); } - public static ResourceFile getSourceDirectoryContaining(ResourceFile sourceFile) { + /** + * Search the currently managed source directories for the given script file. + * + * @param sourceFile the source file + * @return the source directory if found, or null if not + */ + public static ResourceFile findSourceDirectoryContaining(ResourceFile sourceFile) { String sourcePath = sourceFile.getAbsolutePath(); for (ResourceFile sourceDir : getScriptSourceDirectories()) { if (sourcePath.startsWith(sourceDir.getAbsolutePath() + File.separatorChar)) { @@ -122,6 +132,12 @@ public class GhidraScriptUtil { return null; } + /** + * Search the currently managed scripts for one with the given name. + * + * @param scriptName the name + * @return the first file found or null if none are found + */ public static ResourceFile findScriptByName(String scriptName) { return findScriptFileInPaths(getScriptSourceDirectories(), scriptName); } @@ -148,22 +164,22 @@ public class GhidraScriptUtil { * Returns a list of the default script directories. * @return a list of the default script directories */ - public static List getSystemScriptPaths() { - List pathsList = new ArrayList<>(); + public static List getSystemScriptDirectories() { + List dirList = new ArrayList<>(); - addScriptPaths(pathsList, SCRIPTS_SUBDIR_NAME); - addScriptPaths(pathsList, DEV_SCRIPTS_SUBDIR_NAME); + addScriptDirectories(dirList, SCRIPTS_SUBDIR_NAME); + addScriptDirectories(dirList, DEV_SCRIPTS_SUBDIR_NAME); - Collections.sort(pathsList); - return pathsList; + Collections.sort(dirList); + return dirList; } public static ResourceFile getUserScriptDirectory() { return new ResourceFile(USER_SCRIPTS_DIR); } - private static void addScriptPaths(List pathsList, String directoryName) { - pathsList.addAll(Application.findModuleSubDirectories(directoryName)); + private static void addScriptDirectories(List dirList, String directoryName) { + dirList.addAll(Application.findModuleSubDirectories(directoryName)); } /** @@ -171,7 +187,7 @@ public class GhidraScriptUtil { * @param file script file or directory * @return true if file contained within Ghidra installation area */ - public static boolean isSystemScriptPath(ResourceFile file) { + public static boolean isSystemScript(ResourceFile file) { return isSystemFile(file); } @@ -333,8 +349,8 @@ public class GhidraScriptUtil { /** * Fixup name issues, such as package parts in the name and inner class names. - *

      - * This method can handle names with or without '.java' at the end; names with + * + *

      This method can handle names with or without '.java' at the end; names with * '$' (inner classes) and names with '.' characters for package separators * * @param name the name of the script diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java index 815b9ad87e..e1eefcb3d6 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/JavaScriptProvider.java @@ -40,7 +40,7 @@ public class JavaScriptProvider extends GhidraScriptProvider { * @return the bundle */ public GhidraSourceBundle getBundleForSource(ResourceFile sourceFile) { - ResourceFile sourceDir = GhidraScriptUtil.getSourceDirectoryContaining(sourceFile); + ResourceFile sourceDir = GhidraScriptUtil.findSourceDirectoryContaining(sourceFile); if (sourceDir == null) { return null; } @@ -111,15 +111,15 @@ public class JavaScriptProvider extends GhidraScriptProvider { * @throws Exception if build, activation, or class loading fail */ public Class loadClass(ResourceFile sourceFile, PrintWriter writer) throws Exception { - GhidraSourceBundle gb = getBundleForSource(sourceFile); - gb.build(writer); + GhidraSourceBundle bundle = getBundleForSource(sourceFile); + bundle.build(writer); - Bundle b = bundleHost.install(gb); + Bundle osgiBundle = bundleHost.install(bundle); - bundleHost.activateSynchronously(b); + bundleHost.activateSynchronously(osgiBundle); - String classname = gb.classNameForScript(sourceFile); - Class clazz = b.loadClass(classname); // throws ClassNotFoundException + String classname = bundle.classNameForScript(sourceFile); + Class clazz = osgiBundle.loadClass(classname); // throws ClassNotFoundException return clazz; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileManager.java index 3c4f05d67f..d4b151e323 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileManager.java @@ -26,9 +26,9 @@ import generic.jar.ResourceFile; import ghidra.util.exception.AssertException; /** - * A {@link JavaFileManager} that works with Ghidra's {@link ResourceFile}'s. - *

      - * This class is used to dynamically compile Ghidra scripts. + * A {@link JavaFileManager} that works with Ghidra's {@link ResourceFile}s. + * + *

      This class is used to dynamically compile Ghidra scripts. */ public class ResourceFileJavaFileManager implements JavaFileManager { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileObject.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileObject.java index 8531ea7896..2d381432e7 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileObject.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/ResourceFileJavaFileObject.java @@ -27,8 +27,8 @@ import generic.jar.ResourceFile; /** * A {@link JavaFileObject} that works with Ghidra's {@link ResourceFileJavaFileManager}. - *

      - * This class is used to dynamically compile Ghidra scripts. + * + *

      This class is used to dynamically compile Ghidra scripts. */ public class ResourceFileJavaFileObject implements JavaFileObject { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/StringTransformer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/StringTransformer.java index 5ebbe7594f..a0054354be 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/StringTransformer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/StringTransformer.java @@ -16,5 +16,5 @@ package ghidra.app.script; public interface StringTransformer { - T apply(String s); + public T apply(String s); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java index 5f4b846bb4..5d5a699cc0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/TestEnv.java @@ -550,33 +550,33 @@ public class TestEnv { }); } - public ScriptTaskListener runScript(File script) throws PluginException { - GhidraScriptMgrPlugin sm = getPlugin(GhidraScriptMgrPlugin.class); - if (sm == null) { + public ScriptTaskListener runScript(File scriptFile) throws PluginException { + GhidraScriptMgrPlugin scriptManagerPlugin = getPlugin(GhidraScriptMgrPlugin.class); + if (scriptManagerPlugin == null) { lazyTool().addPlugin(GhidraScriptMgrPlugin.class.getName()); - sm = getPlugin(GhidraScriptMgrPlugin.class); + scriptManagerPlugin = getPlugin(GhidraScriptMgrPlugin.class); } JavaScriptProvider scriptProvider = new JavaScriptProvider(); PrintWriter writer = new PrintWriter(System.out); - ResourceFile resourceFile = new ResourceFile(script); - GhidraScript scr=null; + ResourceFile resourceFile = new ResourceFile(scriptFile); + GhidraScript script=null; try { - scr=scriptProvider.getScriptInstance(resourceFile, writer); + script=scriptProvider.getScriptInstance(resourceFile, writer); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); } - if (scr==null) { + if (script==null) { writer.flush(); - throw new RuntimeException("Failed to compile script " + script.getAbsolutePath()); + throw new RuntimeException("Failed to compile script " + scriptFile.getAbsolutePath()); } - String scriptName = script.getName(); + String scriptName = scriptFile.getName(); ScriptTaskListener listener = new ScriptTaskListener(scriptName); - sm.runScript(scriptName, listener); + scriptManagerPlugin.runScript(scriptName, listener); return listener; } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java index 7927919d60..e717fe449d 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/AbstractGhidraScriptMgrPluginTest.java @@ -261,20 +261,20 @@ public abstract class AbstractGhidraScriptMgrPluginTest protected ResourceFile finishNewScriptDialog(String newScriptName) { - SaveDialog sd = waitForDialogComponent(SaveDialog.class); + SaveDialog saveDialog = waitForDialogComponent(SaveDialog.class); if (newScriptName != null) { - setNewScriptName(sd, newScriptName); + setNewScriptName(saveDialog, newScriptName); } - pressButtonByText(sd, "OK"); + pressButtonByText(saveDialog, "OK"); waitForSwing(); - ResourceFile newFile = (ResourceFile) invokeInstanceMethod("getFile", sd); + ResourceFile newFile = (ResourceFile) invokeInstanceMethod("getFile", saveDialog); assertNotNull(newFile); - JTextField textField = (JTextField) getInstanceField("nameField", sd); - assertTrue("New script dialog did not close. Message: " + sd.getStatusText() + - " - text: " + textField.getText(), !sd.isShowing()); + JTextField textField = (JTextField) getInstanceField("nameField", saveDialog); + assertTrue("New script dialog did not close. Message: " + saveDialog.getStatusText() + + " - text: " + textField.getText(), !saveDialog.isShowing()); return newFile; } @@ -334,8 +334,8 @@ public abstract class AbstractGhidraScriptMgrPluginTest GDynamicColumnTableModel model = (GDynamicColumnTableModel) RowObjectTableModel.unwrap(tableModel); - int n = model.getColumnCount(); - for (int i = 0; i < n; i++) { + int columnCount = model.getColumnCount(); + for (int i = 0; i < columnCount; i++) { String name = model.getColumnName(i); if (columnName.equals(name)) { return i; @@ -405,8 +405,8 @@ public abstract class AbstractGhidraScriptMgrPluginTest chooseJavaProvider(); - SaveDialog sd = waitForDialogComponent(SaveDialog.class); - pressButtonByText(sd, "OK"); + SaveDialog saveDialog = waitForDialogComponent(SaveDialog.class); + pressButtonByText(saveDialog, "OK"); waitForSwing(); // initialize our editor variable to the newly opened editor @@ -415,7 +415,7 @@ public abstract class AbstractGhidraScriptMgrPluginTest waitForSwing(); - return sd.getFile(); + return saveDialog.getFile(); } protected void assertCannotCreateNewScriptByName(final String name) throws Exception { @@ -847,8 +847,8 @@ public abstract class AbstractGhidraScriptMgrPluginTest protected void assertEditorContentsSame(ResourceFile file, String expectedText) { - Map map = provider.getEditorMap(); - GhidraScriptEditorComponentProvider fileEditor = map.get(file); + Map editorMap = provider.getEditorMap(); + GhidraScriptEditorComponentProvider fileEditor = editorMap.get(file); final JTextArea textArea = (JTextArea) findComponentByName(fileEditor.getComponent(), GhidraScriptEditorComponentProvider.EDITOR_COMPONENT_NAME); assertNotNull(textArea); @@ -985,7 +985,7 @@ public abstract class AbstractGhidraScriptMgrPluginTest // destroy any NewScriptxxx files...and Temp ones too List paths = provider.getBundleHost() - .getBundlePaths() + .getBundleFiles() .stream() .filter(ResourceFile::isDirectory) .collect(Collectors.toList()); @@ -1139,7 +1139,7 @@ public abstract class AbstractGhidraScriptMgrPluginTest private boolean isReadOnly(ResourceFile script) { assertNotNull(script); - return GhidraScriptUtil.isSystemScriptPath(script); + return GhidraScriptUtil.isSystemScript(script); } protected void assertSaveButtonDisabled() { diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java index 4e7e47112b..5eadf09404 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/BundleHostTest.java @@ -32,7 +32,15 @@ import ghidra.app.plugin.core.osgi.*; import ghidra.test.AbstractGhidraHeadlessIntegrationTest; public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { - static protected void wipe(Path path) throws IOException { + BundleHost bundleHost; + CapturingBundleHostListener capturingBundleHostListener; + + Set tempDirs = new HashSet<>(); + LinkedList bundleStack = new LinkedList<>(); + GhidraBundle currentBundle; + + + protected static void wipe(Path path) throws IOException { if (Files.exists(path)) { for (Path p : (Iterable) Files.walk(path).sorted( Comparator.reverseOrder())::iterator) { @@ -41,23 +49,17 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { } } - BundleHost bundleHost; - CapturingBundleHostListener bhl; - - Set tmpdirs = new HashSet<>(); - LinkedList gbstack = new LinkedList<>(); - GhidraBundle current_gb; protected GhidraBundle pushNewBundle() throws IOException { - String dir = String.format("sourcebundle%03d", tmpdirs.size()); + String dir = String.format("sourcebundle%03d", tempDirs.size()); Path tmpDir = new File(getTestDirectoryPath(), dir).toPath(); Files.createDirectories(tmpDir); - tmpdirs.add(tmpDir); + tempDirs.add(tmpDir); - ResourceFile rp = new ResourceFile(tmpDir.toFile()); - current_gb = bundleHost.add(rp, true, false); - gbstack.push(current_gb); - return current_gb; + ResourceFile sourceDirectory = new ResourceFile(tmpDir.toFile()); + currentBundle = bundleHost.add(sourceDirectory, true, false); + bundleStack.push(currentBundle); + return currentBundle; } static class CapturingBundleHostListener implements BundleHostListener { @@ -75,8 +77,8 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { bundleHost = new BundleHost(); bundleHost.startFramework(); - bhl = new CapturingBundleHostListener(); - bundleHost.addListener(bhl); + capturingBundleHostListener = new CapturingBundleHostListener(); + bundleHost.addListener(capturingBundleHostListener); pushNewBundle(); } @@ -84,29 +86,29 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { @After public void tearDown() throws IOException { bundleHost.dispose(); - bhl = null; + capturingBundleHostListener = null; bundleHost = null; - for (Path tmpdir : tmpdirs) { + for (Path tmpdir : tempDirs) { wipe(tmpdir); } } protected void buildWithExpectations(String expectedCompilerOutput, String expectedSummary) throws Exception { - StringWriter sw = new StringWriter(); + StringWriter stringWriter = new StringWriter(); - current_gb.build(new PrintWriter(sw)); - sw.flush(); + currentBundle.build(new PrintWriter(stringWriter)); + stringWriter.flush(); assertEquals("unexpected output during build", expectedCompilerOutput, - sw.getBuffer().toString()); + stringWriter.getBuffer().toString()); - assertEquals("wrong summary", expectedSummary, bhl.lastBuildSummary); + assertEquals("wrong summary", expectedSummary, capturingBundleHostListener.lastBuildSummary); } protected void activate() throws Exception { - Bundle bundle = bundleHost.install(current_gb); + Bundle bundle = bundleHost.install(currentBundle); assertNotNull("failed to install bundle", bundle); bundle.start(); } @@ -117,7 +119,7 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { } protected Class loadClass(String classname) throws ClassNotFoundException { - Class clazz = current_gb.getOSGiBundle().loadClass(classname); + Class clazz = currentBundle.getOSGiBundle().loadClass(classname); assertNotNull("failed to load class", clazz); return clazz; } @@ -133,19 +135,19 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { protected void addClass(String meta, String imports, String fullclassname, String body) throws IOException { String simplename; - Path tmpsource = current_gb.getPath().getFile(false).toPath(); + Path tmpsource = currentBundle.getFile().getFile(false).toPath(); if (fullclassname.contains(".")) { String packagename; - Pattern classpat = Pattern.compile("^(.*)\\.([^.]*)$"); - Matcher m = classpat.matcher(fullclassname); - if (!m.matches()) { + Pattern pattern = Pattern.compile("^(.*)\\.([^.]*)$"); + Matcher matcher = pattern.matcher(fullclassname); + if (!matcher.matches()) { throw new IllegalArgumentException( "fullclassname must be of the form \"xxxx.xxxx.Xxxx\""); } - packagename = m.group(1); - simplename = m.group(2); + packagename = matcher.group(1); + simplename = matcher.group(2); for (String n : packagename.split("\\.")) { tmpsource = tmpsource.resolve(n); @@ -169,9 +171,9 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { protected Object getInstance(String classname) throws Exception { Class clazz = loadClass(classname); - Object o = clazz.getDeclaredConstructor().newInstance(); - assertNotNull("failed to create instance", o); - return o; + Object object = clazz.getDeclaredConstructor().newInstance(); + assertNotNull("failed to create instance", object); + return object; } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ @@ -221,9 +223,9 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { "BClass.java:7: error: ';' expected\n" + " failing java goes here\n" + " ^\n" + - String.format("skipping %s/apackage/BClass.java\n", current_gb.getPath().toString()) + String.format("skipping %s/apackage/BClass.java\n", currentBundle.getFile().toString()) , - "1 failing source files" + "1 source file with errors" ); // @formatter:on @@ -290,9 +292,9 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { " ^\n" + " symbol: variable Library\n" + " location: class apackage.AClass\n" + - String.format("skipping %s/apackage/AClass.java\n", current_gb.getPath().toString()) + String.format("skipping %s/apackage/AClass.java\n", currentBundle.getFile().toString()) , - "1 failing source files" + "1 source file with errors" ); // @formatter:on } @@ -313,7 +315,7 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { pushNewBundle(); - // @importpackages tag is only parsed from classes in default package + // @importpackage tag is only parsed from classes in default package addClass( "//@importpackage lib\n" , @@ -353,10 +355,10 @@ public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest { "}\n" ); - Path p = current_gb.getPath().getFile(false).toPath(); - p=p.resolve("META-INF"); - Files.createDirectories(p); - Path manifest=p.resolve("MANIFEST.MF"); + Path path = currentBundle.getFile().getFile(false).toPath(); + path=path.resolve("META-INF"); + Files.createDirectories(path); + Path manifest=path.resolve("MANIFEST.MF"); Files.writeString(manifest, "Manifest-Version: 1.0\n" + diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java index 25513c6b7c..2941477638 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin2Test.java @@ -216,7 +216,7 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes tempScriptDir.mkdir(); ResourceFile scriptDir = new ResourceFile(tempScriptDir); - provider.getBundleHost().enablePath(scriptDir); + provider.getBundleHost().enable(scriptDir); try { // create a script file in that directory @@ -257,8 +257,8 @@ public class GhidraScriptMgrPlugin2Test extends AbstractGhidraScriptMgrPluginTes chooseJavaProvider(); - SaveDialog sd = AbstractDockingTest.waitForDialogComponent(SaveDialog.class); - pressButtonByText(sd, "OK"); + SaveDialog saveDialog = AbstractDockingTest.waitForDialogComponent(SaveDialog.class); + pressButtonByText(saveDialog, "OK"); refreshProvider(); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin3Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin3Test.java index 5142c63347..8e824418fa 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin3Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/script/GhidraScriptMgrPlugin3Test.java @@ -148,11 +148,11 @@ public class GhidraScriptMgrPlugin3Test extends AbstractGhidraScriptMgrPluginTes public void testRefreshFindsNewScript() throws Exception { int rowCount = getRowCount(); - JavaScriptProvider jsp = new JavaScriptProvider(); + JavaScriptProvider javaScriptProvider = new JavaScriptProvider(); - ResourceFile newScript = GhidraScriptUtil.createNewScript(jsp, + ResourceFile newScript = GhidraScriptUtil.createNewScript(javaScriptProvider, new ResourceFile(GhidraScriptUtil.USER_SCRIPTS_DIR), provider.getScriptDirectories()); - jsp.createNewScript(newScript, null); + javaScriptProvider.createNewScript(newScript, null); refreshScriptManager(); @@ -283,16 +283,16 @@ public class GhidraScriptMgrPlugin3Test extends AbstractGhidraScriptMgrPluginTes final ResourceFile dir = new ResourceFile(getTestDirectoryPath() + "/test_scripts"); dir.getFile(false).mkdirs(); - provider.getBundleHost().enablePath(dir); + provider.getBundleHost().enable(dir); waitForSwing(); pressNewButton(); chooseJavaProvider(); - SaveDialog sd = waitForDialogComponent(SaveDialog.class); + SaveDialog saveDialog = waitForDialogComponent(SaveDialog.class); - final ListPanel listPanel = (ListPanel) findComponentByName(sd.getComponent(), "PATH_LIST"); + final ListPanel listPanel = (ListPanel) findComponentByName(saveDialog.getComponent(), "PATH_LIST"); assertNotNull(listPanel); assertTrue(listPanel.isVisible()); assertEquals(2, listPanel.getListModel().getSize()); @@ -315,11 +315,11 @@ public class GhidraScriptMgrPlugin3Test extends AbstractGhidraScriptMgrPluginTes }, false); waitForSwing(); - pressButtonByText(sd, "OK"); - assertTrue(!sd.isShowing()); + pressButtonByText(saveDialog, "OK"); + assertTrue(!saveDialog.isShowing()); waitForTasks(); - ResourceFile newScript = sd.getFile(); + ResourceFile newScript = saveDialog.getFile(); assertTrue(newScript.exists()); assertNotNull(newScript); diff --git a/Ghidra/Features/Decompiler/ghidra_scripts/ShowConstantUse.java b/Ghidra/Features/Decompiler/ghidra_scripts/ShowConstantUse.java index 0192e7d58e..24e7cea701 100644 --- a/Ghidra/Features/Decompiler/ghidra_scripts/ShowConstantUse.java +++ b/Ghidra/Features/Decompiler/ghidra_scripts/ShowConstantUse.java @@ -137,16 +137,16 @@ public class ShowConstantUse extends GhidraScript { tableDialog.setMessage("Finished!"); } - private Function getReferencedFunction(Address faddr) { - Function f = currentProgram.getFunctionManager().getFunctionAt(faddr); + private Function getReferencedFunction(Address functionAddress) { + Function f = currentProgram.getFunctionManager().getFunctionAt(functionAddress); // couldn't find the function, see if there is an external ref there. if (f == null) { Reference[] referencesFrom = - currentProgram.getReferenceManager().getReferencesFrom(faddr); - for (Reference element : referencesFrom) { - if (element.isExternalReference()) { - faddr = element.getToAddress(); - f = currentProgram.getFunctionManager().getFunctionAt(faddr); + currentProgram.getReferenceManager().getReferencesFrom(functionAddress); + for (Reference reference : referencesFrom) { + if (reference.isExternalReference()) { + functionAddress = reference.getToAddress(); + f = currentProgram.getFunctionManager().getFunctionAt(functionAddress); if (f != null) { break; } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/util/Path.java b/Ghidra/Framework/Generic/src/main/java/generic/util/Path.java index bdb5298e86..8e64d2976b 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/util/Path.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/util/Path.java @@ -27,7 +27,7 @@ import ghidra.framework.OperatingSystem; public class Path implements Comparable { public static final String GHIDRA_HOME = "$GHIDRA_HOME"; public static final String USER_HOME = "$USER_HOME"; - private ResourceFile path; + private ResourceFile file; private boolean isEnabled; private boolean isEditable; private boolean isReadOnly; @@ -39,10 +39,10 @@ public class Path implements Comparable { *

    5. isEditable = true
    6. *
    7. isReadOnly = false
    8. *
- * @param path absolute directory path + * @param file absolute directory path */ - public Path(File path) { - this(new ResourceFile(path), true, true, false); + public Path(File file) { + this(new ResourceFile(file), true, true, false); } /** @@ -52,21 +52,21 @@ public class Path implements Comparable { *
  • isEditable = true
  • *
  • isReadOnly = false
  • * - * @param path absolute directory path + * @param file absolute directory path */ - public Path(ResourceFile path) { - this(path, true, true, false); + public Path(ResourceFile file) { + this(file, true, true, false); } /** * Identifies an absolute directory path with the specified attributes. - * @param path absolute directory path + * @param file absolute directory path * @param isEnabled directory path will be searched if true * @param isEditable if true files contained within directory are considered editable * @param isReadOnly if true files contained within directory are considered read-only */ - public Path(ResourceFile path, boolean isEnabled, boolean isEditable, boolean isReadOnly) { - this.path = path; + public Path(ResourceFile file, boolean isEnabled, boolean isEditable, boolean isReadOnly) { + this.file = file; this.isEnabled = isEnabled; this.isEditable = isEditable; this.isReadOnly = isReadOnly; @@ -106,7 +106,7 @@ public class Path implements Comparable { * @param isReadOnly if true files contained within directory are considered read-only */ public Path(String path, boolean isEnabled, boolean isEditable, boolean isReadOnly) { - this.path = fromPathString(path); + this.file = fromPathString(path); this.isEnabled = isEnabled; this.isEditable = isEditable; @@ -132,12 +132,12 @@ public class Path implements Comparable { return false; } Path that = (Path) obj; - return this.path.equals(that.path); + return this.file.equals(that.file); } @Override public int hashCode() { - return path.hashCode(); + return file.hashCode(); } /** @@ -171,7 +171,7 @@ public class Path implements Comparable { } public ResourceFile getPath() { - return path; + return file; } /** @@ -182,36 +182,36 @@ public class Path implements Comparable { * @return the path as a ResourceFile. */ public static ResourceFile fromPathString(String path) { - ResourceFile rf = null; + ResourceFile resourceFile = null; if (path.startsWith(GHIDRA_HOME)) { - rf = resolveGhidraHome(path); + resourceFile = resolveGhidraHome(path); } else if (path.startsWith(USER_HOME)) { String userHome = System.getProperty("user.home"); int length = USER_HOME.length(); String relativePath = path.substring(length); - rf = new ResourceFile(new File(userHome + relativePath)); + resourceFile = new ResourceFile(new File(userHome + relativePath)); } else { - rf = new ResourceFile(path); + resourceFile = new ResourceFile(path); } if (OperatingSystem.CURRENT_OPERATING_SYSTEM == OperatingSystem.WINDOWS) { - rf = rf.getCanonicalFile(); + resourceFile = resourceFile.getCanonicalFile(); } - return rf; + return resourceFile; } /** * Returns the path as a string with path element placeholders, such as * {@link #GHIDRA_HOME}. - * @param path the path * + * @param file the file to translate * @return the path as a string . */ - static public String toPathString(ResourceFile path) { + static public String toPathString(ResourceFile file) { String userHome = System.getProperty("user.home"); - String absolutePath = path.getAbsolutePath(); + String absolutePath = file.getAbsolutePath(); for (ResourceFile appRoot : Application.getApplicationRootDirectories()) { String ghidraHome = appRoot.getAbsolutePath(); if (absolutePath.startsWith(ghidraHome)) { @@ -239,7 +239,7 @@ public class Path implements Comparable { * @return the path as a string . */ public String getPathAsString() { - return toPathString(path); + return toPathString(file); } /** @@ -253,24 +253,24 @@ public class Path implements Comparable { public void setPath(String path) { if (isEditable) { - this.path = new ResourceFile(path); + this.file = new ResourceFile(path); } else { throw new IllegalStateException("Path is not editable - " + path); } } - public void setPath(ResourceFile path) { + public void setPath(ResourceFile file) { if (isEditable) { - this.path = path; + this.file = file; } else { - throw new IllegalStateException("Path is not editable - " + path); + throw new IllegalStateException("Path is not editable - " + file); } } public boolean exists() { - return path.exists(); + return file.exists(); } @Override diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GhidraScriptMgrPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GhidraScriptMgrPluginScreenShots.java index 15495f121f..7f4bcbc9ff 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GhidraScriptMgrPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/GhidraScriptMgrPluginScreenShots.java @@ -111,15 +111,15 @@ public class GhidraScriptMgrPluginScreenShots extends GhidraScreenShotGenerator @Test public void testScript_Dirs() throws Exception { - List paths = new ArrayList<>(); - paths.add(Path.fromPathString("$USER_HOME/ghidra_scripts")); - paths.add(Path.fromPathString("$GHIDRA_HOME/Features/Base/ghidra_scripts")); - paths.add(Path.fromPathString("/User/defined/invalid/directory")); + List bundleFiles = new ArrayList<>(); + bundleFiles.add(Path.fromPathString("$USER_HOME/ghidra_scripts")); + bundleFiles.add(Path.fromPathString("$GHIDRA_HOME/Features/Base/ghidra_scripts")); + bundleFiles.add(Path.fromPathString("/User/defined/invalid/directory")); BundleStatusComponentProvider bundleStatusComponentProvider = showProvider(BundleStatusComponentProvider.class); - bundleStatusComponentProvider.setPathsForTesting(paths); + bundleStatusComponentProvider.setBundleFilesForTesting(bundleFiles); waitForComponentProvider(BundleStatusComponentProvider.class); captureComponent(bundleStatusComponentProvider.getComponent());