001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.awt.GridBagLayout; 008import java.io.ByteArrayOutputStream; 009import java.io.IOException; 010import java.io.PrintWriter; 011import java.io.StringWriter; 012import java.net.URL; 013import java.nio.ByteBuffer; 014import java.nio.charset.StandardCharsets; 015import java.util.zip.GZIPOutputStream; 016 017import javax.swing.JCheckBox; 018import javax.swing.JLabel; 019import javax.swing.JOptionPane; 020import javax.swing.JPanel; 021import javax.swing.JScrollPane; 022import javax.swing.SwingUtilities; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.actions.ShowStatusReportAction; 026import org.openstreetmap.josm.gui.ExtendedDialog; 027import org.openstreetmap.josm.gui.preferences.plugin.PluginPreference; 028import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 029import org.openstreetmap.josm.gui.widgets.JosmTextArea; 030import org.openstreetmap.josm.gui.widgets.UrlLabel; 031import org.openstreetmap.josm.plugins.PluginDownloadTask; 032import org.openstreetmap.josm.plugins.PluginHandler; 033 034/** 035 * An exception handler that asks the user to send a bug report. 036 * 037 * @author imi 038 */ 039public final class BugReportExceptionHandler implements Thread.UncaughtExceptionHandler { 040 041 private static boolean handlingInProgress = false; 042 private static BugReporterThread bugReporterThread = null; 043 private static int exceptionCounter = 0; 044 private static boolean suppressExceptionDialogs = false; 045 046 private static class BugReporterThread extends Thread { 047 048 final Throwable e; 049 050 public BugReporterThread(Throwable t) { 051 super("Bug Reporter"); 052 this.e = t; 053 } 054 055 @Override 056 public void run() { 057 // Give the user a chance to deactivate the plugin which threw the exception (if it was thrown from a plugin) 058 final PluginDownloadTask pluginDownloadTask = PluginHandler.updateOrdisablePluginAfterException(e); 059 060 SwingUtilities.invokeLater(new Runnable() { 061 @Override 062 public void run() { 063 // Then ask for submitting a bug report, for exceptions thrown from a plugin too, unless updated to a new version 064 if (pluginDownloadTask == null) { 065 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Unexpected Exception"), new String[] {tr("Do nothing"), tr("Report Bug")}); 066 ed.setIcon(JOptionPane.ERROR_MESSAGE); 067 JPanel pnl = new JPanel(new GridBagLayout()); 068 pnl.add(new JLabel( 069 "<html>" + tr("An unexpected exception occurred.<br>" + 070 "This is always a coding error. If you are running the latest<br>" + 071 "version of JOSM, please consider being kind and file a bug report." 072 ) 073 + "</html>"), GBC.eol()); 074 JCheckBox cbSuppress = null; 075 if (exceptionCounter > 1) { 076 cbSuppress = new JCheckBox(tr("Suppress further error dialogs for this session.")); 077 pnl.add(cbSuppress, GBC.eol()); 078 } 079 ed.setContent(pnl); 080 ed.showDialog(); 081 if (cbSuppress != null && cbSuppress.isSelected()) { 082 suppressExceptionDialogs = true; 083 } 084 if (ed.getValue() != 2) return; 085 askForBugReport(e); 086 } else { 087 // Ask for restart to install new plugin 088 PluginPreference.notifyDownloadResults(Main.parent, pluginDownloadTask); 089 } 090 } 091 }); 092 } 093 } 094 095 @Override 096 public void uncaughtException(Thread t, Throwable e) { 097 handleException(e); 098 } 099 100 //http://stuffthathappens.com/blog/2007/10/15/one-more-note-on-uncaught-exception-handlers/ 101 /** 102 * Handles the given throwable object 103 * @param t The throwable object 104 */ 105 public void handle(Throwable t) { 106 handleException(t); 107 } 108 109 /** 110 * Handles the given exception 111 * @param e the exception 112 */ 113 public static void handleException(final Throwable e) { 114 if (handlingInProgress || suppressExceptionDialogs) 115 return; // we do not handle secondary exceptions, this gets too messy 116 if (bugReporterThread != null && bugReporterThread.isAlive()) 117 return; 118 handlingInProgress = true; 119 exceptionCounter++; 120 try { 121 Main.error(e); 122 if (Main.parent != null) { 123 if (e instanceof OutOfMemoryError) { 124 // do not translate the string, as translation may raise an exception 125 JOptionPane.showMessageDialog(Main.parent, "JOSM is out of memory. " + 126 "Strange things may happen.\nPlease restart JOSM with the -Xmx###M option,\n" + 127 "where ### is the number of MB assigned to JOSM (e.g. 256).\n" + 128 "Currently, " + Runtime.getRuntime().maxMemory()/1024/1024 + " MB are available to JOSM.", 129 "Error", 130 JOptionPane.ERROR_MESSAGE 131 ); 132 return; 133 } 134 135 bugReporterThread = new BugReporterThread(e); 136 bugReporterThread.start(); 137 } 138 } finally { 139 handlingInProgress = false; 140 } 141 } 142 143 private static void askForBugReport(final Throwable e) { 144 try { 145 final int maxlen = 6000; 146 StringWriter stack = new StringWriter(); 147 e.printStackTrace(new PrintWriter(stack)); 148 149 String text = ShowStatusReportAction.getReportHeader() + stack.getBuffer().toString(); 150 String urltext = text.replaceAll("\r",""); 151 if (urltext.length() > maxlen) { 152 urltext = urltext.substring(0,maxlen); 153 int idx = urltext.lastIndexOf('\n'); 154 // cut whole line when not loosing too much 155 if (maxlen-idx < 200) { 156 urltext = urltext.substring(0,idx+1); 157 } 158 urltext += "...<snip>...\n"; 159 } 160 161 JPanel p = new JPanel(new GridBagLayout()); 162 p.add(new JMultilineLabel( 163 tr("You have encountered an error in JOSM. Before you file a bug report " + 164 "make sure you have updated to the latest version of JOSM here:")), GBC.eol()); 165 p.add(new UrlLabel(Main.getJOSMWebsite(),2), GBC.eop().insets(8,0,0,0)); 166 p.add(new JMultilineLabel( 167 tr("You should also update your plugins. If neither of those help please " + 168 "file a bug report in our bugtracker using this link:")), GBC.eol()); 169 p.add(getBugReportUrlLabel(urltext), GBC.eop().insets(8,0,0,0)); 170 p.add(new JMultilineLabel( 171 tr("There the error information provided below should already be " + 172 "filled in for you. Please include information on how to reproduce " + 173 "the error and try to supply as much detail as possible.")), GBC.eop()); 174 p.add(new JMultilineLabel( 175 tr("Alternatively, if that does not work you can manually fill in the information " + 176 "below at this URL:")), GBC.eol()); 177 p.add(new UrlLabel(Main.getJOSMWebsite()+"/newticket",2), GBC.eop().insets(8,0,0,0)); 178 if (Utils.copyToClipboard(text)) { 179 p.add(new JLabel(tr("(The text has already been copied to your clipboard.)")), GBC.eop()); 180 } 181 182 JosmTextArea info = new JosmTextArea(text, 18, 60); 183 info.setCaretPosition(0); 184 info.setEditable(false); 185 p.add(new JScrollPane(info), GBC.eop().fill()); 186 187 for (Component c: p.getComponents()) { 188 if (c instanceof JMultilineLabel) { 189 ((JMultilineLabel)c).setMaxWidth(400); 190 } 191 } 192 193 JOptionPane.showMessageDialog(Main.parent, p, tr("You have encountered a bug in JOSM"), JOptionPane.ERROR_MESSAGE); 194 } catch (Exception e1) { 195 Main.error(e1); 196 } 197 } 198 199 /** 200 * Determines if an exception is currently being handled 201 * @return {@code true} if an exception is currently being handled, {@code false} otherwise 202 */ 203 public static boolean exceptionHandlingInProgress() { 204 return handlingInProgress; 205 } 206 207 /** 208 * Replies the URL to create a JOSM bug report with the given debug text 209 * @param debugText The debug text to provide us 210 * @return The URL to create a JOSM bug report with the given debug text 211 * @since 5849 212 */ 213 public static URL getBugReportUrl(String debugText) { 214 try ( 215 ByteArrayOutputStream out = new ByteArrayOutputStream(); 216 GZIPOutputStream gzip = new GZIPOutputStream(out) 217 ) { 218 gzip.write(debugText.getBytes(StandardCharsets.UTF_8)); 219 gzip.finish(); 220 221 return new URL(Main.getJOSMWebsite()+"/josmticket?" + 222 "gdata="+Base64.encode(ByteBuffer.wrap(out.toByteArray()), true)); 223 } catch (IOException e) { 224 Main.error(e); 225 return null; 226 } 227 } 228 229 /** 230 * Replies the URL label to create a JOSM bug report with the given debug text 231 * @param debugText The debug text to provide us 232 * @return The URL label to create a JOSM bug report with the given debug text 233 * @since 5849 234 */ 235 public static final UrlLabel getBugReportUrlLabel(String debugText) { 236 URL url = getBugReportUrl(debugText); 237 if (url != null) { 238 return new UrlLabel(url.toString(), Main.getJOSMWebsite()+"/josmticket?...", 2); 239 } 240 return null; 241 } 242}