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