001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools.bugreport; 003 004import java.io.PrintWriter; 005import java.io.Serializable; 006import java.lang.reflect.InvocationTargetException; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.ConcurrentModificationException; 012import java.util.HashMap; 013import java.util.IdentityHashMap; 014import java.util.Iterator; 015import java.util.LinkedList; 016import java.util.Map; 017import java.util.Map.Entry; 018import java.util.NoSuchElementException; 019import java.util.Set; 020import java.util.function.Supplier; 021 022import org.openstreetmap.josm.tools.Logging; 023import org.openstreetmap.josm.tools.StreamUtils; 024 025/** 026 * This is a special exception that cannot be directly thrown. 027 * <p> 028 * It is used to capture more information about an exception that was already thrown. 029 * 030 * @author Michael Zangl 031 * @see BugReport 032 * @since 10285 033 */ 034@SuppressWarnings("OverrideThrowableToString") 035public class ReportedException extends RuntimeException { 036 /** 037 * How many entries of a collection to include in the bug report. 038 */ 039 private static final int MAX_COLLECTION_ENTRIES = 30; 040 041 private static final long serialVersionUID = 737333873766201033L; 042 043 /** 044 * We capture all stack traces on exception creation. This allows us to trace synchronization problems better. 045 * We cannot be really sure what happened but we at least see which threads 046 */ 047 private final transient Map<Thread, StackTraceElement[]> allStackTraces = new HashMap<>(); 048 private final LinkedList<Section> sections = new LinkedList<>(); 049 private final transient Thread caughtOnThread; 050 private String methodWarningFrom; 051 052 /** 053 * Constructs a new {@code ReportedException}. 054 * @param exception the cause (which is saved for later retrieval by the {@link #getCause()} method) 055 * @since 14380 056 */ 057 public ReportedException(Throwable exception) { 058 this(exception, Thread.currentThread()); 059 } 060 061 /** 062 * Constructs a new {@code ReportedException}. 063 * @param exception the cause (which is saved for later retrieval by the {@link #getCause()} method) 064 * @param caughtOnThread thread where the exception was caught 065 * @since 14380 066 */ 067 public ReportedException(Throwable exception, Thread caughtOnThread) { 068 super(exception); 069 070 try { 071 allStackTraces.putAll(Thread.getAllStackTraces()); 072 } catch (SecurityException e) { 073 Logging.log(Logging.LEVEL_ERROR, "Unable to get thread stack traces", e); 074 } 075 this.caughtOnThread = caughtOnThread; 076 } 077 078 /** 079 * Displays a warning for this exception. The program can then continue normally. Does not block. 080 */ 081 public void warn() { 082 methodWarningFrom = BugReport.getCallingMethod(2); 083 try { 084 BugReportQueue.getInstance().submit(this); 085 } catch (RuntimeException e) { // NOPMD 086 Logging.error(e); 087 } 088 } 089 090 /** 091 * Starts a new debug data section. This normally does not need to be called manually. 092 * 093 * @param sectionName 094 * The section name. 095 */ 096 public void startSection(String sectionName) { 097 sections.add(new Section(sectionName)); 098 } 099 100 /** 101 * Prints the captured data of this report to a {@link PrintWriter}. 102 * 103 * @param out 104 * The writer to print to. 105 */ 106 public void printReportDataTo(PrintWriter out) { 107 out.println("=== REPORTED CRASH DATA ==="); 108 for (Section s : sections) { 109 s.printSection(out); 110 out.println(); 111 } 112 113 if (methodWarningFrom != null) { 114 out.println("Warning issued by: " + methodWarningFrom); 115 out.println(); 116 } 117 } 118 119 /** 120 * Prints the stack trace of this report to a {@link PrintWriter}. 121 * 122 * @param out 123 * The writer to print to. 124 */ 125 public void printReportStackTo(PrintWriter out) { 126 out.println("=== STACK TRACE ==="); 127 out.println(niceThreadName(caughtOnThread)); 128 getCause().printStackTrace(out); 129 out.println(); 130 } 131 132 /** 133 * Prints the stack traces for other threads of this report to a {@link PrintWriter}. 134 * 135 * @param out 136 * The writer to print to. 137 */ 138 public void printReportThreadsTo(PrintWriter out) { 139 out.println("=== RUNNING THREADS ==="); 140 for (Entry<Thread, StackTraceElement[]> thread : allStackTraces.entrySet()) { 141 out.println(niceThreadName(thread.getKey())); 142 if (caughtOnThread.equals(thread.getKey())) { 143 out.println("Stacktrace see above."); 144 } else { 145 for (StackTraceElement e : thread.getValue()) { 146 out.println(e); 147 } 148 } 149 out.println(); 150 } 151 } 152 153 private static String niceThreadName(Thread thread) { 154 StringBuilder name = new StringBuilder("Thread: ").append(thread.getName()).append(" (").append(thread.getId()).append(')'); 155 ThreadGroup threadGroup = thread.getThreadGroup(); 156 if (threadGroup != null) { 157 name.append(" of ").append(threadGroup.getName()); 158 } 159 return name.toString(); 160 } 161 162 /** 163 * Checks if this exception is considered the same as an other exception. This is the case if both have the same cause and message. 164 * 165 * @param e 166 * The exception to check against. 167 * @return <code>true</code> if they are considered the same. 168 */ 169 public boolean isSame(ReportedException e) { 170 if (!getMessage().equals(e.getMessage())) { 171 return false; 172 } 173 174 return hasSameStackTrace(new CauseTraceIterator(), e.getCause()); 175 } 176 177 private static boolean hasSameStackTrace(CauseTraceIterator causeTraceIterator, Throwable e2) { 178 if (!causeTraceIterator.hasNext()) { 179 // all done. 180 return true; 181 } 182 Throwable e1 = causeTraceIterator.next(); 183 StackTraceElement[] t1 = e1.getStackTrace(); 184 StackTraceElement[] t2 = e2.getStackTrace(); 185 186 if (!Arrays.equals(t1, t2)) { 187 return false; 188 } 189 190 Throwable c1 = e1.getCause(); 191 Throwable c2 = e2.getCause(); 192 if ((c1 == null) != (c2 == null)) { 193 return false; 194 } else if (c1 != null) { 195 return hasSameStackTrace(causeTraceIterator, c2); 196 } else { 197 return true; 198 } 199 } 200 201 /** 202 * Adds some debug values to this exception. The value is converted to a string. Errors during conversion are handled. 203 * 204 * @param key 205 * The key to add this for. Does not need to be unique but it would be nice. 206 * @param value 207 * The value. 208 * @return This exception for easy chaining. 209 */ 210 public ReportedException put(String key, Object value) { 211 return put(key, () -> value); 212 } 213 214 /** 215 * Adds some debug values to this exception. This method automatically catches errors that occur during the production of the value. 216 * 217 * @param key 218 * The key to add this for. Does not need to be unique but it would be nice. 219 * @param valueSupplier 220 * A supplier that is called once to get the value. 221 * @return This exception for easy chaining. 222 * @since 10586 223 */ 224 public ReportedException put(String key, Supplier<Object> valueSupplier) { 225 String string; 226 try { 227 Object value = valueSupplier.get(); 228 if (value == null) { 229 string = "null"; 230 } else if (value instanceof Collection) { 231 string = makeCollectionNice((Collection<?>) value); 232 } else if (value.getClass().isArray()) { 233 string = makeCollectionNice(Arrays.asList(value)); 234 } else { 235 string = value.toString(); 236 } 237 } catch (RuntimeException t) { // NOPMD 238 Logging.warn(t); 239 string = "<Error calling toString()>"; 240 } 241 sections.getLast().put(key, string); 242 return this; 243 } 244 245 private static String makeCollectionNice(Collection<?> value) { 246 int lines = 0; 247 StringBuilder str = new StringBuilder(32); 248 for (Object e : value) { 249 str.append("\n - "); 250 if (lines <= MAX_COLLECTION_ENTRIES) { 251 ++lines; 252 str.append(e); 253 } else { 254 str.append("\n ... (") 255 .append(value.size()) 256 .append(" entries)"); 257 break; 258 } 259 } 260 return str.toString(); 261 } 262 263 @Override 264 public String toString() { 265 return "ReportedException [thread=" + caughtOnThread + ", exception=" + getCause() 266 + ", methodWarningFrom=" + methodWarningFrom + ']'; 267 } 268 269 /** 270 * Check if this exception may be caused by a threading issue. 271 * @return <code>true</code> if it is. 272 * @since 10585 273 */ 274 public boolean mayHaveConcurrentSource() { 275 return StreamUtils.toStream(CauseTraceIterator::new) 276 .anyMatch(t -> t instanceof ConcurrentModificationException || t instanceof InvocationTargetException); 277 } 278 279 /** 280 * Check if this is caused by an out of memory situation 281 * @return <code>true</code> if it is. 282 * @since 10819 283 */ 284 public boolean isOutOfMemory() { 285 return StreamUtils.toStream(CauseTraceIterator::new).anyMatch(t -> t instanceof OutOfMemoryError); 286 } 287 288 /** 289 * Iterates over the causes for this exception. Ignores cycles and aborts iteration then. 290 * @author Michal Zangl 291 * @since 10585 292 */ 293 private final class CauseTraceIterator implements Iterator<Throwable> { 294 private Throwable current = getCause(); 295 private final Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>()); 296 297 @Override 298 public boolean hasNext() { 299 return current != null; 300 } 301 302 @Override 303 public Throwable next() { 304 if (!hasNext()) { 305 throw new NoSuchElementException(); 306 } 307 Throwable toReturn = current; 308 advance(); 309 return toReturn; 310 } 311 312 private void advance() { 313 dejaVu.add(current); 314 current = current.getCause(); 315 if (current != null && dejaVu.contains(current)) { 316 current = null; 317 } 318 } 319 } 320 321 private static class SectionEntry implements Serializable { 322 323 private static final long serialVersionUID = 1L; 324 325 private final String key; 326 private final String value; 327 328 SectionEntry(String key, String value) { 329 this.key = key; 330 this.value = value; 331 } 332 333 /** 334 * Prints this entry to the output stream in a line. 335 * @param out The stream to print to. 336 */ 337 public void print(PrintWriter out) { 338 out.print(" - "); 339 out.print(key); 340 out.print(": "); 341 out.println(value); 342 } 343 } 344 345 private static class Section implements Serializable { 346 347 private static final long serialVersionUID = 1L; 348 349 private final String sectionName; 350 private final ArrayList<SectionEntry> entries = new ArrayList<>(); 351 352 Section(String sectionName) { 353 this.sectionName = sectionName; 354 } 355 356 /** 357 * Add a key/value entry to this section. 358 * @param key The key. Need not be unique. 359 * @param value The value. 360 */ 361 public void put(String key, String value) { 362 entries.add(new SectionEntry(key, value)); 363 } 364 365 /** 366 * Prints this section to the output stream. 367 * @param out The stream to print to. 368 */ 369 public void printSection(PrintWriter out) { 370 out.println(sectionName + ':'); 371 if (entries.isEmpty()) { 372 out.println("No data collected."); 373 } else { 374 for (SectionEntry e : entries) { 375 e.print(out); 376 } 377 } 378 } 379 } 380}