From 79c99b01fdf76ab1be601955b468eaa589884447 Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Fri, 28 Aug 2020 19:54:42 -0400 Subject: [PATCH 1/3] GP-126 added cumulative analysis task times to stored Program Information --- .../core/analysis/AutoAnalysisManager.java | 14 ++ .../core/analysis/AutoAnalysisPlugin.java | 6 + .../core/analysis/StoredAnalyzerTimes.java | 170 ++++++++++++++++++ .../StoredAnalyzerTimesPropertyEditor.java | 131 ++++++++++++++ .../framework/options/AbstractOptions.java | 58 ++---- .../ghidra/framework/options/EditorState.java | 5 +- .../java/ghidra/framework/options/Option.java | 29 ++- 7 files changed, 359 insertions(+), 54 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimes.java create mode 100644 Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimesPropertyEditor.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java index 647e5d01eb..8658597847 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java @@ -800,6 +800,7 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl notifyAnalysisEnded(); if (printTaskTimes) { printTimedTasks(); + saveTaskTimes(); } } } @@ -1286,6 +1287,19 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl Msg.info(this, taskTimeString); } + private void saveTaskTimes() { + + StoredAnalyzerTimes times = StoredAnalyzerTimes.getStoredAnalyzerTimes(program); + + String taskNames[] = getTimedTasks(); + for (String element : taskNames) { + long taskTimeMSec = getTaskTime(timedTasks, element); + times.addTime(element, taskTimeMSec); + } + + StoredAnalyzerTimes.setStoredAnalyzerTimes(program, times); + } + /** * Schedule an analysis worker to run while auto analysis is suspended. Invocation will block * until callback is completed or cancelled. If an analysis task is busy, it will be allowed to diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java index bcb7542928..756e15aec0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisPlugin.java @@ -32,6 +32,7 @@ import ghidra.app.events.*; import ghidra.app.plugin.PluginCategoryNames; import ghidra.app.services.Analyzer; import ghidra.app.util.importer.MessageLog; +import ghidra.framework.options.OptionType; import ghidra.framework.options.Options; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; @@ -275,6 +276,11 @@ public class AutoAnalysisPlugin extends Plugin implements AutoAnalysisManagerLis } private void programActivated(final Program program) { + + program.getOptions(StoredAnalyzerTimes.OPTIONS_LIST) + .registerOption(StoredAnalyzerTimes.OPTION_NAME, OptionType.CUSTOM_TYPE, + null, null, "Cumulative analysis task times", + new StoredAnalyzerTimesPropertyEditor()); // invokeLater() to ensure that all other plugins have been notified of the program // activated. This makes sure plugins like the Listing have opened and painted the diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimes.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimes.java new file mode 100644 index 0000000000..e1f08c4352 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimes.java @@ -0,0 +1,170 @@ +/* ### + * 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.analysis; + +import java.util.*; + +import ghidra.framework.options.*; +import ghidra.program.model.listing.Program; + +/** + * StoredAnalyzerTimes provides a custom option container for + * accumulated analysis times for named tasks. + */ +public class StoredAnalyzerTimes implements CustomOption { + + public static final String OPTIONS_LIST = Program.PROGRAM_INFO + ".Analysis Times"; + public static final String OPTION_NAME = "Times"; + + private Map taskTimes = new HashMap<>(); + private Long totalTime; + private String[] names; + + @Override + public void readState(SaveState saveState) { + taskTimes.clear(); + for (String taskName : saveState.getNames()) { + taskTimes.put(taskName, saveState.getLong(taskName, 0)); + } + names = null; + totalTime = null; + } + + @Override + public void writeState(SaveState saveState) { + for (String taskName : taskTimes.keySet()) { + saveState.putLong(taskName, taskTimes.get(taskName)); + } + } + + /** + * Clear all task entries and times + */ + public void clear() { + taskTimes.clear(); + names = null; + totalTime = null; + } + + /** + * Determine if any task times exist + * @return true if no task times available, else false + */ + public boolean isEmpty() { + return taskTimes.isEmpty(); + } + + /** + * Clear time entry corresponding to specified taskName + * @param taskName analysis task name + */ + public void clear(String taskName) { + taskTimes.remove(taskName); + names = null; + totalTime = null; + } + + /** + * Add the specified time corresponding to the specified analysis taskName + * @param taskName analysis task name + * @param t time increment + */ + public void addTime(String taskName, long t) { + long cumulativeTime = taskTimes.getOrDefault(taskName, 0L) + t; + taskTimes.put(taskName, cumulativeTime); + names = null; + totalTime = null; + } + + /** + * Get the accumulated time for the specified analysis taskName + * @param taskName analysis task name + * @return accumulated task time or null if entry not found + */ + public Long getTime(String taskName) { + return taskTimes.get(taskName); + } + + /** + * Get the total accumulated task time for all task entries + * @return total accumuated task time + */ + public long getTotalTime() { + if (totalTime == null) { + long sum = 0; + for (long t : taskTimes.values()) { + sum += t; + } + totalTime = sum; + } + return totalTime; + } + + /** + * Get all task names for which time entries exist + * @return array of task names + */ + public String[] getTaskNames() { + if (names == null) { + names = taskTimes.keySet().toArray(new String[taskTimes.size()]); + Arrays.sort(names); + } + return names; + } + + @Override + public int hashCode() { + return taskTimes.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StoredAnalyzerTimes)) { + return false; + } + return taskTimes.equals(((StoredAnalyzerTimes) obj).taskTimes); + } + + @Override + public StoredAnalyzerTimes clone() { + StoredAnalyzerTimes newInstance = new StoredAnalyzerTimes(); + newInstance.taskTimes = new HashMap<>(taskTimes); + return newInstance; + } + + /** + * Get the StoredAnalyzerTimes options data from the specified program + * @param program program + * @return StoredAnalyzerTimes option data + */ + public static StoredAnalyzerTimes getStoredAnalyzerTimes(Program program) { + Options options = program.getOptions(OPTIONS_LIST); + StoredAnalyzerTimes times = (StoredAnalyzerTimes) options + .getCustomOption(StoredAnalyzerTimes.OPTION_NAME, new StoredAnalyzerTimes()); + return times; + } + + /** + * Set the updated StoredAnalyzerTimes option data on the specified program + * @param program program + * @param times StoredAnalyzerTimes option data + */ + public static void setStoredAnalyzerTimes(Program program, StoredAnalyzerTimes times) { + Options options = program.getOptions(OPTIONS_LIST); + options.putObject(StoredAnalyzerTimes.OPTION_NAME, times); + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimesPropertyEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimesPropertyEditor.java new file mode 100644 index 0000000000..b8ccc8142d --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/StoredAnalyzerTimesPropertyEditor.java @@ -0,0 +1,131 @@ +/* ### + * 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.analysis; + +import java.awt.*; +import java.beans.PropertyEditorSupport; + +import javax.swing.*; + +import docking.widgets.label.GDLabel; +import ghidra.framework.options.CustomOptionsEditor; +import ghidra.util.StringUtilities; +import ghidra.util.layout.PairLayout; + +/** + * StoredAnalyzerTimesPropertyEditor implements a custom option + * editor panel for {@link StoredAnalyzerTimes}. Ability to edit values + * is disabled with panel intended for display purpose only. + */ +public class StoredAnalyzerTimesPropertyEditor extends PropertyEditorSupport + implements CustomOptionsEditor { + + private StoredAnalyzerTimes times; + + @Override + public boolean supportsCustomEditor() { + return true; + } + + @Override + public String[] getOptionNames() { + if (times == null) { + return new String[0]; + } + return times.getTaskNames(); + } + + @Override + public String[] getOptionDescriptions() { + return null; + } + + @Override + public void setValue(Object value) { + if (!(value instanceof StoredAnalyzerTimes)) { + return; + } + + times = (StoredAnalyzerTimes) value; + firePropertyChange(); + } + + @Override + public Object getValue() { + return times.clone(); + } + + @Override + public Component getCustomEditor() { + return buildEditor(); + } + + /** + * Build analysis time panel showing all task names and corresponding + * cumulative times in seconds. Edit ability is disabled. + * @return options panel + */ + private Component buildEditor() { + + if (times == null || times.isEmpty()) { + JPanel panel = new JPanel(new FlowLayout()); + panel.add(new JLabel("No Data Available")); + return panel; + } + + JPanel panel = new JPanel(new PairLayout(6, 10)); + + panel.add(new GDLabel("")); + GDLabel label = new GDLabel("seconds", SwingConstants.RIGHT); + panel.add(label); + + for (String taskName : getOptionNames()) { + label = new GDLabel(taskName, SwingConstants.RIGHT); + label.setToolTipText(taskName); + panel.add(label); + + Long timeMS = times.getTime(taskName); + if (timeMS == null) { + continue; + } + + JTextField valueField = new JTextField(formatTimeMS(timeMS)); + valueField.setEditable(false); + valueField.setHorizontalAlignment(SwingConstants.RIGHT); + panel.add(valueField); + } + + label = new GDLabel("TOTAL", SwingConstants.RIGHT); + label.setFont(label.getFont().deriveFont(Font.BOLD)); + panel.add(label); + + JTextField valueField = new JTextField(formatTimeMS(times.getTotalTime())); + valueField.setEditable(false); + valueField.setHorizontalAlignment(SwingConstants.RIGHT); + valueField.setBorder(BorderFactory.createLineBorder(Color.black, 2)); + panel.add(valueField); + + return panel; + } + + private String formatTimeMS(long timeMS) { + String str = Long.toUnsignedString(timeMS / 1000L); + str += "."; + str += StringUtilities.pad(Long.toUnsignedString(timeMS % 1000L), '0', 3); + return str; + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java index d1a05000c3..a533133e06 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java @@ -144,63 +144,25 @@ public abstract class AbstractOptions implements Options { } Option currentOption = valueMap.get(optionName); - if (currentOption == null) { + if (currentOption == null || + !isCompatibleOption(currentOption, type, defaultValue, editor)) { Option option = createRegisteredOption(optionName, type, description, help, defaultValue, editor); valueMap.put(optionName, option); - return; - } - - Option newOption = null; - if (currentOption.isRegistered()) { - // Registered again - newOption = - copyRegisteredOption(currentOption, type, description, defaultValue, help, editor); } else { - // option was accessed, but not registered - newOption = - createRegisteredOption(optionName, type, description, help, defaultValue, editor); + currentOption.updateRegistration(description, help, defaultValue, editor); } - - copyCurrentValue(currentOption, newOption); - valueMap.put(optionName, newOption); } - protected void copyCurrentValue(Option currentOption, Option newOption) { - if (currentOption.isDefault()) { - return; // don't copy the current value if it is just the old default. + private boolean isCompatibleOption(Option option, OptionType type, Object defaultValue, + PropertyEditor editor) { + if (option.getOptionType() != type) { + return false; } - - Object currentValue = currentOption.getCurrentValue(); - OptionType type = currentOption.getOptionType(); - if (!isNullable(type) && currentValue == null) { - return; // not allowed to be null - } - - // null is allowed; null can represent a valid 'cleared' state - newOption.setCurrentValue(currentValue); - - } - - private Option copyRegisteredOption(Option currentOption, OptionType type, - String description, Object defaultValue, HelpLocation help, PropertyEditor editor) { - - // We probably don't need to do anything special if we are re-registering an option, - // which is what the below code handles - String oldDescription = currentOption.getDescription(); - HelpLocation oldHelp = currentOption.getHelpLocation(); - Object oldDefaultValue = currentOption.getDefaultValue(); - PropertyEditor oldEditor = currentOption.getPropertyEditor(); - - String newDescripiton = oldDescription == null ? description : oldDescription; - HelpLocation newHelpLocation = oldHelp == null ? help : oldHelp; - Object newDefaultValue = oldDefaultValue == null ? defaultValue : oldDefaultValue; - PropertyEditor newEditor = oldEditor == null ? editor : oldEditor; - - String optionName = currentOption.getName(); - return createRegisteredOption(optionName, type, newDescripiton, newHelpLocation, - newDefaultValue, newEditor); + Object optionValue = option.getValue(null); + return optionValue == null || defaultValue == null || + optionValue.getClass().equals(defaultValue.getClass()); } @Override diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java index c98f009d7a..666d914c6e 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java @@ -20,6 +20,7 @@ import java.beans.*; import java.util.HashSet; import java.util.Set; +import ghidra.framework.Application; import ghidra.util.SystemUtilities; public class EditorState implements PropertyChangeListener { @@ -146,7 +147,9 @@ public class EditorState implements PropertyChangeListener { editor.removePropertyChangeListener(this); editor = new ErrorPropertyEditor( - "Ghidra does not know how to use PropertyEditor: " + editor.getClass().getName(), null); + Application.getName() + " does not know how to use PropertyEditor: " + + editor.getClass().getName(), + null); return editor.getCustomEditor(); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Option.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Option.java index ce5e5c258a..90c3c293b1 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Option.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/Option.java @@ -24,13 +24,13 @@ import utilities.util.reflection.ReflectionUtilities; public abstract class Option { private final String name; - private final Object defaultValue; + private Object defaultValue; private boolean isRegistered; - private final String description; - private final HelpLocation helpLocation; - private final OptionType optionType; + private String description; + private HelpLocation helpLocation; + private OptionType optionType; - private final PropertyEditor propertyEditor; + private PropertyEditor propertyEditor; private String inceptionInformation; protected Option(String name, OptionType optionType, String description, @@ -48,6 +48,25 @@ public abstract class Option { } } + /** + * Update any null registration information using the specified updated information. + * This assumption is that multiple registrations for the same option may be partial + * but will be compatible. No compatibility checks are performed between existing + * registration and update registration info. + * @param updatedDescription updated option description (will be ignored if description previously set) + * @param updatedHelp updated option help location (will be ignored if help location previously set) + * @param updatedDefaultValue updated option default-value (will be ignored if default-value previously set) + * @param updatedEditor updated option editor (will be ignored if editor previously set) + */ + final void updateRegistration(String updatedDescription, HelpLocation updatedHelp, + Object updatedDefaultValue, PropertyEditor updatedEditor) { + description = description != null ? description : updatedDescription; + helpLocation = helpLocation != null ? helpLocation : updatedHelp; + defaultValue = defaultValue != null ? defaultValue : updatedDefaultValue; + propertyEditor = propertyEditor != null ? propertyEditor : updatedEditor; + isRegistered = true; + } + public abstract Object getCurrentValue(); public abstract void doSetCurrentValue(Object value); From 19646075940deb10007fe4c5dcbad92e0f42fac2 Mon Sep 17 00:00:00 2001 From: ghidravore Date: Mon, 31 Aug 2020 16:15:38 -0400 Subject: [PATCH 2/3] Tweaked the registerOption code. --- .../framework/options/AbstractOptions.java | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java index a533133e06..45d9ef8d7e 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/AbstractOptions.java @@ -29,6 +29,7 @@ import ghidra.util.HelpLocation; import ghidra.util.Msg; import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakSet; +import ghidra.util.exception.AssertException; import utilities.util.reflection.ReflectionUtilities; public abstract class AbstractOptions implements Options { @@ -143,20 +144,45 @@ public abstract class AbstractOptions implements Options { ReflectionUtilities.createJavaFilteredThrowable()); } - Option currentOption = valueMap.get(optionName); - if (currentOption == null || - !isCompatibleOption(currentOption, type, defaultValue, editor)) { - Option option = - createRegisteredOption(optionName, type, description, help, defaultValue, editor); - valueMap.put(optionName, option); - } - else { + Option currentOption = getExistingComptibleOption(optionName, type, defaultValue); + if (currentOption != null) { currentOption.updateRegistration(description, help, defaultValue, editor); + return; } + + Option option = + createRegisteredOption(optionName, type, description, help, defaultValue, editor); + + valueMap.put(optionName, option); } - private boolean isCompatibleOption(Option option, OptionType type, Object defaultValue, - PropertyEditor editor) { + private Option getExistingComptibleOption(String optionName, OptionType type, + Object defaultValue) { + + // There are several cases where an existing option may exist when registering an option + // 1) the option was accessed before it was registered + // 2) the option was loaded from a store (database or toolstate) + // 3) the option was registered more than once. + // + // The only time this is a problem is if the exiting option type is not compatible with + // the type being registered. If we encounter an incompatible option, we just log a + // warning and return null so that the new option will replace it. Otherwise, we return + // the existing option so it can be updated with the data from the registration. + + Option option = valueMap.get(optionName); + if (option == null) { + return null; + } + + if (!isCompatibleOption(option, type, defaultValue)) { + Msg.error(this, "Registered option incompatible with existing option: " + optionName, + new AssertException()); + return null; + } + return option; + } + + private boolean isCompatibleOption(Option option, OptionType type, Object defaultValue) { if (option.getOptionType() != type) { return false; } From 9d759a0e7d7e5fb07d8cbab8ace242f20e49c38f Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Tue, 8 Sep 2020 14:45:01 -0400 Subject: [PATCH 3/3] GP-126 improve resiliance to missing custom option support --- .../main/java/ghidra/framework/options/EditorState.java | 8 +++++++- .../main/java/ghidra/framework/options/OptionType.java | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java index 666d914c6e..aa115f42cb 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java @@ -120,10 +120,16 @@ public class EditorState implements PropertyChangeListener { * directly, as opposed to using the generic framework. */ public boolean supportsCustomOptionsEditor() { - return (editor instanceof CustomOptionsEditor); + return editor == null || (editor instanceof CustomOptionsEditor); } public Component getEditorComponent() { + if (editor == null) { + // can occur if support has been dropped for custom state/option + editor = new ErrorPropertyEditor( + "Ghidra does not know how to render state: " + name, null); + return editor.getCustomEditor(); + } if (editor.supportsCustomEditor()) { return editor.getCustomEditor(); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/OptionType.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/OptionType.java index 443733b5b9..9be0fc9bfc 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/OptionType.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/OptionType.java @@ -223,8 +223,13 @@ public enum OptionType { option.readState(saveState); return option; } + catch (ClassNotFoundException e) { + Msg.warn(this, + "Ignoring unsupported customOption instance for: " + customOptionClassName); + } catch (Exception e) { - Msg.error(this, "Can't create customOption instance for: " + customOptionClassName, + Msg.error(this, + "Can't create customOption instance for: " + customOptionClassName + e); } return null;