source: josm/trunk/src/org/openstreetmap/josm/tools/bugreport/BugReport.java

Last change on this file was 18550, checked in by taylor.smock, 20 months ago

Fix CID 1497740: Concurrent data access violations

SUPPRESSED_EXCEPTIONS.add was originally not guarded, since
the possible exception could be caused by pop, and I wanted
to prevent that by keeping multiple threads from calling
pop at the same time.

  • Property svn:eol-style set to native
File size: 10.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools.bugreport;
3
4import java.io.PrintWriter;
5import java.io.Serializable;
6import java.io.StringWriter;
7import java.time.Instant;
8import java.util.ArrayDeque;
9import java.util.Deque;
10import java.util.concurrent.CopyOnWriteArrayList;
11import java.util.function.Predicate;
12
13import org.openstreetmap.josm.tools.Pair;
14
15/**
16 * This class contains utility methods to create and handle a bug report.
17 * <p>
18 * It allows you to configure the format and request to send the bug report.
19 * <p>
20 * It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an
21 * exception.
22 * <h1> Handling Exceptions </h1>
23 * In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there.
24 * <p>
25 * You should then add some debug information there. This can be the OSM ids that caused the error, information on the data you were working on
26 * or other local variables. Make sure that no exceptions may occur while computing the values. It is best to send plain local variables to
27 * put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report.
28 * The global exception handler will do the rest.
29 * <pre>
30 * int id = ...;
31 * String tag = "...";
32 * try {
33 * ... your code ...
34 * } catch (RuntimeException t) {
35 * throw BugReport.intercept(t).put("id", id).put("tag", () -&gt; x.getTag());
36 * }
37 * </pre>
38 *
39 * Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report
40 * the exception or ignore it.
41 *
42 * @author Michael Zangl
43 * @since 10285
44 */
45public final class BugReport implements Serializable {
46 private static final long serialVersionUID = 1L;
47 /** The maximum suppressed exceptions to keep to report */
48 private static final byte MAXIMUM_SUPPRESSED_EXCEPTIONS = 4;
49 /** The list of suppressed exceptions, Pair&lt;time reported, exception&gt; */
50 private static final Deque<Pair<Instant, Throwable>> SUPPRESSED_EXCEPTIONS = new ArrayDeque<>(MAXIMUM_SUPPRESSED_EXCEPTIONS);
51
52 private boolean includeStatusReport = true;
53 private boolean includeData = true;
54 private boolean includeAllStackTraces;
55 private final ReportedException exception;
56 private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>();
57
58 /**
59 * Create a new bug report
60 * @param e The {@link ReportedException} to use. No more data should be added after creating the report.
61 */
62 public BugReport(ReportedException e) {
63 this.exception = e;
64 includeAllStackTraces = e.mayHaveConcurrentSource();
65 }
66
67 /**
68 * Add a suppressed exception. Mostly useful for when a chain of exceptions causes an actual bug report.
69 * This should only be used when an exception is raised in {@link org.openstreetmap.josm.gui.util.GuiHelper}
70 * or {@link org.openstreetmap.josm.gui.progress.swing.ProgressMonitorExecutor} at this time.
71 * {@link org.openstreetmap.josm.tools.Logging} may call this in the future, when logging a throwable.
72 * @param t The throwable raised. If {@code null}, we add a new {@code NullPointerException} instead.
73 * @since 18549
74 */
75 public static void addSuppressedException(Throwable t) {
76 // Ensure we don't call pop in more than MAXIMUM_SUPPRESSED_EXCEPTIONS threads. This guard is
77 // here just in case someone doesn't read the javadocs.
78 synchronized (SUPPRESSED_EXCEPTIONS) {
79 SUPPRESSED_EXCEPTIONS.add(new Pair<>(Instant.now(), t != null ? t : new NullPointerException()));
80 // Ensure we aren't keeping exceptions forever
81 while (SUPPRESSED_EXCEPTIONS.size() > MAXIMUM_SUPPRESSED_EXCEPTIONS) {
82 SUPPRESSED_EXCEPTIONS.pop();
83 }
84 }
85 }
86
87 /**
88 * Determines if this report should include a system status report
89 * @return <code>true</code> to include it.
90 * @since 10597
91 */
92 public boolean isIncludeStatusReport() {
93 return includeStatusReport;
94 }
95
96 /**
97 * Set if this report should include a system status report
98 * @param includeStatusReport if the status report should be included
99 * @since 10585
100 */
101 public void setIncludeStatusReport(boolean includeStatusReport) {
102 this.includeStatusReport = includeStatusReport;
103 fireChange();
104 }
105
106 /**
107 * Determines if this report should include the data that was traced.
108 * @return <code>true</code> to include it.
109 * @since 10597
110 */
111 public boolean isIncludeData() {
112 return includeData;
113 }
114
115 /**
116 * Set if this report should include the data that was traced.
117 * @param includeData if data should be included
118 * @since 10585
119 */
120 public void setIncludeData(boolean includeData) {
121 this.includeData = includeData;
122 fireChange();
123 }
124
125 /**
126 * Determines if this report should include the stack traces for all other threads.
127 * @return <code>true</code> to include it.
128 * @since 10597
129 */
130 public boolean isIncludeAllStackTraces() {
131 return includeAllStackTraces;
132 }
133
134 /**
135 * Sets if this report should include the stack traces for all other threads.
136 * @param includeAllStackTraces if all stack traces should be included
137 * @since 10585
138 */
139 public void setIncludeAllStackTraces(boolean includeAllStackTraces) {
140 this.includeAllStackTraces = includeAllStackTraces;
141 fireChange();
142 }
143
144 /**
145 * Gets the full string that should be send as error report.
146 * @param header header text for the error report
147 * @return The string.
148 * @since 10585
149 */
150 public String getReportText(String header) {
151 StringWriter stringWriter = new StringWriter();
152 PrintWriter out = new PrintWriter(stringWriter);
153 if (isIncludeStatusReport()) {
154 try {
155 out.println(header);
156 } catch (RuntimeException e) { // NOPMD
157 out.println("Could not generate status report: " + e.getMessage());
158 }
159 }
160 if (isIncludeData()) {
161 exception.printReportDataTo(out);
162 }
163 exception.printReportStackTo(out);
164 if (isIncludeAllStackTraces()) {
165 exception.printReportThreadsTo(out);
166 }
167 synchronized (SUPPRESSED_EXCEPTIONS) {
168 if (!SUPPRESSED_EXCEPTIONS.isEmpty()) {
169 out.println("=== ADDITIONAL EXCEPTIONS ===");
170 // Avoid multiple bug reports from reading from the deque at the same time.
171 while (SUPPRESSED_EXCEPTIONS.peek() != null) {
172 Pair<Instant, Throwable> currentException = SUPPRESSED_EXCEPTIONS.pop();
173 out.println("==== Exception at " + currentException.a.toEpochMilli() + " ====");
174 currentException.b.printStackTrace(out);
175 }
176 }
177 }
178 return stringWriter.toString().replaceAll("\r", "");
179 }
180
181 /**
182 * Add a new change listener.
183 * @param listener The listener
184 * @since 10585
185 */
186 public void addChangeListener(BugReportListener listener) {
187 listeners.add(listener);
188 }
189
190 /**
191 * Remove a change listener.
192 * @param listener The listener
193 * @since 10585
194 */
195 public void removeChangeListener(BugReportListener listener) {
196 listeners.remove(listener);
197 }
198
199 private void fireChange() {
200 listeners.forEach(l -> l.bugReportChanged(this));
201 }
202
203 /**
204 * This should be called whenever you want to add more information to a given exception.
205 * @param t The throwable that was thrown.
206 * @return A {@link ReportedException} to which you can add additional information.
207 */
208 public static ReportedException intercept(Throwable t) {
209 ReportedException e;
210 if (t instanceof ReportedException) {
211 e = (ReportedException) t;
212 } else {
213 e = new ReportedException(t);
214 }
215 e.startSection(getCallingMethod(2));
216 return e;
217 }
218
219 /**
220 * Find the method that called us.
221 *
222 * @param offset
223 * How many methods to look back in the stack trace. 1 gives the method calling this method, 0 gives you getCallingMethod().
224 * @return The method name.
225 */
226 public static String getCallingMethod(int offset) {
227 StackTraceElement found = getCallingMethod(offset + 1, BugReport.class.getName(), "getCallingMethod"::equals);
228 if (found != null) {
229 return found.getClassName().replaceFirst(".*\\.", "") + '#' + found.getMethodName();
230 } else {
231 return "?";
232 }
233 }
234
235 /**
236 * Find the method that called the given method on the current stack trace.
237 * @param offset
238 * How many methods to look back in the stack trace.
239 * 1 gives the method calling this method, 0 gives you the method with the given name..
240 * @param className The name of the class to search for
241 * @param methodName The name of the method to search for
242 * @return The class and method name or null if it is unknown.
243 */
244 public static StackTraceElement getCallingMethod(int offset, String className, Predicate<String> methodName) {
245 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
246 for (int i = 0; i < stackTrace.length - offset; i++) {
247 StackTraceElement element = stackTrace[i];
248 if (className.equals(element.getClassName()) && methodName.test(element.getMethodName())) {
249 return stackTrace[i + offset];
250 }
251 }
252 return null;
253 }
254
255 /**
256 * A listener that listens to changes to this report.
257 * @author Michael Zangl
258 * @since 10585
259 */
260 @FunctionalInterface
261 public interface BugReportListener {
262 /**
263 * Called whenever this bug report was changed, e.g. the data to be included in it.
264 * @param report The report that was changed.
265 */
266 void bugReportChanged(BugReport report);
267 }
268}
Note: See TracBrowser for help on using the repository browser.