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
84 changes: 84 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

#ifdef NDEBUG
# undef NDEBUG
Expand All @@ -20,6 +21,7 @@

#ifdef SENTRY_PLATFORM_WINDOWS
# include <malloc.h>
# include <process.h>
# include <synchapi.h>
# define sleep_s(SECONDS) Sleep((SECONDS) * 1000)
#else
Expand Down Expand Up @@ -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);
Comment thread
jpnurmi marked this conversation as resolved.
}
#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;
}
Comment thread
jpnurmi marked this conversation as resolved.

// 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;
Comment thread
jpnurmi marked this conversation as resolved.
}

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)
{
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions tests/test_integration_crashpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
56 changes: 56 additions & 0 deletions tests/test_integration_http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import itertools
import json
import os
import subprocess
import time

import pytest
Expand Down Expand Up @@ -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):
Comment thread
sentry[bot] marked this conversation as resolved.
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
36 changes: 27 additions & 9 deletions tests/test_integration_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import os
import subprocess
import sys
import time
import struct
Expand Down Expand Up @@ -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.

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Loading