diff --git a/examples/example.c b/examples/example.c index 16c2edd22..d3a083280 100644 --- a/examples/example.c +++ b/examples/example.c @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef NDEBUG # undef NDEBUG @@ -20,6 +21,7 @@ #ifdef SENTRY_PLATFORM_WINDOWS # include +# include # include # define sleep_s(SECONDS) Sleep((SECONDS) * 1000) #else @@ -137,6 +139,83 @@ on_crash_callback( return event; } +static sentry_value_t +restart_on_crash( + const sentry_ucontext_t *uctx, sentry_value_t event, void *user_data) +{ + (void)uctx; + +#ifdef SENTRY_PLATFORM_WINDOWS + wchar_t **argv = user_data; + if (argv && argv[0]) { + _wspawnv(_P_NOWAIT, argv[0], (const wchar_t *const *)argv); + } +#else + char **argv = user_data; + if (!argv || !argv[0]) { + return event; + } + if (fork() == 0) { + // The crashing signal is blocked while the crash handler runs. Do not + // let the restarted child inherit that mask. + sigset_t set; + sigfillset(&set); + sigprocmask(SIG_UNBLOCK, &set, NULL); + + execv(argv[0], argv); + _exit(127); + } +#endif + + return event; +} + +// Forward all original arguments except "restart-on-crash" +static void * +restart_args(int argc, char **argv) +{ +#ifdef SENTRY_PLATFORM_WINDOWS + wchar_t **child_argv = calloc((size_t)argc + 1, sizeof(wchar_t *)); + if (!child_argv) { + return NULL; + } + + child_argv[0] = calloc(MAX_PATH, sizeof(wchar_t)); + if (!child_argv[0] + || GetModuleFileNameW(NULL, child_argv[0], MAX_PATH) == 0) { + return child_argv; + } + + int child_argc = 1; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "restart-on-crash") == 0) { + continue; + } + + size_t len = strlen(argv[i]) + 1; + child_argv[child_argc] = calloc(len, sizeof(wchar_t)); + if (!child_argv[child_argc]) { + return child_argv; + } + mbstowcs(child_argv[child_argc++], argv[i], len); + } + return child_argv; +#else + char **child_argv = calloc((size_t)argc + 1, sizeof(char *)); + if (!child_argv) { + return NULL; + } + + int child_argc = 0; + for (int i = 0; i < argc; i++) { + if (strcmp(argv[i], "restart-on-crash") != 0) { + child_argv[child_argc++] = argv[i]; + } + } + return child_argv; +#endif +} + static sentry_value_t before_transaction_callback(sentry_value_t tx, void *user_data) { @@ -637,6 +716,11 @@ main(int argc, char **argv) options, discarding_on_crash_callback, NULL); } + if (has_arg(argc, argv, "restart-on-crash")) { + sentry_options_set_on_crash( + options, restart_on_crash, restart_args(argc, argv)); + } + if (has_arg(argc, argv, "before-transaction")) { sentry_options_set_before_transaction( options, before_transaction_callback, NULL); diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index dd05892c5..b73f5e73e 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -1063,3 +1063,31 @@ def test_crashpad_cache_max_age(cmake, httpserver): assert len(cache_files) == 3 for f in cache_files: assert time.time() - f.stat().st_mtime <= 5 * 24 * 60 * 60 + + +@pytest.mark.skipif( + sys.platform == "darwin", + reason="crashpad doesn't provide SetFirstChanceExceptionHandler on macOS", +) +def test_crashpad_restart_on_crash(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) + + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + # The restarted child inherits stdio, so PIPE waits for it without a sleep. + run( + tmp_path, + "sentry_example", + ["crash", "restart-on-crash"], + expect_failure=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert waiting.result + assert len(httpserver.log) == 2 + for req in httpserver.log: + assert_crashpad_upload(req[0]) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 81633bbb3..b42dec0d5 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1,6 +1,7 @@ import itertools import json import os +import subprocess import time import pytest @@ -1317,3 +1318,58 @@ def test_http_retry_session_on_network_error(cmake, httpserver, unreachable_dsn) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 0 + + +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad or is_qemu, reason="test needs breakpad backend" + ), + ), + ], +) +@pytest.mark.skipif(is_qemu, reason="unreliable under qemu-user") +def test_restart_on_crash(cmake, httpserver, backend): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + # The restarted child inherits stdio, so PIPE waits for it without a sleep. + run( + tmp_path, + "sentry_example", + ["crash", "restart-on-crash"], + expect_failure=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + run( + tmp_path, + "sentry_example", + ["log", "no-setup"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert waiting.result + assert len(httpserver.log) == 2 + for req in httpserver.log: + envelope = Envelope.deserialize(req[0].get_data()) + if backend == "inproc": + assert_inproc_crash(envelope) + elif backend == "breakpad": + assert_breakpad_crash(envelope) + else: + assert False diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 138564cd1..7a5075c77 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -6,6 +6,7 @@ """ import os +import subprocess import sys import time import struct @@ -39,7 +40,7 @@ SANITIZER_ARGS = ["shutdown-timeout", "10000"] if is_asan or is_tsan else [] -def run_crash(tmp_path, exe, args, env, wait_for_daemon=False): +def run_crash(tmp_path, exe, args, env, **kwargs): """ Run a crash test. @@ -62,14 +63,7 @@ def run_crash(tmp_path, exe, args, env, wait_for_daemon=False): else: env["ASAN_OPTIONS"] = asan_signal_opts - run( - tmp_path, - exe, - args, - expect_failure=True, - env=env, - wait_for_daemon=wait_for_daemon, - ) + run(tmp_path, exe, args, expect_failure=True, env=env, **kwargs) def test_native_capture_crash(cmake, httpserver): @@ -1024,3 +1018,27 @@ def test_native_cache_keep(cmake, cache_keep, unreachable_dsn): assert cache_files[0].stem == dmp_files[0].stem else: assert len(list(cache_dir.glob("*.envelope"))) == 0 + + +def test_native_restart_on_crash(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + # The restarted child inherits stdio, so PIPE waits for it without a sleep. + run_crash( + tmp_path, + "sentry_example", + ["crash", "restart-on-crash"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert waiting.result + assert len(httpserver.log) == 2 + for req in httpserver.log: + envelope = Envelope.deserialize(req[0].get_data()) + assert_native_crash(envelope)