Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

**Features**:

- Native: add opt-in async crash upload mode so crashed apps can exit early after crash data is captured, while the crash daemon finishes potentially large uploads in the background. ([#1739](https://github.com/getsentry/sentry-native/pull/1739))
- Add a `transfer_timeout` option for SDK-managed HTTP transports. ([#1741](https://github.com/getsentry/sentry-native/pull/1741))

**Fixes**:
Expand Down
4 changes: 4 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,10 @@ main(int argc, char **argv)
}
}
}
if (has_arg(argc, argv, "async-crash-upload")) {
sentry_options_set_crash_upload_mode(
options, SENTRY_CRASH_UPLOAD_MODE_ASYNC);
}

// E2E test mode: generate unique test ID for event correlation
char e2e_test_id[37] = { 0 };
Expand Down
40 changes: 40 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,25 @@ typedef enum {
SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP = 2,
} sentry_crash_reporting_mode_t;

/**
* Crash upload mode for the native backend.
* Controls whether the crashed application remains blocked while upload and
* shutdown work finishes after crash data has been captured.
*/
typedef enum {
/**
* Keep the crashed application blocked until the native crash daemon
* finishes upload and shutdown work.
*/
SENTRY_CRASH_UPLOAD_MODE_SYNC = 0,

/**
* Allow the crashed application to terminate after crash data has been
* captured. The native crash daemon continues upload and shutdown work.
*/
SENTRY_CRASH_UPLOAD_MODE_ASYNC = 1,
} sentry_crash_upload_mode_t;

/**
* Controls if and when envelopes are kept in the persistent cache.
*/
Expand Down Expand Up @@ -1882,6 +1901,27 @@ SENTRY_API void sentry_options_set_crash_reporting_mode(
SENTRY_API sentry_crash_reporting_mode_t
sentry_options_get_crash_reporting_mode(const sentry_options_t *opts);

/**
* Sets the crash upload mode for the native backend.
*
* This setting controls what happens after crash data has been captured. In
* sync mode, the crashed application remains blocked while the native crash
* daemon finishes upload and shutdown work. In async mode, the crashed
* application can terminate after crash data has been captured while the daemon
* continues upload and shutdown work.
*
* This setting only has an effect when using the `native` backend.
* Default is `SENTRY_CRASH_UPLOAD_MODE_SYNC`.
*/
SENTRY_API void sentry_options_set_crash_upload_mode(
sentry_options_t *opts, sentry_crash_upload_mode_t mode);

/**
* Gets the crash upload mode for the native backend.
*/
SENTRY_API sentry_crash_upload_mode_t sentry_options_get_crash_upload_mode(
const sentry_options_t *opts);

/**
* Enables a wait for the crash report upload to be finished before shutting
* down. This is disabled by default.
Expand Down
4 changes: 3 additions & 1 deletion src/backends/native/sentry_crash_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ typedef enum {
SENTRY_CRASH_STATE_READY = 0,
SENTRY_CRASH_STATE_CRASHED = 1,
SENTRY_CRASH_STATE_PROCESSING = 2,
SENTRY_CRASH_STATE_DONE = 3
SENTRY_CRASH_STATE_CAPTURED = 3,
SENTRY_CRASH_STATE_DONE = 4
} sentry_crash_state_t;

/**
Expand Down Expand Up @@ -274,6 +275,7 @@ typedef struct {
// Configuration (set by app during init)
sentry_minidump_mode_t minidump_mode;
int crash_reporting_mode; // sentry_crash_reporting_mode_t
int crash_upload_mode; // sentry_crash_upload_mode_t
bool debug_enabled; // Debug logging enabled in parent process
bool attach_screenshot; // Screenshot attachment enabled in parent process
bool attach_session_replay; // Session replay attachment enabled in parent
Expand Down
21 changes: 19 additions & 2 deletions src/backends/native/sentry_crash_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -2805,10 +2805,11 @@ write_envelope_with_minidump(const sentry_options_t *options,
*
* Called by the crash daemon (out-of-process on Linux/macOS).
*/
void
bool
sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
{
SENTRY_DEBUG("Processing crash - START");
bool crash_captured = false;

sentry_crash_context_t *ctx = ipc->shmem;

Expand Down Expand Up @@ -3119,11 +3120,14 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
if (options && options->transport && options->run) {
SENTRY_DEBUG("Capturing crash envelope");
sentry__capture_envelope(options->transport, envelope, options);
crash_captured = true;
SENTRY_DEBUG("Crash envelope captured (queued)");
} else {
SENTRY_WARN("No transport available for sending envelope");
sentry_envelope_free(envelope);
}
} else {
crash_captured = true;
}

// Clean up temporary envelope file (keep minidump for
Expand Down Expand Up @@ -3194,6 +3198,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
done:
SENTRY_DEBUG("Processing crash - END");
SENTRY_DEBUG("Crash processing complete");
return crash_captured;
}

/**
Expand Down Expand Up @@ -3481,9 +3486,21 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
long state = sentry__atomic_fetch(&ipc->shmem->state);
if (state == SENTRY_CRASH_STATE_CRASHED && !crash_processed) {
SENTRY_DEBUG("Crash notification received, processing");
sentry__process_crash(options, ipc);
bool crash_captured = sentry__process_crash(options, ipc);
crash_processed = true;

if (crash_captured
&& ipc->shmem->crash_upload_mode
== SENTRY_CRASH_UPLOAD_MODE_ASYNC) {
// Crash data is durable after processing returns;
// remaining daemon work does not require the crashed
// process.
SENTRY_DEBUG(
"Crash captured, allowing app process to exit");
sentry__atomic_store(
&ipc->shmem->state, SENTRY_CRASH_STATE_CAPTURED);
Comment thread
cursor[bot] marked this conversation as resolved.
}

// After processing crash, exit regardless of parent state
// (parent has likely already exited after re-raising signal)
SENTRY_DEBUG("Crash processed, daemon exiting");
Expand Down
3 changes: 2 additions & 1 deletion src/backends/native/sentry_crash_daemon.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ int sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid,
*
* @param options Sentry options (DSN, transport, etc.)
* @param ipc Crash IPC with crash context in shared memory
* @return true if the crash envelope was captured for upload or reporting
*/
void sentry__process_crash(
bool sentry__process_crash(
const struct sentry_options_s *options, sentry_crash_ipc_t *ipc);

#endif
8 changes: 4 additions & 4 deletions src/backends/native/sentry_crash_handler.c
Original file line number Diff line number Diff line change
Expand Up @@ -670,8 +670,8 @@ crash_signal_handler(int signum, siginfo_t *info, void *context)
if (state == SENTRY_CRASH_STATE_PROCESSING && !processing_started) {
// Daemon started processing (no logging - signal-safe)
processing_started = true;
} else if (state == SENTRY_CRASH_STATE_DONE) {
// Daemon finished processing (no logging - signal-safe)
} else if (state >= SENTRY_CRASH_STATE_CAPTURED) {
// Daemon captured crash data (no logging - signal-safe)
goto daemon_handling;
}

Expand Down Expand Up @@ -954,8 +954,8 @@ crash_exception_filter(EXCEPTION_POINTERS *exception_info)
// Daemon started processing (no logging - exception filter
// context)
processing_started = true;
} else if (state == SENTRY_CRASH_STATE_DONE) {
// Daemon finished processing (no logging - exception filter
} else if (state >= SENTRY_CRASH_STATE_CAPTURED) {
// Daemon captured crash data (no logging - exception filter
// context)
break;
}
Expand Down
2 changes: 1 addition & 1 deletion src/backends/native/sentry_wer.c
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ process_wer_exception(
waited_ms += SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS) {
if (InterlockedCompareExchange(&ctx->state,
SENTRY_CRASH_STATE_DONE, SENTRY_CRASH_STATE_DONE)
== SENTRY_CRASH_STATE_DONE) {
>= SENTRY_CRASH_STATE_CAPTURED) {
break;
}
Sleep(SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS);
Expand Down
1 change: 1 addition & 0 deletions src/backends/sentry_backend_native.c
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ native_backend_startup(

// Set crash reporting mode from options
ctx->crash_reporting_mode = options->crash_reporting_mode;
ctx->crash_upload_mode = options->crash_upload_mode;

// Pass debug logging setting to daemon
ctx->debug_enabled = options->debug;
Expand Down
20 changes: 20 additions & 0 deletions src/sentry_options.c
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ sentry_options_new(void)
opts->crash_reporting_mode
= SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP; // Default: best of
// both worlds
opts->crash_upload_mode = SENTRY_CRASH_UPLOAD_MODE_SYNC;
opts->http_retry = false;
opts->send_client_reports = true;
opts->enable_large_attachments = false;
Expand Down Expand Up @@ -616,6 +617,25 @@ sentry_options_get_crash_reporting_mode(const sentry_options_t *opts)
return (sentry_crash_reporting_mode_t)opts->crash_reporting_mode;
}

void
sentry_options_set_crash_upload_mode(
sentry_options_t *opts, sentry_crash_upload_mode_t mode)
{
int imode = (int)mode;
if (imode < SENTRY_CRASH_UPLOAD_MODE_SYNC) {
imode = SENTRY_CRASH_UPLOAD_MODE_SYNC;
} else if (imode > SENTRY_CRASH_UPLOAD_MODE_ASYNC) {
imode = SENTRY_CRASH_UPLOAD_MODE_ASYNC;
}
opts->crash_upload_mode = imode;
}

sentry_crash_upload_mode_t
sentry_options_get_crash_upload_mode(const sentry_options_t *opts)
{
return (sentry_crash_upload_mode_t)opts->crash_upload_mode;
}

void
sentry_options_set_crashpad_wait_for_upload(
sentry_options_t *opts, int wait_for_upload)
Expand Down
1 change: 1 addition & 0 deletions src/sentry_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ struct sentry_options_s {
// sentry_crash_context.h)
int crash_reporting_mode; // 0=minidump, 1=native, 2=native_with_minidump
// (see sentry_crash_reporting_mode_t)
int crash_upload_mode; // 0=sync, 1=async (see sentry_crash_upload_mode_t)

#ifdef SENTRY_PLATFORM_NX
void (*network_connect_func)(void);
Expand Down
16 changes: 15 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io
import json
import sys
import time
import urllib
import pytest
import pprint
Expand All @@ -24,6 +25,8 @@ def adb(*args, **kwargs):

SENTRY_VERSION = "0.14.2"

from .assertions import wait_for_daemon as _wait_for_daemon


def make_dsn(httpserver, auth="uiaeosnrtdy", id=123456, proxy_host=False):
url = urllib.parse.urlsplit(httpserver.url_for("/{}".format(id)))
Expand Down Expand Up @@ -96,7 +99,13 @@ def extract_request(httpserver_log, cond):
return (None, httpserver_log)


def run(cwd, exe, args, expect_failure=False, env=None, **kwargs):
def run(
cwd, exe, args, expect_failure=False, env=None, wait_for_daemon=False, **kwargs
):
if wait_for_daemon:
assert (
"log" in args or exe != "sentry_example"
), "sentry_example needs 'log' when waiting for the daemon"
if env is None:
env = dict(os.environ)
if kwargs.get("check"):
Expand All @@ -105,6 +114,7 @@ def run(cwd, exe, args, expect_failure=False, env=None, **kwargs):
)
check = expect_failure == False
__tracebackhide__ = True
started_at = time.time()
Comment thread
jpnurmi marked this conversation as resolved.
if os.environ.get("ANDROID_API"):
# older android emulators do not correctly pass down the returncode
# so we basically echo the return code, and parse it manually
Expand Down Expand Up @@ -179,6 +189,10 @@ def run(cwd, exe, args, expect_failure=False, env=None, **kwargs):
]
try:
result = subprocess.run([*cmd, *args], cwd=cwd, env=env, check=check, **kwargs)
if wait_for_daemon:
assert _wait_for_daemon(
cwd, started_at
), "native crash daemon did not finish within timeout"
if expect_failure:
assert (
result.returncode != 0
Expand Down
30 changes: 29 additions & 1 deletion tests/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import msgpack

from . import SENTRY_VERSION
from .conditions import is_android
from .conditions import is_android, is_asan, is_tsan

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

Expand Down Expand Up @@ -676,3 +676,31 @@ def wait_for_file(path, timeout=10.0, poll_interval=0.1):
return True
time.sleep(poll_interval)
return False


def wait_for_daemon(tmp_path, started_at, timeout=None):
import time

if timeout is None:
timeout = 30.0 if is_asan or is_tsan else 10.0

db_dir = Path(tmp_path) / ".sentry-native"
# Account for filesystems that truncate mtimes below time.time() precision.
started_at -= 1.0

deadline = time.time() + timeout
while time.time() < deadline:
for log_path in db_dir.glob("sentry-daemon-*.log"):
try:
if log_path.stat().st_mtime < started_at:
continue
log = log_path.read_text(errors="replace")
except OSError:
continue

if "Marking crash state as DONE" in log:
Comment thread
sentry[bot] marked this conversation as resolved.
return True
Comment thread
cursor[bot] marked this conversation as resolved.

time.sleep(0.1)

return False
22 changes: 16 additions & 6 deletions tests/test_e2e_sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def extract_test_id(output):
raise ValueError(f"TEST_ID not found in output. Output was:\n{decoded[:500]}")


def run_crash_e2e(tmp_path, exe, args, env):
def run_crash_e2e(tmp_path, exe, args, env, wait_for_daemon=False):
"""
Run a crash test for E2E, capturing output for test ID extraction.

Expand All @@ -370,7 +370,14 @@ def run_crash_e2e(tmp_path, exe, args, env):
env["ASAN_OPTIONS"] = asan_signal_opts

# Use check_output to capture stdout for test ID extraction
return check_output(tmp_path, exe, args, env=env, expect_failure=True)
return check_output(
tmp_path,
exe,
args,
env=env,
expect_failure=True,
wait_for_daemon=wait_for_daemon,
)


@pytest.mark.skipif(
Expand Down Expand Up @@ -427,12 +434,15 @@ def run_crash_and_send(self, mode_args):
# Run with crash - capture output for test ID
# Enable structured logs and capture a log message before crashing
crash_args = ["log", "e2e-test", "capture-log"] + mode_args + ["crash"]
output = run_crash_e2e(self.tmp_path, "sentry_example", crash_args, env=env)
output = run_crash_e2e(
self.tmp_path,
"sentry_example",
crash_args,
env=env,
wait_for_daemon=True,
)
test_id = extract_test_id(output)

# Wait for crash daemon to process
time.sleep(2)

# Print daemon logs for debugging (especially useful for Windows thread duplication investigation)
self.print_daemon_logs()

Expand Down
Loading
Loading