10
10
import io .sentry .hints .TransactionEnd ;
11
11
import io .sentry .protocol .Mechanism ;
12
12
import io .sentry .protocol .SentryId ;
13
+ import io .sentry .util .AutoClosableReentrantLock ;
13
14
import io .sentry .util .HintUtils ;
14
15
import io .sentry .util .Objects ;
15
16
import java .io .Closeable ;
17
+ import java .util .HashSet ;
18
+ import java .util .Set ;
16
19
import java .util .concurrent .atomic .AtomicReference ;
17
20
import org .jetbrains .annotations .ApiStatus ;
18
21
import org .jetbrains .annotations .NotNull ;
@@ -28,6 +31,8 @@ public final class UncaughtExceptionHandlerIntegration
28
31
/** Reference to the pre-existing uncaught exception handler. */
29
32
private @ Nullable Thread .UncaughtExceptionHandler defaultExceptionHandler ;
30
33
34
+ private static final @ NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock ();
35
+
31
36
private @ Nullable IScopes scopes ;
32
37
private @ Nullable SentryOptions options ;
33
38
@@ -65,27 +70,33 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO
65
70
this .options .isEnableUncaughtExceptionHandler ());
66
71
67
72
if (this .options .isEnableUncaughtExceptionHandler ()) {
68
- final Thread .UncaughtExceptionHandler currentHandler =
69
- threadAdapter .getDefaultUncaughtExceptionHandler ();
70
- if (currentHandler != null ) {
71
- this .options
72
- .getLogger ()
73
- .log (
74
- SentryLevel .DEBUG ,
75
- "default UncaughtExceptionHandler class='"
76
- + currentHandler .getClass ().getName ()
77
- + "'" );
78
-
79
- if (currentHandler instanceof UncaughtExceptionHandlerIntegration ) {
80
- final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
81
- (UncaughtExceptionHandlerIntegration ) currentHandler ;
82
- defaultExceptionHandler = currentHandlerIntegration .defaultExceptionHandler ;
83
- } else {
84
- defaultExceptionHandler = currentHandler ;
73
+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
74
+ final Thread .UncaughtExceptionHandler currentHandler =
75
+ threadAdapter .getDefaultUncaughtExceptionHandler ();
76
+ if (currentHandler != null ) {
77
+ this .options
78
+ .getLogger ()
79
+ .log (
80
+ SentryLevel .DEBUG ,
81
+ "default UncaughtExceptionHandler class='"
82
+ + currentHandler .getClass ().getName ()
83
+ + "'" );
84
+ if (currentHandler instanceof UncaughtExceptionHandlerIntegration ) {
85
+ final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
86
+ (UncaughtExceptionHandlerIntegration ) currentHandler ;
87
+ if (currentHandlerIntegration .scopes != null
88
+ && scopes .getGlobalScope () == currentHandlerIntegration .scopes .getGlobalScope ()) {
89
+ defaultExceptionHandler = currentHandlerIntegration .defaultExceptionHandler ;
90
+ } else {
91
+ defaultExceptionHandler = currentHandler ;
92
+ }
93
+ } else {
94
+ defaultExceptionHandler = currentHandler ;
95
+ }
85
96
}
86
- }
87
97
88
- threadAdapter .setDefaultUncaughtExceptionHandler (this );
98
+ threadAdapter .setDefaultUncaughtExceptionHandler (this );
99
+ }
89
100
90
101
this .options
91
102
.getLogger ()
@@ -157,14 +168,88 @@ static Throwable getUnhandledThrowable(
157
168
return new ExceptionMechanismException (mechanism , thrown , thread );
158
169
}
159
170
171
+ /**
172
+ * Remove this UncaughtExceptionHandlerIntegration from the exception handler chain.
173
+ *
174
+ * <p>If this integration is currently the default handler, restore the initial handler, if this
175
+ * integration is not the current default call removeFromHandlerTree
176
+ */
160
177
@ Override
161
178
public void close () {
162
- if (this == threadAdapter .getDefaultUncaughtExceptionHandler ()) {
163
- threadAdapter .setDefaultUncaughtExceptionHandler (defaultExceptionHandler );
179
+ try (final @ NotNull ISentryLifecycleToken ignored = lock .acquire ()) {
180
+ if (this == threadAdapter .getDefaultUncaughtExceptionHandler ()) {
181
+ threadAdapter .setDefaultUncaughtExceptionHandler (defaultExceptionHandler );
182
+
183
+ if (options != null ) {
184
+ options
185
+ .getLogger ()
186
+ .log (SentryLevel .DEBUG , "UncaughtExceptionHandlerIntegration removed." );
187
+ }
188
+ } else {
189
+ removeFromHandlerTree (threadAdapter .getDefaultUncaughtExceptionHandler ());
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Intermediary method before calling the actual recursive method. Used to initialize HashSet to
196
+ * keep track of visited handlers to avoid infinite recursion in case of cycles in the chain.
197
+ */
198
+ private void removeFromHandlerTree (@ Nullable Thread .UncaughtExceptionHandler currentHandler ) {
199
+ removeFromHandlerTree (currentHandler , new HashSet <>());
200
+ }
201
+
202
+ /**
203
+ * Recursively traverses the chain of UncaughtExceptionHandlerIntegrations to find and remove this
204
+ * specific integration instance.
205
+ *
206
+ * <p>Checks if this instance is the defaultExceptionHandler of the current handler, if so replace
207
+ * with its own defaultExceptionHandler, thus removing it from the chain.
208
+ *
209
+ * <p>If not, recursively calls itself on the next handler in the chain.
210
+ *
211
+ * <p>Recursion stops if the current handler is not an instance of
212
+ * UncaughtExceptionHandlerIntegration, the handler was found and removed or a cycle was detected.
213
+ *
214
+ * @param currentHandler The current handler in the chain to examine
215
+ * @param visited Set of already visited handlers to detect cycles
216
+ */
217
+ private void removeFromHandlerTree (
218
+ @ Nullable Thread .UncaughtExceptionHandler currentHandler ,
219
+ @ NotNull Set <Thread .UncaughtExceptionHandler > visited ) {
220
+
221
+ if (currentHandler == null ) {
222
+ if (options != null ) {
223
+ options .getLogger ().log (SentryLevel .DEBUG , "Found no UncaughtExceptionHandler to remove." );
224
+ }
225
+ return ;
226
+ }
227
+
228
+ if (!visited .add (currentHandler )) {
229
+ if (options != null ) {
230
+ options
231
+ .getLogger ()
232
+ .log (
233
+ SentryLevel .WARNING ,
234
+ "Cycle detected in UncaughtExceptionHandler chain while removing handler." );
235
+ }
236
+ return ;
237
+ }
238
+
239
+ if (!(currentHandler instanceof UncaughtExceptionHandlerIntegration )) {
240
+ return ;
241
+ }
242
+
243
+ final UncaughtExceptionHandlerIntegration currentHandlerIntegration =
244
+ (UncaughtExceptionHandlerIntegration ) currentHandler ;
164
245
246
+ if (this == currentHandlerIntegration .defaultExceptionHandler ) {
247
+ currentHandlerIntegration .defaultExceptionHandler = defaultExceptionHandler ;
165
248
if (options != null ) {
166
249
options .getLogger ().log (SentryLevel .DEBUG , "UncaughtExceptionHandlerIntegration removed." );
167
250
}
251
+ } else {
252
+ removeFromHandlerTree (currentHandlerIntegration .defaultExceptionHandler , visited );
168
253
}
169
254
}
170
255
0 commit comments