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