-
-
Notifications
You must be signed in to change notification settings - Fork 203
feat(android): allow NDK preload for .NET/CoreCLR #1613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
648b794
feat(android): preload NDK integration
jpnurmi a7e942e
test(android): parameterize signal tests for Mono/CoreCLR and preload
jpnurmi 6208a37
fix(android): preserve signal handler chain across close/reinit with …
jpnurmi 132bea8
fix(android): gate handler thread check on g_preloaded
jpnurmi ce86798
Update CHANGELOG.md
jpnurmi bc7eebd
Revert SENTRY_SIGNAL_SAFE_LOG changes that slipped in
jpnurmi 750c178
Update sentry-native-ndk.api
jpnurmi 8765273
fix(android): guard g_preloaded with SENTRY_PLATFORM_UNIX
jpnurmi 6c1fcf5
fix(android): reset signal handlers before fallthrough in preload path
jpnurmi 996de1c
fix(inproc): propagate signal handler install failure to sentry_init
jpnurmi 34eb38c
fix(android): clear g_preloaded after reset in preload fallthrough
jpnurmi 0aa6a80
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/andr…
jpnurmi fade4c8
Apply suggestions from code review
jpnurmi ad5004f
Apply remaining suggestions & run make format
jpnurmi f856845
Update sentry_close docs
jpnurmi d321297
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/andr…
jpnurmi 1b73d81
Update CHANGELOG.md
jpnurmi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| <application> | ||
| <!-- Must run before the managed runtime provider emitted by dotnet/android | ||
| so that sentry-native is already in the signal chain when the runtime | ||
| installs its own handlers. | ||
| initOrder must stay higher than AppInitOrder-1 (1999999999) used by the .NET runtime: | ||
| https://github.com/dotnet/android/blob/3c50ff4573b5/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs#L724 --> | ||
| <provider | ||
| android:name="io.sentry.ndk.SentryNdkPreloadProvider" | ||
| android:authorities="${applicationId}.SentryNdkPreloadProvider" | ||
| android:initOrder="2000000000" | ||
| android:exported="false" /> | ||
| </application> | ||
| </manifest> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
ndk/lib/src/main/java/io/sentry/ndk/SentryNdkPreloadProvider.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package io.sentry.ndk; | ||
|
|
||
| import android.content.ContentProvider; | ||
| import android.content.ContentValues; | ||
| import android.content.Context; | ||
| import android.content.pm.ApplicationInfo; | ||
| import android.content.pm.PackageManager; | ||
| import android.database.Cursor; | ||
| import android.net.Uri; | ||
| import android.os.Bundle; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| /** | ||
| * Preloads the NDK integration before the .NET runtime provider on Android. | ||
| * | ||
| * <p>This is intended for downstream SDK integrations that run with CoreCLR on | ||
| * Android. By installing sentry-native before the managed runtime registers | ||
| * its own signal handlers, native crash signals can chain from the runtime | ||
| * back to the Native SDK, while runtime-generated fault signals can still be | ||
| * consumed by the runtime for managed exception handling. | ||
| * | ||
| * <p>This is the preload alternative to CHAIN_AT_START. Mono on Android | ||
| * continues to use CHAIN_AT_START. | ||
| * | ||
| * <p>Enabled by setting {@code io.sentry.ndk.preload} to {@code true} in the | ||
| * app manifest metadata. The high {@code initOrder} ensures this runs before | ||
| * the runtime provider emitted by dotnet/android. | ||
| */ | ||
| public final class SentryNdkPreloadProvider extends ContentProvider { | ||
|
|
||
| @Override | ||
| public boolean onCreate() { | ||
| final Context context = getContext(); | ||
| if (context == null) { | ||
| return false; | ||
| } | ||
| try { | ||
| final ApplicationInfo info = | ||
| context | ||
| .getPackageManager() | ||
| .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); | ||
| final Bundle metadata = info.metaData; | ||
| if (metadata != null && metadata.getBoolean("io.sentry.ndk.preload", false)) { | ||
| android.util.Log.d("sentry", "io.sentry.ndk.preload read: true"); | ||
| SentryNdk.preload(); | ||
| android.util.Log.d("sentry", "SentryNdk.preload() completed"); | ||
| } | ||
| } catch (Throwable e) { | ||
| android.util.Log.e("sentry", "SentryNdk.preload() failed", e); | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nullable Cursor query( | ||
| @NotNull Uri uri, | ||
| @Nullable String[] projection, | ||
| @Nullable String selection, | ||
| @Nullable String[] selectionArgs, | ||
| @Nullable String sortOrder) { | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nullable String getType(@NotNull Uri uri) { | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public @Nullable Uri insert(@NotNull Uri uri, @Nullable ContentValues values) { | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public int delete(@NotNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public int update( | ||
| @NotNull Uri uri, | ||
| @Nullable ContentValues values, | ||
| @Nullable String selection, | ||
| @Nullable String[] selectionArgs) { | ||
| return 0; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -200,6 +200,14 @@ static volatile long g_handler_has_work = 0; | |||||||||||||||||||||||||||||||||||||
| #define CRASH_STATE_DONE 2 | ||||||||||||||||||||||||||||||||||||||
| static volatile long g_crash_handling_state = CRASH_STATE_IDLE; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Set once handlers were installed via sentry__backend_preload(). In this | ||||||||||||||||||||||||||||||||||||||
| // mode sentry may keep its chain position across sentry_close() until either | ||||||||||||||||||||||||||||||||||||||
| // full init reuses it, or a pre-init/post-close signal falls through, and | ||||||||||||||||||||||||||||||||||||||
| // consumes the preload state. | ||||||||||||||||||||||||||||||||||||||
| #ifdef SENTRY_PLATFORM_UNIX | ||||||||||||||||||||||||||||||||||||||
| static volatile long g_preloaded = 0; | ||||||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||||||
jpnurmi marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // trigger/schedule primitives that block the other side until this side is done | ||||||||||||||||||||||||||||||||||||||
| #ifdef SENTRY_PLATFORM_UNIX | ||||||||||||||||||||||||||||||||||||||
| static int g_handler_pipe[2] = { -1, -1 }; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -468,6 +476,37 @@ invoke_signal_handler(int signum, siginfo_t *info, void *user_context) | |||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| static int | ||||||||||||||||||||||||||||||||||||||
| install_signal_handlers(void) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| if (sentry__atomic_fetch(&g_preloaded)) { | ||||||||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| memset(g_previous_handlers, 0, sizeof(g_previous_handlers)); | ||||||||||||||||||||||||||||||||||||||
| for (size_t i = 0; i < SIGNAL_COUNT; ++i) { | ||||||||||||||||||||||||||||||||||||||
| if (sigaction( | ||||||||||||||||||||||||||||||||||||||
| SIGNAL_DEFINITIONS[i].signum, NULL, &g_previous_handlers[i]) | ||||||||||||||||||||||||||||||||||||||
| == -1) { | ||||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| setup_sigaltstack(&g_signal_stack, "init"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| sigemptyset(&g_sigaction.sa_mask); | ||||||||||||||||||||||||||||||||||||||
| g_sigaction.sa_sigaction = handle_signal; | ||||||||||||||||||||||||||||||||||||||
| // SA_NODEFER allows the signal to be delivered while the handler is | ||||||||||||||||||||||||||||||||||||||
| // running. This is needed for recursive crash detection to work - | ||||||||||||||||||||||||||||||||||||||
| // without it, a crash during crash handling would block the signal | ||||||||||||||||||||||||||||||||||||||
| // and leave the process in an undefined state. | ||||||||||||||||||||||||||||||||||||||
| g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; | ||||||||||||||||||||||||||||||||||||||
| for (size_t i = 0; i < SIGNAL_COUNT; ++i) { | ||||||||||||||||||||||||||||||||||||||
| sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| static int | ||||||||||||||||||||||||||||||||||||||
| startup_inproc_backend( | ||||||||||||||||||||||||||||||||||||||
| sentry_backend_t *backend, const sentry_options_t *options) | ||||||||||||||||||||||||||||||||||||||
sentry[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -503,39 +542,27 @@ startup_inproc_backend( | |||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // save the old signal handlers | ||||||||||||||||||||||||||||||||||||||
| memset(g_previous_handlers, 0, sizeof(g_previous_handlers)); | ||||||||||||||||||||||||||||||||||||||
| for (size_t i = 0; i < SIGNAL_COUNT; ++i) { | ||||||||||||||||||||||||||||||||||||||
| if (sigaction( | ||||||||||||||||||||||||||||||||||||||
| SIGNAL_DEFINITIONS[i].signum, NULL, &g_previous_handlers[i]) | ||||||||||||||||||||||||||||||||||||||
| == -1) { | ||||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| setup_sigaltstack(&g_signal_stack, "init"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // install our own signal handler | ||||||||||||||||||||||||||||||||||||||
| sigemptyset(&g_sigaction.sa_mask); | ||||||||||||||||||||||||||||||||||||||
| g_sigaction.sa_sigaction = handle_signal; | ||||||||||||||||||||||||||||||||||||||
| // SA_NODEFER allows the signal to be delivered while the handler is | ||||||||||||||||||||||||||||||||||||||
| // running. This is needed for recursive crash detection to work - | ||||||||||||||||||||||||||||||||||||||
| // without it, a crash during crash handling would block the signal | ||||||||||||||||||||||||||||||||||||||
| // and leave the process in an undefined state. | ||||||||||||||||||||||||||||||||||||||
| g_sigaction.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER; | ||||||||||||||||||||||||||||||||||||||
| for (size_t i = 0; i < SIGNAL_COUNT; ++i) { | ||||||||||||||||||||||||||||||||||||||
| sigaction(SIGNAL_DEFINITIONS[i].signum, &g_sigaction, NULL); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||||||
| return install_signal_handlers(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| static void | ||||||||||||||||||||||||||||||||||||||
| shutdown_inproc_backend(sentry_backend_t *backend) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| stop_handler_thread(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| teardown_sigaltstack(&g_signal_stack); | ||||||||||||||||||||||||||||||||||||||
| reset_signal_handlers(); | ||||||||||||||||||||||||||||||||||||||
| // In Android preload mode, we intentionally keep our signal handlers | ||||||||||||||||||||||||||||||||||||||
| // installed across shutdown. The handler thread is torn down, but our | ||||||||||||||||||||||||||||||||||||||
| // position in the signal chain must remain stable so that a later | ||||||||||||||||||||||||||||||||||||||
| // sentry_init() can reactivate full crash handling without losing the | ||||||||||||||||||||||||||||||||||||||
| // ordering relative to the managed runtime. | ||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||
| // While in this state, full inproc crash processing is inactive and any | ||||||||||||||||||||||||||||||||||||||
| // signal we still observe will fall through to the previously installed | ||||||||||||||||||||||||||||||||||||||
| // handler. | ||||||||||||||||||||||||||||||||||||||
| if (!sentry__atomic_fetch(&g_preloaded)) { | ||||||||||||||||||||||||||||||||||||||
| teardown_sigaltstack(&g_signal_stack); | ||||||||||||||||||||||||||||||||||||||
| reset_signal_handlers(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
jpnurmi marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (backend) { | ||||||||||||||||||||||||||||||||||||||
| backend->data = NULL; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1643,6 +1670,34 @@ process_ucontext(const sentry_ucontext_t *uctx) | |||||||||||||||||||||||||||||||||||||
| "multiple recursive crashes detected, bailing out"); | ||||||||||||||||||||||||||||||||||||||
| goto cleanup; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // If we were only preloaded (before sentry_init()) or were closed after | ||||||||||||||||||||||||||||||||||||||
| // preload (handlers still installed, but no handler thread), do not attempt | ||||||||||||||||||||||||||||||||||||||
| // full crash capture here. In this state, preload only serves as a | ||||||||||||||||||||||||||||||||||||||
| // placeholder in the signal chain. | ||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||
| // We therefore remove our placeholder from the chain and forward the | ||||||||||||||||||||||||||||||||||||||
| // signal to the previously installed handler set. | ||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||
| // This path is expected to be terminal for real crash signals. If it | ||||||||||||||||||||||||||||||||||||||
| // returns, preload mode is consumed for the remainder of the process | ||||||||||||||||||||||||||||||||||||||
| // lifetime until a later sentry_init() reinstalls handlers from the | ||||||||||||||||||||||||||||||||||||||
| // then-current chain. | ||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||
| // This also covers the window after preload but before the managed runtime | ||||||||||||||||||||||||||||||||||||||
| // has installed its own handlers: a signal seen in that window is forwarded | ||||||||||||||||||||||||||||||||||||||
| // to the pre-preload handler set rather than being captured by Sentry. | ||||||||||||||||||||||||||||||||||||||
| if (sentry__atomic_fetch(&g_preloaded) | ||||||||||||||||||||||||||||||||||||||
| && !sentry__atomic_fetch(&g_handler_thread_ready)) { | ||||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| SENTRY_SIGNAL_SAFE_LOG( | ||||||||||||||||||||||||||||||||||||||
| "handler thread not ready, falling through to previous handler"); | ||||||||||||||||||||||||||||||||||||||
| reset_signal_handlers(); | ||||||||||||||||||||||||||||||||||||||
| sentry__atomic_store(&g_preloaded, 0); | ||||||||||||||||||||||||||||||||||||||
| sentry__leave_signal_handler(); | ||||||||||||||||||||||||||||||||||||||
| invoke_signal_handler( | ||||||||||||||||||||||||||||||||||||||
| uctx->signum, uctx->siginfo, (void *)uctx->user_context); | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
sentry[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!g_backend_config.enable_logging_when_crashed) { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1742,6 +1797,16 @@ handle_except(sentry_backend_t *UNUSED(backend), const sentry_ucontext_t *uctx) | |||||||||||||||||||||||||||||||||||||
| process_ucontext(uctx); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| void | ||||||||||||||||||||||||||||||||||||||
| sentry__backend_preload(void) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| #ifdef SENTRY_PLATFORM_UNIX | ||||||||||||||||||||||||||||||||||||||
| if (install_signal_handlers() == 0) { | ||||||||||||||||||||||||||||||||||||||
| sentry__atomic_store(&g_preloaded, 1); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| sentry_backend_t * | ||||||||||||||||||||||||||||||||||||||
| sentry__backend_new(void) | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.