1010import io .sentry .hints .TransactionEnd ;
1111import io .sentry .protocol .Mechanism ;
1212import io .sentry .protocol .SentryId ;
13+ import io .sentry .util .AutoClosableReentrantLock ;
1314import io .sentry .util .HintUtils ;
1415import io .sentry .util .Objects ;
1516import java .io .Closeable ;
17+ import java .util .HashSet ;
18+ import java .util .Set ;
1619import java .util .concurrent .atomic .AtomicReference ;
1720import org .jetbrains .annotations .ApiStatus ;
1821import org .jetbrains .annotations .NotNull ;
@@ -28,6 +31,8 @@ public final class UncaughtExceptionHandlerIntegration
2831 /** Reference to the pre-existing uncaught exception handler. */
2932 private @ Nullable Thread .UncaughtExceptionHandler defaultExceptionHandler ;
3033
34+ private static final @ NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock ();
35+
3136 private @ Nullable IScopes scopes ;
3237 private @ Nullable SentryOptions options ;
3338
@@ -65,27 +70,33 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO
6570 this .options .isEnableUncaughtExceptionHandler ());
6671
6772 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+ }
8596 }
86- }
8797
88- threadAdapter .setDefaultUncaughtExceptionHandler (this );
98+ threadAdapter .setDefaultUncaughtExceptionHandler (this );
99+ }
89100
90101 this .options
91102 .getLogger ()
@@ -157,14 +168,88 @@ static Throwable getUnhandledThrowable(
157168 return new ExceptionMechanismException (mechanism , thrown , thread );
158169 }
159170
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+ */
160177 @ Override
161178 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 ;
164245
246+ if (this == currentHandlerIntegration .defaultExceptionHandler ) {
247+ currentHandlerIntegration .defaultExceptionHandler = defaultExceptionHandler ;
165248 if (options != null ) {
166249 options .getLogger ().log (SentryLevel .DEBUG , "UncaughtExceptionHandlerIntegration removed." );
167250 }
251+ } else {
252+ removeFromHandlerTree (currentHandlerIntegration .defaultExceptionHandler , visited );
168253 }
169254 }
170255
0 commit comments