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