package net.sf.openrocket.gui.main; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import net.sf.openrocket.gui.dialogs.BugReportDialog; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.ExceptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SwingExceptionHandler implements Thread.UncaughtExceptionHandler, ExceptionHandler { private static final Logger log = LoggerFactory.getLogger(SwingExceptionHandler.class); private static final int MEMORY_RESERVE = 512 * 1024; /** * A memory reserve of 0.5 MB of memory, that can be freed when showing the dialog. * <p> * This field is package-private so that the JRE cannot optimize its use away. */ volatile byte[] memoryReserve = null; private volatile boolean handling = false; @Override public void uncaughtException(final Thread thread, final Throwable throwable) { // Free memory reserve if out of memory if (isOutOfMemoryError(throwable)) { memoryReserve = null; handling = false; log.error("Out of memory error detected", throwable); } if (isNonFatalJREBug(throwable)) { log.warn("Ignoring non-fatal JRE bug", throwable); return; } log.error("Handling uncaught exception on thread=" + thread, throwable); throwable.printStackTrace(); if (handling) { log.warn("Exception is currently being handled, ignoring"); return; } try { handling = true; // Show on the EDT if (SwingUtilities.isEventDispatchThread()) { log.info("Exception handler running on EDT, showing dialog"); showDialog(thread, throwable); } else { log.info("Exception handler not on EDT, invoking dialog on EDT"); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { showDialog(thread, throwable); } }); } } catch (Throwable ex) { // Make sure the handler does not throw any exceptions try { log.error("Caught exception while handling exception", ex); System.err.println("Exception in exception handler, dumping exception:"); ex.printStackTrace(); } catch (Exception ignore) { } } finally { // Mark handling as completed handling = false; } } /** * Handle an error condition programmatically without throwing an exception. * This can be used in cases where recovery of the error is desirable. * <p> * This method is guaranteed never to throw an exception, and can thus be safely * used in finally blocks. * * @param message the error message. */ @Override public void handleErrorCondition(String message) { log.error(message, new Throwable()); handleErrorCondition(new InternalException(message)); } /** * Handle an error condition programmatically without throwing an exception. * This can be used in cases where recovery of the error is desirable. * <p> * This method is guaranteed never to throw an exception, and can thus be safely * used in finally blocks. * * @param message the error message. * @param exception the exception that occurred. */ @Override public void handleErrorCondition(String message, Throwable exception) { log.error(message, exception); handleErrorCondition(new InternalException(message, exception)); } /** * Handle an error condition programmatically without throwing an exception. * This can be used in cases where recovery of the error is desirable. * <p> * This method is guaranteed never to throw an exception, and can thus be safely * used in finally blocks. * * @param exception the exception that occurred. */ @Override public void handleErrorCondition(final Throwable exception) { try { if (!(exception instanceof InternalException)) { log.error("Error occurred", exception); } final Thread thread = Thread.currentThread(); if (SwingUtilities.isEventDispatchThread()) { log.info("Running in EDT, showing dialog"); this.showDialog(thread, exception); } else { log.info("Not in EDT, invoking dialog later"); final SwingExceptionHandler instance = this; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { instance.showDialog(thread, exception); } }); } } catch (Exception e) { log.error("Exception occurred in error handler", e); } } /** * The actual handling routine. * * @param t the thread that caused the exception, or <code>null</code>. * @param e the exception. */ private void showDialog(Thread t, Throwable e) { // Out of memory if (isOutOfMemoryError(e)) { log.info("Showing out-of-memory dialog"); JOptionPane.showMessageDialog(null, new Object[] { "OpenRocket is out of available memory!", "You should immediately close unnecessary design windows,", "save any unsaved designs and restart OpenRocket!" }, "Out of memory", JOptionPane.ERROR_MESSAGE); return; } // Create the message String msg = e.getClass().getSimpleName() + ": " + e.getMessage(); if (msg.length() > 90) { msg = msg.substring(0, 80) + "..."; } // Unknown Error if (!(e instanceof Exception) && !(e instanceof LinkageError)) { log.info("Showing Error dialog"); JOptionPane.showMessageDialog(null, new Object[] { "An unknown Java error occurred:", msg, "<html>You should immediately close unnecessary design windows,<br>" + "save any unsaved designs and restart OpenRocket!" }, "Unknown Java error", JOptionPane.ERROR_MESSAGE); return; } // Normal exception, show question dialog log.info("Showing Exception dialog"); int selection = JOptionPane.showOptionDialog(null, new Object[] { "OpenRocket encountered an uncaught exception. This typically signifies " + "a bug in the software.", "<html><em>        " + msg + "</em>", " ", "Please take a moment to report this bug to the developers.", "This can be done automatically if you have an Internet connection." }, "Uncaught exception", JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE, null, new Object[] { "View bug report", "Close" }, "View bug report"); if (selection != 0) { // User cancelled log.info(Markers.USER_MARKER, "User chose not to fill bug report"); return; } // Show bug report dialog log.info(Markers.USER_MARKER, "User requested sending bug report"); BugReportDialog.showExceptionDialog(null, t, e); } /** * Registers the uncaught exception handler. This should be used to ensure that * all necessary registrations are performed. */ public void registerExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler(this); // Handler for modal dialogs of Sun's Java implementation // See bug ID 4499199. System.setProperty("sun.awt.exception.handler", AwtHandler.class.getName()); reserveMemory(); } /** * Reserve the buffer memory that is freed in case an OutOfMemoryError occurs. */ private void reserveMemory() { memoryReserve = new byte[MEMORY_RESERVE]; for (int i = 0; i < MEMORY_RESERVE; i++) { memoryReserve[i] = (byte) i; } } /** * Return whether this throwable was caused by an OutOfMemoryError * condition. An exception is deemed to be caused by OutOfMemoryError * if the throwable or any of its causes is of the type OutOfMemoryError. * <p> * This method is required because Apple's JRE implementation sometimes * masks OutOfMemoryErrors within RuntimeExceptions. Idiots. * * @param t the throwable to examine. * @return whether this is an out-of-memory condition. */ private boolean isOutOfMemoryError(Throwable t) { while (t != null) { if (t instanceof OutOfMemoryError) return true; t = t.getCause(); } return false; } /** * Handler used in modal dialogs by Sun Java implementation. */ public static class AwtHandler { public void handle(Throwable t) { Application.getExceptionHandler().uncaughtException(Thread.currentThread(), t); } } /** * Detect various non-fatal Sun JRE bugs. * * @param t the throwable * @return whether this exception should be ignored */ private boolean isNonFatalJREBug(Throwable t) { // NOTE: Calling method logs the entire throwable, so log only message here /* * Detect and ignore bug 6826104 in Sun JRE. */ if (t instanceof NullPointerException) { StackTraceElement[] trace = t.getStackTrace(); if (trace.length > 3 && trace[0].getClassName().equals("sun.awt.X11.XWindowPeer") && trace[0].getMethodName().equals("restoreTransientFor") && trace[1].getClassName().equals("sun.awt.X11.XWindowPeer") && trace[1].getMethodName().equals("removeFromTransientFors") && trace[2].getClassName().equals("sun.awt.X11.XWindowPeer") && trace[2].getMethodName().equals("setModalBlocked")) { log.warn("Ignoring Sun JRE bug (6826104): http://bugs.sun.com/view_bug.do?bug_id=6826104" + t); return true; } } /* * Detect and ignore bug 6828938 in Sun JRE 1.6.0_14 - 1.6.0_16. */ if (t instanceof ArrayIndexOutOfBoundsException) { final String buggyClass = "sun.font.FontDesignMetrics"; StackTraceElement[] elements = t.getStackTrace(); if (elements.length >= 3 && (buggyClass.equals(elements[0].getClassName()) || buggyClass.equals(elements[1].getClassName()) || buggyClass.equals(elements[2].getClassName()))) { log.warn("Ignoring Sun JRE bug 6828938: " + "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6828938): " + t); return true; } } /* * Detect and ignore bug 6561072 in Sun JRE 1.6.0_? */ if (t instanceof NullPointerException) { StackTraceElement[] trace = t.getStackTrace(); if (trace.length > 3 && trace[0].getClassName().equals("javax.swing.JComponent") && trace[0].getMethodName().equals("repaint") && trace[1].getClassName().equals("sun.swing.FilePane$2") && trace[1].getMethodName().equals("repaintListSelection") && trace[2].getClassName().equals("sun.swing.FilePane$2") && trace[2].getMethodName().equals("repaintSelection")) { log.warn("Ignoring Sun JRE bug 6561072 " + "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6561072): " + t); return true; } } /* * Detect and ignore bug 6933331 in Sun JRE 1.6.0_18 and others */ if (t instanceof IllegalStateException) { StackTraceElement[] trace = t.getStackTrace(); if (trace.length > 1 && trace[0].getClassName().equals("sun.awt.windows.WComponentPeer") && trace[0].getMethodName().equals("getBackBuffer")) { log.warn("Ignoring Sun JRE bug 6933331 " + "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6933331): " + t); return true; } } /* * Detect and ignore bug in Sun JRE 1.6.0_19 */ if (t instanceof NullPointerException) { StackTraceElement[] trace = t.getStackTrace(); if (trace.length > 3 && trace[0].getClassName().equals("sun.awt.shell.Win32ShellFolder2") && trace[0].getMethodName().equals("pidlsEqual") && trace[1].getClassName().equals("sun.awt.shell.Win32ShellFolder2") && trace[1].getMethodName().equals("equals") && trace[2].getClassName().equals("sun.awt.shell.Win32ShellFolderManager2") && trace[2].getMethodName().equals("isFileSystemRoot")) { log.warn("Ignoring Sun JRE bug " + "(see http://forums.sun.com/thread.jspa?threadID=5435324): " + t); return true; } } /* * Detect Sun JRE bug in D3D */ if (t instanceof ClassCastException) { if (t.getMessage().equals("sun.awt.Win32GraphicsConfig cannot be cast to sun.java2d.d3d.D3DGraphicsConfig")) { log.warn("Ignoring Sun JRE bug " + "(see http://forums.sun.com/thread.jspa?threadID=5440525): " + t); return true; } } /* * Detect and ignore DnD bug in component tree - related to 6560955 in Sun JRE. */ if (t instanceof NullPointerException) { StackTraceElement[] trace = t.getStackTrace(); if (trace.length > 2 && trace[0].getClassName().equals("javax.swing.tree.TreePath") && trace[0].getMethodName().equals("pathByAddingChild") && trace[1].getClassName().equals("javax.swing.plaf.basic.BasicTreeUI") && trace[1].getMethodName().equals("getDropLineRect")) { log.warn("Ignoring Sun JRE bug updating drop location " + "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955): " + t); return true; } } return false; } @SuppressWarnings("unused") private static class InternalException extends Exception { public InternalException() { super(); } public InternalException(String message, Throwable cause) { super(message, cause); } public InternalException(String message) { super(message); } public InternalException(Throwable cause) { super(cause); } } }