diff --git a/Ghidra/Framework/Docking/src/main/java/docking/AbstractErrDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/AbstractErrDialog.java new file mode 100644 index 0000000000..0a97580c63 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/AbstractErrDialog.java @@ -0,0 +1,47 @@ +/* ### + * 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 docking; + +import utility.function.Callback; + +/** + * A dialog that is meant to be extended for showing exceptions + */ +abstract class AbstractErrDialog extends DialogComponentProvider { + + // at some point, there are too many exceptions to show + protected static final int MAX_EXCEPTIONS = 100; + protected static final String TITLE_TEXT = "Multiple Errors"; + + private Callback closedCallback = Callback.dummy(); + + protected AbstractErrDialog(String title) { + super(title, true, false, true, false); + } + + @Override + protected final void dialogClosed() { + closedCallback.call(); + } + + abstract void addException(String message, Throwable t); + + abstract int getExceptionCount(); + + void setClosedCallback(Callback callback) { + closedCallback = Callback.dummyIfNull(callback); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DockingErrorDisplay.java b/Ghidra/Framework/Docking/src/main/java/docking/DockingErrorDisplay.java index 1a12aa4974..e215803ad5 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DockingErrorDisplay.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DockingErrorDisplay.java @@ -17,16 +17,23 @@ package docking; import java.awt.Component; import java.awt.Window; -import java.io.*; -import docking.widgets.OkDialog; import docking.widgets.OptionDialog; import ghidra.util.*; import ghidra.util.exception.MultipleCauses; public class DockingErrorDisplay implements ErrorDisplay { - private static final int TRACE_BUFFER_SIZE = 250; + /** + * Error dialog used to append exceptions. + * + *

While this dialog is showing all new exceptions will be added to the dialog. When + * this dialog is closed, this reference will be cleared. + * + *

Note: all use of this variable must be on the Swing thread to avoid thread + * visibility issues. + */ + private static AbstractErrDialog activeDialog; ConsoleErrorDisplay consoleDisplay = new ConsoleErrorDisplay(); @@ -52,8 +59,8 @@ public class DockingErrorDisplay implements ErrorDisplay { private void displayMessage(MessageType messageType, ErrorLogger errorLogger, Object originator, Component parent, String title, Object message, Throwable throwable) { - int dialogType = OptionDialog.PLAIN_MESSAGE; + int dialogType = OptionDialog.PLAIN_MESSAGE; String messageString = message != null ? message.toString() : null; String rawMessage = HTMLUtilities.fromHTML(messageString); switch (messageType) { @@ -75,7 +82,7 @@ public class DockingErrorDisplay implements ErrorDisplay { break; } - showDialog(title, message, throwable, dialogType, messageString, getWindow(parent)); + showDialog(title, throwable, dialogType, messageString, getWindow(parent)); } private Component getWindow(Component component) { @@ -85,33 +92,36 @@ public class DockingErrorDisplay implements ErrorDisplay { return component; } - private void showDialog(final String title, final Object message, final Throwable throwable, + private void showDialog(final String title, final Throwable throwable, final int dialogType, final String messageString, final Component parent) { - SystemUtilities.runIfSwingOrPostSwingLater( - () -> doShowDialog(title, message, throwable, dialogType, messageString, parent)); + Swing.runIfSwingOrRunLater( + () -> showDialogOnSwing(title, throwable, dialogType, messageString, parent)); } - private void doShowDialog(final String title, final Object message, final Throwable throwable, + private void showDialogOnSwing(String title, Throwable throwable, int dialogType, String messageString, Component parent) { - DialogComponentProvider dialog = null; - if (throwable != null) { - dialog = createErrorDialog(title, message, throwable, messageString); + + if (activeDialog != null) { + activeDialog.addException(messageString, throwable); + return; } - else { - dialog = new OkDialog(title, messageString, dialogType); - } - DockingWindowManager.showDialog(parent, dialog); + + activeDialog = createErrorDialog(title, throwable, messageString); + activeDialog.setClosedCallback(() -> { + activeDialog.setClosedCallback(null); + activeDialog = null; + }); + DockingWindowManager.showDialog(parent, activeDialog); } - private DialogComponentProvider createErrorDialog(final String title, final Object message, - final Throwable throwable, String messageString) { + private AbstractErrDialog createErrorDialog(String title, Throwable throwable, + String messageString) { if (containsMultipleCauses(throwable)) { return new ErrLogExpandableDialog(title, messageString, throwable); } - return ErrLogDialog.createExceptionDialog(title, messageString, - buildStackTrace(throwable, message == null ? throwable.getMessage() : messageString)); + return ErrLogDialog.createExceptionDialog(title, messageString, throwable); } private boolean containsMultipleCauses(Throwable throwable) { @@ -125,34 +135,4 @@ public class DockingErrorDisplay implements ErrorDisplay { return containsMultipleCauses(throwable.getCause()); } - - /** - * Build a displayable stack trace from a Throwable - * - * @param t the throwable - * @param msg message prefix - * @return multi-line stack trace - */ - private String buildStackTrace(Throwable t, String msg) { - StringBuffer sb = new StringBuffer(TRACE_BUFFER_SIZE); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PrintStream ps = new PrintStream(baos); - - if (msg != null) { - ps.println(msg); - } - - t.printStackTrace(ps); - sb.append(baos.toString()); - ps.close(); - try { - baos.close(); - } - catch (IOException e) { - // shouldn't happen--not really connected to the system - } - - return sb.toString(); - } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ErrLogDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/ErrLogDialog.java index 5b036ed161..b086f80f1d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/ErrLogDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/ErrLogDialog.java @@ -20,20 +20,35 @@ import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; import javax.swing.*; import docking.widgets.ScrollableTextArea; import docking.widgets.label.GHtmlLabel; import docking.widgets.label.GIconLabel; +import docking.widgets.table.*; +import generic.json.Json; import generic.util.WindowUtilities; +import ghidra.docking.settings.Settings; import ghidra.framework.Application; +import ghidra.framework.plugintool.ServiceProvider; +import ghidra.framework.plugintool.ServiceProviderStub; import ghidra.util.HTMLUtilities; +import ghidra.util.Swing; +import ghidra.util.table.column.DefaultTimestampRenderer; +import ghidra.util.table.column.GColumnRenderer; +import utilities.util.reflection.ReflectionUtilities; -public class ErrLogDialog extends DialogComponentProvider { - private static final int TEXT_ROWS = 30; +/** + * A dialog that takes error text and displays it with an option details button. If there is + * an {@link ErrorReporter}, then a button is provided to report the error. + */ +public class ErrLogDialog extends AbstractErrDialog { + private static final int TEXT_ROWS = 20; private static final int TEXT_COLUMNS = 80; - private static final int ERROR_BUFFER_SIZE = 1024; private static final String SEND = "Log Error..."; private static final String DETAIL = "Details >>>"; @@ -46,32 +61,30 @@ public class ErrLogDialog extends DialogComponentProvider { /** tracks 'details panel' open state across invocations */ private static boolean isShowingDetails = false; + private int errorId = 0; + // state-dependent gui members - private ErrorDetailsPanel detailsPanel; + private ErrorDetailsSplitPane detailsPane; private JButton detailsButton; private JButton sendButton; private JPanel mainPanel; private static ErrorReporter errorReporter; - public static ErrLogDialog createExceptionDialog(String title, String message, String details) { - return new ErrLogDialog(title, message, details, true); + private List errors = new ArrayList<>(); + + public static ErrLogDialog createExceptionDialog(String title, String message, Throwable t) { + return new ErrLogDialog(title, message, t); } - public static ErrLogDialog createLogMessageDialog(String title, String message, - String details) { - return new ErrLogDialog(title, message, details, false); - } + private ErrLogDialog(String title, String message, Throwable throwable) { + super(title != null ? title : "Error"); + + ErrEntry error = new ErrEntry(message, throwable); + errors.add(error); - /** - * Constructor. - * Used by the Err class's static methods for logging various - * kinds of errors: Runtime, System, User, Asserts - */ - private ErrLogDialog(String title, String message, String details, boolean isException) { - super(title != null ? title : "Error", true, false, true, false); setRememberSize(false); setRememberLocation(false); - buildMainPanel(message, addUsefulReportingInfo(details), isException); + buildMainPanel(message); } private String addUsefulReportingInfo(String details) { @@ -127,13 +140,21 @@ public class ErrLogDialog extends DialogComponentProvider { return errorReporter; } - private void buildMainPanel(String message, String details, boolean isException) { + private void buildMainPanel(String message) { JPanel introPanel = new JPanel(new BorderLayout(10, 10)); introPanel.add( new GIconLabel(UIManager.getIcon("OptionPane.errorIcon"), SwingConstants.RIGHT), BorderLayout.WEST); - introPanel.add(new GHtmlLabel(HTMLUtilities.toHTML(message)), BorderLayout.CENTER); + introPanel.add(new GHtmlLabel(HTMLUtilities.toHTML(message)) { + @Override + public Dimension getPreferredSize() { + // rendering HTML the label can expand larger than the screen; keep it reasonable + Dimension size = super.getPreferredSize(); + size.width = 300; + return size; + } + }, BorderLayout.CENTER); mainPanel = new JPanel(new BorderLayout(10, 20)); mainPanel.add(introPanel, BorderLayout.NORTH); @@ -141,21 +162,15 @@ public class ErrLogDialog extends DialogComponentProvider { sendButton = new JButton(SEND); sendButton.addActionListener(e -> sendDetails()); - detailsPanel = new ErrorDetailsPanel(); detailsButton = new JButton(isShowingDetails ? CLOSE : DETAIL); detailsButton.addActionListener(e -> { String label = detailsButton.getText(); showDetails(label.equals(DETAIL)); }); - if (isException) { - detailsPanel.setExceptionMessage(details); - } - else { - detailsPanel.setLogMessage(details); - } + detailsPane = new ErrorDetailsSplitPane(); - JPanel buttonPanel = new JPanel(new GridLayout(2, 1, 5, 5)); + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING, 5, 5)); buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); if (errorReporter != null) { buttonPanel.add(sendButton); @@ -163,16 +178,16 @@ public class ErrLogDialog extends DialogComponentProvider { buttonPanel.add(detailsButton); introPanel.add(buttonPanel, BorderLayout.EAST); - mainPanel.add(detailsPanel, BorderLayout.CENTER); + mainPanel.add(detailsPane, BorderLayout.CENTER); addWorkPanel(mainPanel); addOKButton(); + setDefaultButton(okButton); // show the details panel if it was showing previously - detailsPanel.setVisible(isShowingDetails); - -// setHelpLocation(new HelpLocation(HelpTopics.INTRO, "Err")); + detailsPane.setVisible(isShowingDetails); + detailsPane.selectFirstError(); } @Override @@ -185,56 +200,156 @@ public class ErrLogDialog extends DialogComponentProvider { cancelCallback(); } - /** - * Send error details from dialog. - */ private void sendDetails() { - String details = detailsPanel.getDetails(); + String details = detailsPane.getDetails(); String title = getTitle(); close(); errorReporter.report(rootPanel, title, details); } - /** - * opens and closes the details panel; used also by Err when - * showLog is called from SessionGui Help menu to show details - * when visible - */ private void showDetails(boolean visible) { isShowingDetails = visible; String label = (visible ? CLOSE : DETAIL); detailsButton.setText(label); - detailsPanel.setVisible(visible); + detailsPane.setVisible(visible); repack(); // need to re-pack so the detailsPanel can be hidden correctly } - // custom "pack" so the detailsPanel can be shown/hidden correctly - @Override - protected void repack() { - - // hide the dialog so that the user doesn't see us resize and then move it, which looks - // awkward - getDialog().setVisible(false); - - detailsPanel.invalidate(); // force to be invalid so resizes correctly - rootPanel.validate(); - - super.repack(); - - // center the dialog after its size changes for a cleaner appearance - DockingDialog dialog = getDialog(); - Container parent = dialog.getParent(); - Point centerPoint = WindowUtilities.centerOnComponent(parent, dialog); - dialog.setLocation(centerPoint); - - getDialog().setVisible(true); - } - @Override protected void dialogShown() { - - // TODO test that the parent DockingDialog code handles this.... WindowUtilities.ensureOnScreen(getDialog()); + Swing.runLater(() -> okButton.requestFocusInWindow()); + } + + @Override + void addException(String message, Throwable t) { + + int n = errors.size(); + if (n > MAX_EXCEPTIONS) { + return; + } + + errors.add(new ErrEntry(message, t)); + + detailsPane.update(); + + // signal the new error + setTitle(TITLE_TEXT + " (" + n + 1 + ")"); + } + + @Override + int getExceptionCount() { + return errors.size(); + } + + private class ErrorDetailsSplitPane extends JSplitPane { + + private final double TOP_PREFERRED_RESIZE_WEIGHT = .80; + private ErrorDetailsPanel detailsPanel; + private ErrorDetailsTablePanel tablePanel; + + private Dimension openedSize; + + ErrorDetailsSplitPane() { + super(VERTICAL_SPLIT); + setResizeWeight(TOP_PREFERRED_RESIZE_WEIGHT); + + detailsPanel = new ErrorDetailsPanel(); + tablePanel = new ErrorDetailsTablePanel(); + + setTopComponent(detailsPanel); + setBottomComponent(tablePanel); + + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent event) { + if (!isShowing()) { + return; + } + Rectangle localBounds = getBounds(); + if (!detailsButton.getText().equals(DETAIL)) { + openedSize = new Dimension(localBounds.width, localBounds.height); + } + } + }); + } + + void selectFirstError() { + tablePanel.selectFirstError(); + } + + String getDetails() { + return detailsPanel.getDetails(); + } + + void setExceptionMessage(String s) { + detailsPanel.setExceptionMessage(s); + } + + void update() { + tablePanel.update(); + } + + @Override + public Dimension getPreferredSize() { + Dimension superSize = super.getPreferredSize(); + if (detailsButton.getText().equals(DETAIL)) { + return superSize; + } + + if (openedSize == null) { + return superSize; + } + + return openedSize; + } + + } + + private class ErrorDetailsTablePanel extends JPanel { + + private ErrEntryTableModel model; + private GTable errorsTable; + private GTableFilterPanel tableFilterPanel; + + ErrorDetailsTablePanel() { + setLayout(new BorderLayout()); + model = new ErrEntryTableModel(); + errorsTable = new GTable(model); + tableFilterPanel = new GTableFilterPanel(errorsTable, model); + + errorsTable.getSelectionManager().addListSelectionListener(e -> { + if (e.getValueIsAdjusting()) { + return; + } + + int firstIndex = errorsTable.getSelectedRow(); + if (firstIndex == -1) { + return; + } + ErrEntry err = tableFilterPanel.getRowObject(firstIndex); + detailsPane.setExceptionMessage(err.getDetailsText()); + }); + + JPanel tablePanel = new JPanel(new BorderLayout()); + tablePanel.add(new JScrollPane(errorsTable), BorderLayout.CENTER); + tablePanel.add(tableFilterPanel, BorderLayout.SOUTH); + + add(tablePanel, BorderLayout.CENTER); + + // initialize this value to something small so the full dialog will not consume the + // entire screen height + setPreferredSize(new Dimension(400, 100)); + } + + void selectFirstError() { + errorsTable.selectRow(0); + } + + private void update() { + model.fireTableDataChanged(); + } + } /** @@ -244,76 +359,178 @@ public class ErrLogDialog extends DialogComponentProvider { */ private class ErrorDetailsPanel extends JPanel { private ScrollableTextArea textDetails; - private StringBuffer errorDetailsBuffer; - private Dimension closedSize; - private Dimension openedSize; private ErrorDetailsPanel() { super(new BorderLayout(0, 0)); - errorDetailsBuffer = new StringBuffer(ERROR_BUFFER_SIZE); textDetails = new ScrollableTextArea(TEXT_ROWS, TEXT_COLUMNS); textDetails.setEditable(false); + add(textDetails, BorderLayout.CENTER); + validate(); textDetails.scrollToBottom(); - - // set the initial preferred size of this panel - // when "closed" - Rectangle bounds = getBounds(); - closedSize = new Dimension(bounds.width, 0); - - addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent event) { - if (!isShowing()) { - return; - } - Rectangle localBounds = getBounds(); - if (detailsButton.getText().equals(DETAIL)) { - closedSize.width = localBounds.width; - } - else { - openedSize = new Dimension(localBounds.width, localBounds.height); - } - } - }); } - @Override - public Dimension getPreferredSize() { - if (detailsButton.getText().equals(DETAIL)) { - return closedSize; - } + private void setExceptionMessage(String message) { - if (openedSize == null) { - return super.getPreferredSize(); - } - - return openedSize; - } - - /** - * resets the current error buffer to the contents of msg - */ - private void setLogMessage(String msg) { - errorDetailsBuffer = new StringBuffer(msg); - textDetails.setText(msg); - - // scroll to bottom so user is viewing the last message - textDetails.scrollToBottom(); - } - - private void setExceptionMessage(String msg) { - errorDetailsBuffer = new StringBuffer(msg); - textDetails.setText(msg); + String updated = addUsefulReportingInfo(message); + textDetails.setText(updated); // scroll to the top the see the pertinent part of the exception textDetails.scrollToTop(); } - private final String getDetails() { - return errorDetailsBuffer.toString(); + private String getDetails() { + return textDetails.getText(); } } + private class ErrEntry { + + private String message; + private String details; + private Date timestamp = new Date(); + private int myId = ++errorId; + + ErrEntry(String message, Throwable t) { + String updated = message; + if (HTMLUtilities.isHTML(updated)) { + updated = HTMLUtilities.fromHTML(updated); + } + this.message = updated; + + if (t != null) { + this.details = ReflectionUtilities.stackTraceToString(t); + } + } + + int getId() { + return myId; + } + + String getMessage() { + return message; + } + + Date getTimestamp() { + return timestamp; + } + + String getDetailsText() { + if (details == null) { + return message; + } + return details; + } + + String getDetails() { + return details; + } + + @Override + public String toString() { + return Json.toString(this); + } + } + + private class ErrEntryTableModel extends GDynamicColumnTableModel { + + public ErrEntryTableModel() { + super(new ServiceProviderStub()); + } + + @Override + protected TableColumnDescriptor createTableColumnDescriptor() { + TableColumnDescriptor descriptor = new TableColumnDescriptor(); + descriptor.addVisibleColumn(new IdColumn(), 1, true); + descriptor.addVisibleColumn(new MessageColumn()); + descriptor.addHiddenColumn(new DetailsColumn()); + descriptor.addVisibleColumn(new TimestampColumn()); + return descriptor; + } + + @Override + public String getName() { + return "Unexpectd Errors"; + } + + @Override + public List getModelData() { + return errors; + } + + @Override + public Object getDataSource() { + return null; + } + + private class IdColumn extends AbstractDynamicTableColumnStub { + + @Override + public Integer getValue(ErrEntry rowObject, Settings settings, ServiceProvider sp) + throws IllegalArgumentException { + return rowObject.getId(); + } + + @Override + public String getColumnName() { + return "#"; + } + + @Override + public int getColumnPreferredWidth() { + return 40; + } + } + + private class MessageColumn extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(ErrEntry rowObject, Settings settings, ServiceProvider sp) + throws IllegalArgumentException { + return rowObject.getMessage(); + } + + @Override + public String getColumnName() { + return "Message"; + } + + } + + private class DetailsColumn extends AbstractDynamicTableColumnStub { + + @Override + public String getValue(ErrEntry rowObject, Settings settings, ServiceProvider sp) + throws IllegalArgumentException { + return rowObject.getDetails(); + } + + @Override + public String getColumnName() { + return "Details"; + } + } + + private class TimestampColumn extends AbstractDynamicTableColumnStub { + + private GColumnRenderer renderer = new DefaultTimestampRenderer(); + + @Override + public Date getValue(ErrEntry rowObject, Settings settings, ServiceProvider sp) + throws IllegalArgumentException { + return rowObject.getTimestamp(); + } + + @Override + public String getColumnName() { + return "Time"; + } + + @Override + public GColumnRenderer getColumnRenderer() { + return renderer; + } + } + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/ErrLogExpandableDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/ErrLogExpandableDialog.java index d91ade235a..518b229cba 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/ErrLogExpandableDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/ErrLogExpandableDialog.java @@ -32,12 +32,12 @@ import docking.widgets.label.GHtmlLabel; import docking.widgets.tree.*; import docking.widgets.tree.support.GTreeDragNDropHandler; import ghidra.util.*; -import ghidra.util.exception.*; +import ghidra.util.exception.MultipleCauses; import ghidra.util.html.HTMLElement; import resources.ResourceManager; import util.CollectionUtils; -public class ErrLogExpandableDialog extends DialogComponentProvider { +public class ErrLogExpandableDialog extends AbstractErrDialog { public static ImageIcon IMG_REPORT = ResourceManager.loadImage("images/report.png"); public static ImageIcon IMG_EXCEPTION = ResourceManager.loadImage("images/exception.png"); public static ImageIcon IMG_FRAME_ELEMENT = @@ -53,113 +53,21 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { private static boolean showingDetails = false; protected ReportRootNode root; - protected GTree excTree; + protected GTree tree; + private List errors = new ArrayList<>(); /** This spacer addresses the optical impression that the message panel changes size when showing details */ protected Component horizontalSpacer; protected JButton detailButton; protected JButton sendButton; - protected boolean hasConsole = false; protected JPopupMenu popup; - protected static class ExcTreeTransferHandler extends TransferHandler - implements GTreeDragNDropHandler { + protected ErrLogExpandableDialog(String title, String msg, Throwable throwable) { + super(title); - protected ReportRootNode root; + errors.add(throwable); - public ExcTreeTransferHandler(ReportRootNode root) { - this.root = root; - } - - @Override - public DataFlavor[] getSupportedDataFlavors(List transferNodes) { - return new DataFlavor[] { DataFlavor.stringFlavor }; - } - - @Override - protected Transferable createTransferable(JComponent c) { - ArrayList nodes = new ArrayList<>(); - for (TreePath path : ((JTree) c).getSelectionPaths()) { - nodes.add((GTreeNode) path.getLastPathComponent()); - } - try { - return new StringSelection( - (String) getTransferData(nodes, DataFlavor.stringFlavor)); - } - catch (UnsupportedFlavorException e) { - Msg.debug(this, e.getMessage(), e); - } - return null; - } - - @Override - public Object getTransferData(List transferNodes, DataFlavor flavor) - throws UnsupportedFlavorException { - if (flavor != DataFlavor.stringFlavor) { - throw new UnsupportedFlavorException(flavor); - } - if (transferNodes.isEmpty()) { - return null; - } - if (transferNodes.size() == 1) { - GTreeNode node = transferNodes.get(0); - if (node instanceof NodeWithText) { - return ((NodeWithText) node).collectReportText(transferNodes, 0).trim(); - } - return null; - } - return root.collectReportText(transferNodes, 0).trim(); - } - - @Override - public boolean isStartDragOk(List dragUserData, int dragAction) { - for (GTreeNode node : dragUserData) { - if (node instanceof NodeWithText) { - return true; - } - } - return false; - } - - @Override - public int getSupportedDragActions() { - return DnDConstants.ACTION_COPY; - } - - @Override - public int getSourceActions(JComponent c) { - return COPY; - } - - @Override - public boolean isDropSiteOk(GTreeNode destUserData, DataFlavor[] flavors, int dropAction) { - return false; - } - - @Override - public void drop(GTreeNode destUserData, Transferable transferable, int dropAction) { - throw new UnsupportedOperationException(); - } - } - - public ErrLogExpandableDialog(String title, String msg, MultipleCauses mc) { - this(title, msg, mc.getCauses(), null, true, true); - } - - public ErrLogExpandableDialog(String title, String msg, Throwable exc) { - this(title, msg, Collections.singletonList(exc), HasConsoleText.Util.get(exc), true, true); - } - - public ErrLogExpandableDialog(String title, String msg, Collection report) { - this(title, msg, report, null, false, false); - } - - protected ErrLogExpandableDialog(String title, String msg, Collection report, - String console, boolean modal, boolean hasDismiss) { - super(title, modal); - - hasConsole = console != null; popup = new JPopupMenu(); JMenuItem menuCopy = new JMenuItem("Copy"); menuCopy.setActionCommand((String) TransferHandler.getCopyAction().getValue(Action.NAME)); @@ -173,13 +81,12 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { msgPanel.setLayout(new BorderLayout(16, 16)); msgPanel.setBorder(new EmptyBorder(16, 16, 16, 16)); { - JLabel msgText = new GHtmlLabel(getHTML(msg, report)) { + JLabel msgText = new GHtmlLabel(getHTML(msg, CollectionUtils.asSet(throwable))) { @Override public Dimension getPreferredSize() { - // when rendering HTML the label can expand larger than the screen; - // keep it reasonable + // rendering HTML the label can expand larger than the screen; keep it reasonable Dimension size = super.getPreferredSize(); - size.width = 500; + size.width = 300; return size; } }; @@ -206,7 +113,7 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { msgPanel.add(buttonBox, BorderLayout.EAST); horizontalSpacer = Box.createVerticalStrut(10); - horizontalSpacer.setVisible(showingDetails | hasConsole); + horizontalSpacer.setVisible(showingDetails); msgPanel.add(horizontalSpacer, BorderLayout.SOUTH); } workPanel.add(msgPanel, BorderLayout.NORTH); @@ -214,29 +121,8 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { Box workBox = Box.createVerticalBox(); { - if (hasConsole) { - JTextArea consoleText = new JTextArea(console); - JScrollPane consoleScroll = - new JScrollPane(consoleText, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED) { - - @Override - public Dimension getPreferredSize() { - Dimension dim = super.getPreferredSize(); - dim.height = 400; - dim.width = 800; // trial and error? - return dim; - } - }; - consoleText.setEditable(false); - consoleText.setBackground(Color.BLACK); - consoleText.setForeground(Color.WHITE); - consoleText.setFont(Font.decode("Monospaced")); - workBox.add(consoleScroll); - } - - root = new ReportRootNode(getTitle(), report); - excTree = new GTree(root) { + root = new ReportRootNode(getTitle(), CollectionUtils.asSet(throwable)); + tree = new GTree(root) { @Override public Dimension getPreferredSize() { @@ -249,19 +135,19 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { for (GTreeNode node : CollectionUtils.asIterable(root.iterator(true))) { if (node instanceof ReportExceptionNode) { - excTree.expandTree(node); + tree.expandTree(node); } } - excTree.setSelectedNode(root.getChild(0)); - excTree.setVisible(showingDetails); + tree.setSelectedNode(root.getChild(0)); + tree.setVisible(showingDetails); ExcTreeTransferHandler handler = new ExcTreeTransferHandler(root); - excTree.setDragNDropHandler(handler); - excTree.setTransferHandler(handler); - ActionMap map = excTree.getActionMap(); + tree.setDragNDropHandler(handler); + tree.setTransferHandler(handler); + ActionMap map = tree.getActionMap(); map.put(TransferHandler.getCopyAction().getValue(Action.NAME), TransferHandler.getCopyAction()); - excTree.addMouseListener(new MouseAdapter() { + tree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); @@ -279,22 +165,19 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { } }); - workBox.add(excTree); + workBox.add(tree); } workPanel.add(workBox, BorderLayout.CENTER); repack(); addWorkPanel(workPanel); - if (hasDismiss) { - addDismissButton(); - } + addDismissButton(); } private String getHTML(String msg, Collection report) { // - // TODO // Usage question: The content herein will be escaped unless you call addHTMLContenet(). // Further, clients can provide messages that contain HTML. Is there a // use case where we want to show escaped HTML content? @@ -332,14 +215,6 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { String htmlTMsg = addBR(tMsg); body.addElement("p").addHTMLContent(htmlTMsg); - if (t instanceof CausesImportant) { // I choose not to recurse - HTMLElement ul = body.addElement("ul"); - for (Throwable ts : MultipleCauses.Util.iterCauses(t)) { - String tsMsg = getMessage(ts); - String htmlTSMsg = addBR(tsMsg); - ul.addElement("li").addHTMLContent(htmlTSMsg); - } - } } return html.toString(); } @@ -357,15 +232,15 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { return t.getClass().getSimpleName(); } - void detailCallback() { + private void detailCallback() { showingDetails = !showingDetails; - excTree.setVisible(showingDetails); - horizontalSpacer.setVisible(showingDetails | hasConsole); + tree.setVisible(showingDetails); + horizontalSpacer.setVisible(showingDetails); detailButton.setText(showingDetails ? CLOSE : DETAIL); repack(); } - void sendCallback() { + private void sendCallback() { String details = root.collectReportText(null, 0).trim(); String title = getTitle(); close(); @@ -379,6 +254,24 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { return dim; } + @Override + public void addException(String message, Throwable t) { + + int n = errors.size(); + if (n > MAX_EXCEPTIONS) { + return; + } + + errors.add(t); + setTitle(TITLE_TEXT + " (" + n + 1 + ")"); // signal the new error + root.addNode(new ReportExceptionNode(t)); + } + + @Override + int getExceptionCount() { + return root.getChildCount(); + } + static interface NodeWithText { public String getReportText(); @@ -528,9 +421,6 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { @Override public String getReportText() { - if (exc instanceof HasConsoleText) { - return getName() + "\n" + HasConsoleText.Util.get(exc); - } return getName(); } @@ -661,6 +551,87 @@ public class ErrLogExpandableDialog extends DialogComponentProvider { return false; } } + + private static class ExcTreeTransferHandler extends TransferHandler + implements GTreeDragNDropHandler { + + protected ReportRootNode root; + + public ExcTreeTransferHandler(ReportRootNode root) { + this.root = root; + } + + @Override + public DataFlavor[] getSupportedDataFlavors(List transferNodes) { + return new DataFlavor[] { DataFlavor.stringFlavor }; + } + + @Override + protected Transferable createTransferable(JComponent c) { + ArrayList nodes = new ArrayList<>(); + for (TreePath path : ((JTree) c).getSelectionPaths()) { + nodes.add((GTreeNode) path.getLastPathComponent()); + } + try { + return new StringSelection( + (String) getTransferData(nodes, DataFlavor.stringFlavor)); + } + catch (UnsupportedFlavorException e) { + Msg.debug(this, e.getMessage(), e); + } + return null; + } + + @Override + public Object getTransferData(List transferNodes, DataFlavor flavor) + throws UnsupportedFlavorException { + if (flavor != DataFlavor.stringFlavor) { + throw new UnsupportedFlavorException(flavor); + } + if (transferNodes.isEmpty()) { + return null; + } + if (transferNodes.size() == 1) { + GTreeNode node = transferNodes.get(0); + if (node instanceof NodeWithText) { + return ((NodeWithText) node).collectReportText(transferNodes, 0).trim(); + } + return null; + } + return root.collectReportText(transferNodes, 0).trim(); + } + + @Override + public boolean isStartDragOk(List dragUserData, int dragAction) { + for (GTreeNode node : dragUserData) { + if (node instanceof NodeWithText) { + return true; + } + } + return false; + } + + @Override + public int getSupportedDragActions() { + return DnDConstants.ACTION_COPY; + } + + @Override + public int getSourceActions(JComponent c) { + return COPY; + } + + @Override + public boolean isDropSiteOk(GTreeNode destUserData, DataFlavor[] flavors, int dropAction) { + return false; + } + + @Override + public void drop(GTreeNode destUserData, Transferable transferable, int dropAction) { + throw new UnsupportedOperationException(); + } + } + } class TransferActionListener implements ActionListener, PropertyChangeListener { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java index c1b52b84f9..221667a736 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/ScrollableTextArea.java @@ -102,7 +102,7 @@ public class ScrollableTextArea extends JScrollPane { } /** - * Appends the text to the text area maintained in this scrollpane + * Appends the text to the text area maintained in this scroll pane * @param text the text to append. */ public void append(String text) { @@ -111,13 +111,15 @@ public class ScrollableTextArea extends JScrollPane { /** * Returns the number of lines current set in the text area + * @return the count */ public int getLineCount() { return textArea.getLineCount(); } /** - * Returns the tabsize set in the text area + * Returns the tab size set in the text area + * @return the size */ public int getTabSize() { return textArea.getTabSize(); @@ -125,6 +127,7 @@ public class ScrollableTextArea extends JScrollPane { /** * Returns the total area height of the text area (row height * line count) + * @return the height */ public int getTextAreaHeight() { return (textArea.getAreaHeight()); @@ -132,6 +135,7 @@ public class ScrollableTextArea extends JScrollPane { /** * Returns the visible height of the text area + * @return the height */ public int getTextVisibleHeight() { return textArea.getVisibleHeight(); @@ -200,6 +204,7 @@ public class ScrollableTextArea extends JScrollPane { /** * Returns the text contained within the text area + * @return the text */ public String getText() { return textArea.getText(); diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/table/column/DefaultTimestampRenderer.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/table/column/DefaultTimestampRenderer.java new file mode 100644 index 0000000000..1a8de7b73e --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/table/column/DefaultTimestampRenderer.java @@ -0,0 +1,55 @@ +/* ### + * 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.util.table.column; + +import java.awt.Component; +import java.util.Date; + +import javax.swing.JLabel; + +import docking.widgets.table.GTableCellRenderingData; +import ghidra.docking.settings.Settings; +import ghidra.util.DateUtils; + +/** + * A renderer for clients that wish to display a {@link Date} as a timestamp with the + * date and time. + */ +public class DefaultTimestampRenderer extends AbstractGColumnRenderer { + + @Override + public Component getTableCellRendererComponent(GTableCellRenderingData data) { + + JLabel label = (JLabel) super.getTableCellRendererComponent(data); + Date value = (Date) data.getValue(); + + if (value != null) { + label.setText(DateUtils.formatDateTimestamp(value)); + } + return label; + } + + @Override + public String getFilterString(Date t, Settings settings) { + return DateUtils.formatDateTimestamp(t); + } + + @Override + public ColumnConstraintFilterMode getColumnConstraintFilterMode() { + // This allows for text filtering in the table and date filtering on columns + return ColumnConstraintFilterMode.ALLOW_ALL_FILTERS; + } +} diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/DockingErrorDisplayTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/DockingErrorDisplayTest.java index 6ced1b0bf9..4cf1f91894 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/DockingErrorDisplayTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/DockingErrorDisplayTest.java @@ -35,7 +35,7 @@ public class DockingErrorDisplayTest extends AbstractDockingTest { DockingErrorDisplay display = new DockingErrorDisplay(); DefaultErrorLogger logger = new DefaultErrorLogger(); Exception exception = new Exception("My test exception"); - doDisplay(display, logger, exception); + reportException(display, logger, exception); assertErrLogDialog(); } @@ -46,11 +46,29 @@ public class DockingErrorDisplayTest extends AbstractDockingTest { DefaultErrorLogger logger = new DefaultErrorLogger(); Exception nestedException = new Exception("My nested test exception"); Exception exception = new Exception("My test exception", nestedException); - doDisplay(display, logger, exception); + reportException(display, logger, exception); assertErrLogDialog(); } + @Test + public void testDefaultErrorDisplay_MultipleAsynchronousExceptions() { + + DockingErrorDisplay display = new DockingErrorDisplay(); + DefaultErrorLogger logger = new DefaultErrorLogger(); + Exception exception = new Exception("My test exception"); + reportException(display, logger, exception); + + ErrLogDialog dialog = getErrLogDialog(); + + assertExceptionCount(dialog, 1); + + reportException(display, logger, new NullPointerException("It is null!")); + assertExceptionCount(dialog, 2); + + close(dialog); + } + @Test public void testMultipleCausesErrorDisplay() { DockingErrorDisplay display = new DockingErrorDisplay(); @@ -58,43 +76,51 @@ public class DockingErrorDisplayTest extends AbstractDockingTest { Throwable firstCause = new Exception("My test exception - first cause"); MultipleCauses exception = new MultipleCauses(Collections.singletonList(firstCause)); - doDisplay(display, logger, exception); + reportException(display, logger, exception); - assertErrLogExpandableDialog(); + ErrLogExpandableDialog dialog = assertErrLogExpandableDialog(); + assertExceptionCount(dialog, 1); + + reportException(display, logger, new NullPointerException("It is null!")); + assertExceptionCount(dialog, 2); + + close(dialog); } - private void assertErrLogExpandableDialog() { - Window w = waitForWindow(TEST_TITLE, 2000); - assertNotNull(w); + private void assertExceptionCount(AbstractErrDialog errDialog, int n) { - final ErrLogExpandableDialog errDialog = + int actual = errDialog.getExceptionCount(); + assertEquals(n, actual); + } + + private ErrLogExpandableDialog assertErrLogExpandableDialog() { + Window w = waitForWindow(TEST_TITLE); + + ErrLogExpandableDialog errDialog = getDialogComponentProvider(w, ErrLogExpandableDialog.class); assertNotNull(errDialog); - - runSwing(new Runnable() { - @Override - public void run() { - errDialog.close(); - } - }); + return errDialog; } private void assertErrLogDialog() { - Window w = waitForWindow(TEST_TITLE, 2000); + Window w = waitForWindow(TEST_TITLE); assertNotNull(w); - final ErrLogDialog errDialog = getDialogComponentProvider(w, ErrLogDialog.class); + ErrLogDialog errDialog = getDialogComponentProvider(w, ErrLogDialog.class); assertNotNull(errDialog); - - runSwing(new Runnable() { - @Override - public void run() { - errDialog.close(); - } - }); + close(errDialog); } - private void doDisplay(final DockingErrorDisplay display, final DefaultErrorLogger logger, + private ErrLogDialog getErrLogDialog() { + Window w = waitForWindow(TEST_TITLE); + assertNotNull(w); + + ErrLogDialog errDialog = getDialogComponentProvider(w, ErrLogDialog.class); + assertNotNull(errDialog); + return errDialog; + } + + private void reportException(final DockingErrorDisplay display, final DefaultErrorLogger logger, final Throwable throwable) { runSwing(new Runnable() { @Override diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/CausesImportant.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/CausesImportant.java deleted file mode 100644 index 1e274fa7c7..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/CausesImportant.java +++ /dev/null @@ -1,36 +0,0 @@ -/* ### - * 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.util.exception; - -import java.util.Arrays; - -import org.apache.commons.lang3.StringUtils; - -public interface CausesImportant { - public static class Util { - public static String getMessages(Throwable exc) { - if (exc instanceof CausesImportant) { - StringBuilder result = new StringBuilder(exc.getMessage()); - for (Throwable cause : MultipleCauses.Util.iterCauses(exc)) { - result.append( - StringUtils.join(Arrays.asList(cause.getMessage().split("\n")), "\n\t")); - } - return result.toString(); - } - return exc.getMessage(); - } - } -} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/HasConsoleText.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/HasConsoleText.java deleted file mode 100644 index f87856ad25..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/HasConsoleText.java +++ /dev/null @@ -1,30 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * 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.util.exception; - -public interface HasConsoleText { - public String getConsoleText(); - - public static class Util { - public static String get(Throwable exc) { - if (exc instanceof HasConsoleText) { - return ((HasConsoleText) exc).getConsoleText(); - } - return null; - } - } -} diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeManagerDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeManagerDB.java index c9ff2b264b..329ccb1f4f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeManagerDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/data/DataTypeManagerDB.java @@ -89,7 +89,7 @@ abstract public class DataTypeManagerDB implements DataTypeManager { protected DBHandle dbHandle; private AddressMap addrMap; - private ErrorHandler errHandler; + private ErrorHandler errHandler = new DbErrorHandler(); private DataTypeConflictHandler currentHandler; private CategoryDB root; @@ -168,12 +168,7 @@ abstract public class DataTypeManagerDB implements DataTypeManager { */ protected DataTypeManagerDB() { this.lock = new Lock("DataTypeManagerDB"); - errHandler = new ErrorHandler() { - @Override - public void dbError(IOException e) { - Msg.showError(this, null, "IO ERROR", e.getMessage(), e); - } - }; + try { dbHandle = new DBHandle(); int id = startTransaction(""); @@ -213,13 +208,6 @@ abstract public class DataTypeManagerDB implements DataTypeManager { ") for read-only Datatype Archive: " + packedDBfile.getAbsolutePath()); } - errHandler = new ErrorHandler() { - @Override - public void dbError(IOException e) { - Msg.showError(this, null, "IO ERROR", e.getMessage(), e); - } - }; - // Open packed database archive boolean openSuccess = false; PackedDatabase pdb = null; @@ -4089,6 +4077,20 @@ abstract public class DataTypeManagerDB implements DataTypeManager { } } } + + private class DbErrorHandler implements ErrorHandler { + + @Override + public void dbError(IOException e) { + + String message = e.getMessage(); + if (e instanceof ClosedException) { + message = "Data type archive is closed: " + getName(); + } + + Msg.showError(this, null, "IO ERROR", message, e); + } + } } /** diff --git a/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java b/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java index d1b5a56693..1f427274cc 100644 --- a/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java +++ b/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java @@ -499,14 +499,31 @@ public class ReflectionUtilities { * @return the string */ public static String stackTraceToString(Throwable t) { - StringBuffer sb = new StringBuffer(); + return stackTraceToString(t.getMessage(), t); + } + + /** + * Turns the given {@link Throwable} into a String version of its + * {@link Throwable#printStackTrace()} method. + * + * @param message the preferred message to use. If null, the throwable message will be used + * @param t the throwable + * @return the string + */ + public static String stackTraceToString(String message, Throwable t) { + StringBuilder sb = new StringBuilder(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); - String msg = t.getMessage(); - if (msg != null) { - ps.println(msg); + if (message != null) { + ps.println(message); + } + else { + String throwableMessage = t.getMessage(); + if (throwableMessage != null) { + ps.println(throwableMessage); + } } t.printStackTrace(ps); diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/IntroScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/IntroScreenShots.java index 7c0a5dd59d..c54e102490 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/IntroScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/IntroScreenShots.java @@ -55,8 +55,8 @@ public class IntroScreenShots extends GhidraScreenShotGenerator { @Test public void testErr_Dialog() { runSwing(() -> { - ErrLogDialog dialog = ErrLogDialog.createLogMessageDialog("Unexpected Error", - "Oops, this is really bad!", ""); + ErrLogDialog dialog = ErrLogDialog.createExceptionDialog("Unexpected Error", + "Oops, this is really bad!", new Throwable()); DockingWindowManager.showDialog(null, dialog); }, false); waitForSwing();