Skip to content
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

ref(on_crash): follow-up to make on_crash releasable #734

Merged
merged 13 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ jobs:

- name: Expose llvm PATH for Mac
if: ${{ runner.os == 'macOS' }}
run: echo $(brew --prefix llvm@13)/bin >> $GITHUB_PATH
run: echo $(brew --prefix llvm@14)/bin >> $GITHUB_PATH
supervacuus marked this conversation as resolved.
Show resolved Hide resolved

- name: Installing Android SDK Dependencies
if: ${{ env['ANDROID_API'] }}
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@

## Unreleased
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.5.0 or 0.4.19? Moving the sampling to the end of the pre-processing (in #729), thereby executing before_send() unconditionally, could be considered a breaking change. I am fine either way.


**Features**

- Provide `on_crash()` callback to allow clients to act on detected crashes
Swatinem marked this conversation as resolved.
Show resolved Hide resolved
([#724](https://github.com/getsentry/sentry-native/pull/724),
[#734](https://github.com/getsentry/sentry-native/pull/734))

**Fixes**

- Make Windows ModuleFinder more resilient to missing Debug Info
([#732](https://github.com/getsentry/sentry-native/pull/732))
- Aligned pre-send event processing in `sentry_capture_event()` with the
[cross-SDK session filter order](https://develop.sentry.dev/sdk/sessions/#filter-order)
([#729](https://github.com/getsentry/sentry-native/pull/729))

**Thank you**:

Features, fixes and improvements in this release have been contributed by:

- [@espkk](https://github.com/espkk)

## 0.4.18

**Features**:
Expand Down
63 changes: 63 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,51 @@
# define sleep_s(SECONDS) sleep(SECONDS)
#endif

static sentry_value_t
before_send_callback(sentry_value_t event, void *hint, void *closure)
{
(void)hint;
(void)closure;

// make our mark on the event
sentry_value_set_by_key(
event, "adapted_by", sentry_value_new_string("before_send"));

// tell the backend to proceed with the event
return event;
}

static sentry_value_t
discarding_before_send_callback(sentry_value_t event, void *hint, void *closure)
{
(void)hint;
(void)closure;

// discard event and signal backend to stop further processing
sentry_value_decref(event);
return sentry_value_new_null();
}

static int
discarding_on_crash_callback(const sentry_ucontext_t *uctx, void *closure)
{
(void)uctx;
(void)closure;

// tell the backend to discard the event
return 0;
}

static int
on_crash_callback(const sentry_ucontext_t *uctx, void *closure)
{
(void)uctx;
(void)closure;

// tell the backend to retain the event
return 1;
}

static void
print_envelope(sentry_envelope_t *envelope, void *unused_state)
{
Expand Down Expand Up @@ -105,6 +150,24 @@ main(int argc, char **argv)
sentry_options_set_max_spans(options, 5);
}

if (has_arg(argc, argv, "before-send")) {
sentry_options_set_before_send(options, before_send_callback, NULL);
}

if (has_arg(argc, argv, "discarding-before-send")) {
sentry_options_set_before_send(
options, discarding_before_send_callback, NULL);
}

if (has_arg(argc, argv, "discarding-on-crash")) {
sentry_options_set_on_crash(
options, discarding_on_crash_callback, NULL);
}

if (has_arg(argc, argv, "on-crash")) {
sentry_options_set_on_crash(options, on_crash_callback, NULL);
}

sentry_init(options);

if (!has_arg(argc, argv, "no-setup")) {
Expand Down
11 changes: 9 additions & 2 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ extern "C" {

#include <inttypes.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>

/* context type dependencies */
Expand Down Expand Up @@ -771,6 +770,10 @@ SENTRY_API void sentry_options_set_transport(
* call `sentry_value_decref` on the provided event, and return a
* `sentry_value_new_null()` instead.
*
* If you have set an `on_crash` callback (even one that only returns 1),
* `before_send` will no longer be invoked for crash-events. This allows you to
* better differentiate between crashes and all other events.
*
* This function may be invoked inside of a signal handler and must be safe for
* that purpose, see https://man7.org/linux/man-pages/man7/signal-safety.7.html.
* On Windows, it may be called from inside of a `UnhandledExceptionFilter`, see
Expand Down Expand Up @@ -804,13 +807,17 @@ SENTRY_API void sentry_options_set_before_send(
*
* If the callback returns false outgoing crash report will be discarded.
*
* If this callback is set in the options, a concurrently enabled `before_send`
* callback will no longer be called in the crash case. This allows for better
* differentiation between crashes and other events.
*
* This function may be invoked inside of a signal handler and must be safe for
* that purpose, see https://man7.org/linux/man-pages/man7/signal-safety.7.html.
* On Windows, it may be called from inside of a `UnhandledExceptionFilter`, see
* the documentation on SEH (structured exception handling) for more information
* https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling
*/
typedef bool (*sentry_crash_function_t)(
typedef int (*sentry_crash_function_t)(
const sentry_ucontext_t *uctx, void *closure);

/**
Expand Down
4 changes: 2 additions & 2 deletions src/backends/sentry_backend_breakpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ sentry__breakpad_backend_callback(

if (should_handle) {
sentry_value_t event = sentry_value_new_event();
sentry_envelope_t *envelope
= sentry__prepare_event(options, event, NULL);
sentry_envelope_t *envelope = sentry__prepare_event(
options, event, nullptr, !options->on_crash_func);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without informing sentry__prepare_event() whether before_send() should be invoked, we cannot make sure that a set on_crash() prevents the execution of the before_send() callback.

// the event we just prepared is empty,
// so no error is recorded for it
sentry__record_errors_on_current_session(1);
Expand Down
54 changes: 45 additions & 9 deletions src/backends/sentry_backend_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ extern "C" {
extern "C" {

#ifdef SENTRY_PLATFORM_LINUX
# include <unistd.h>
# define SIGNAL_STACK_SIZE 65536
static stack_t g_signal_stack;

Expand Down Expand Up @@ -133,10 +134,9 @@ sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context)
# endif
SENTRY_DEBUG("flushing session and queue before crashpad handler");

bool should_handle = true;
bool should_dump = true;

SENTRY_WITH_OPTIONS (options) {
sentry__write_crash_marker(options);

if (options->on_crash_func) {
sentry_ucontext_t uctx;
Expand All @@ -149,17 +149,19 @@ sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context)
# endif

SENTRY_TRACE("invoking `on_crash` hook");
should_handle
= options->on_crash_func(&uctx, options->on_crash_data);
should_dump = options->on_crash_func(&uctx, options->on_crash_data);
} else if (options->before_send_func) {
sentry_value_t event = sentry_value_new_event();
SENTRY_TRACE("invoking `before_send` hook");
event = options->before_send_func(
event, nullptr, options->before_send_data);
should_dump = !sentry_value_is_null(event);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was missing up to now. For the crashpad backend, it was irrelevant whether you returned the event or a null value.

The other difference (which I have not changed) to the other backends: we create the event right before the hook and then discard it again. The other backends allow changes made to the event received in before_send() to land in the backend.

sentry_value_decref(event);
}

if (should_handle) {
if (should_dump) {
sentry__write_crash_marker(options);

sentry__record_errors_on_current_session(1);
sentry_session_t *session = sentry__end_current_session_with_status(
SENTRY_SESSION_STATUS_CRASHED);
Expand All @@ -175,18 +177,52 @@ sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context)
sentry_transport_free(disk_transport);
}
} else {
SENTRY_TRACE("event was discarded by the `on_crash` hook");
SENTRY_TRACE("event was discarded");
}

sentry__transport_dump_queue(options->transport, options->run);
}

SENTRY_DEBUG("handing control over to crashpad");
# ifndef SENTRY_PLATFORM_WINDOWS
sentry__leave_signal_handler();
# endif
// further handling can be skipped via on_crash hook
return !should_handle;

// If we __don't__ want a minidump produced by crashpad we need to either
// exit or longjmp at this point. The crashpad client handler which calls
// back here (SetFirstChanceExceptionHandler) does the same if the
// application is not shutdown via the crashpad_handler process.
//
// If we would return `true` here without changing any of the global signal-
// handling state or rectifying the cause of the signal, this would turn
// into a signal-handler/exception-filter loop, because some
// signals/exceptions (like SIGSEGV) are unrecoverable.
//
// Ideally the SetFirstChanceExceptionHandler would accept more than a
// boolean to differentiate between:
//
// * we accept our fate and want a minidump (currently returning `false`)
// * we accept our fate and don't want a minidump (currently not available)
// * we rectified the situation, so crashpads signal-handler can simply
// return, thereby letting the not-rectified signal-cause trigger a
// signal-handler/exception-filter again, which probably leads to us
// (currently returning `true`)
//
// TODO(supervacuus):
// * we need integration tests for more signal/exception types not only
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have assert / abort in there, do these cover this?

// for unmapped memory access (which is the current crash in example.c).
// * we should adapt the SetFirstChanceExceptionHandler interface in
// crashpad
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is, as mentioned above, to change the state of the global signal configuration (aka reset to default handlers... which will in turn terminate the app). This looks cleaner on the surface, but I am not sure how happy crashpad is if we pull the rug in the first-chance handler. This is different from resetting when shutting down.

if (!should_dump) {
# ifdef SENTRY_PLATFORM_WINDOWS
// TerminateProcess(GetCurrentProcess(), kTerminationCodeCrashNoDump);
TerminateProcess(GetCurrentProcess(), 1);
# else
_exit(EXIT_FAILURE);
# endif
}

// we did not "handle" the signal, so crashpad should do that.
return false;
}
#endif

Expand Down
4 changes: 2 additions & 2 deletions src/backends/sentry_backend_inproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -535,8 +535,8 @@ handle_ucontext(const sentry_ucontext_t *uctx)
}

if (should_handle) {
supervacuus marked this conversation as resolved.
Show resolved Hide resolved
sentry_envelope_t *envelope
= sentry__prepare_event(options, event, NULL);
sentry_envelope_t *envelope = sentry__prepare_event(
options, event, NULL, !options->on_crash_func);
// TODO(tracing): Revisit when investigating transaction flushing
// during hard crashes.

Expand Down
6 changes: 3 additions & 3 deletions src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ sentry__capture_event(sentry_value_t event)
if (sentry__event_is_transaction(event)) {
envelope = sentry__prepare_transaction(options, event, &event_id);
} else {
envelope = sentry__prepare_event(options, event, &event_id);
envelope = sentry__prepare_event(options, event, &event_id, true);
}
if (envelope) {
if (options->session) {
Expand Down Expand Up @@ -460,7 +460,7 @@ sentry__should_send_transaction(sentry_value_t tx_cxt)

sentry_envelope_t *
sentry__prepare_event(const sentry_options_t *options, sentry_value_t event,
sentry_uuid_t *event_id)
sentry_uuid_t *event_id, bool invoke_before_send)
{
sentry_envelope_t *envelope = NULL;

Expand All @@ -477,7 +477,7 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event,
sentry__scope_apply_to_event(scope, options, event, mode);
}

if (options->before_send_func) {
if (options->before_send_func && invoke_before_send) {
SENTRY_TRACE("invoking `before_send` hook");
event
= options->before_send_func(event, NULL, options->before_send_data);
Expand Down
5 changes: 2 additions & 3 deletions src/sentry_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ bool sentry__event_is_transaction(sentry_value_t event);
* being passed in is not a transaction.
*
* More specifically, it will do the following things:
* - sample the event, possibly discarding it,
* - apply the scope to it,
* - call the before_send hook on it,
* - call the before_send hook on it (if invoke_before_send == true),
* - add the event to a new envelope,
* - record errors on the current session,
* - add any attachments to the envelope as well
Expand All @@ -51,7 +50,7 @@ bool sentry__event_is_transaction(sentry_value_t event);
* `event_id` out-parameter.
*/
sentry_envelope_t *sentry__prepare_event(const sentry_options_t *options,
sentry_value_t event, sentry_uuid_t *event_id);
sentry_value_t event, sentry_uuid_t *event_id, bool invoke_before_send);

/**
* Sends a sentry event, regardless of its type.
Expand Down
23 changes: 21 additions & 2 deletions tests/assertions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import datetime
import email
import gzip
import sys
import platform
import re
from .conditions import is_android
import sys

from .conditions import is_android

VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)(?:[-\.]?)(.*)")

Expand Down Expand Up @@ -194,6 +194,25 @@ def assert_crash(envelope):
assert_stacktrace(envelope, inside_exception=True, check_size=False)


def assert_crash_timestamp(has_files, tmp_path):
# The crash file should survive a `sentry_init` and should still be there
# even after restarts.
if has_files:
with open("{}/.sentry-native/last_crash".format(tmp_path)) as f:
crash_timestamp = f.read()
assert_timestamp(crash_timestamp)


def assert_before_send(envelope):
event = envelope.get_event()
assert_matches(event, {"adapted_by": "before_send"})


def assert_no_before_send(envelope):
event = envelope.get_event()
assert ("adapted_by", "before_send") not in event.items()


def assert_crashpad_upload(req):
multipart = gzip.decompress(req.get_data())
msg = email.message_from_bytes(bytes(str(req.headers), encoding="utf8") + multipart)
Expand Down