From d5faff5e02752df7f85629b5a7af596e82593ba5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 11 Aug 2026 12:28:04 +0200 Subject: [PATCH 01/13] ref(native): use .run for crash daemon Have the native crash daemon adopt the application's existing .run instead of creating a second unrelated .run, and store daemon logs, minidumps, crash envelopes, and signal-handler scratch files within that .run. Protect daemon-owned work with a dedicated `.run.daemon.lock` so old-run processing waits for completion. Keep retry cache and external storage database-global, and leave crashed runs for normal next-launch cleanup. --- .../native/minidump/sentry_minidump_macos.c | 18 +-- src/backends/native/sentry_crash_context.h | 4 +- src/backends/native/sentry_crash_daemon.c | 148 ++++++------------ src/backends/native/sentry_crash_handler.c | 119 +++++--------- src/backends/sentry_backend_native.c | 108 ++++++------- src/sentry_database.c | 103 +++++++++--- src/sentry_database.h | 16 +- tests/assertions.py | 2 +- tests/test_e2e_sentry.py | 2 +- tests/test_integration_native.py | 71 ++++++++- tests/test_integration_tus.py | 4 +- tests/unit/test_native_backend.c | 92 +++++++++++ tests/unit/tests.inc | 2 + 13 files changed, 409 insertions(+), 280 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_macos.c b/src/backends/native/minidump/sentry_minidump_macos.c index 982024ddde..f1456f5f6a 100644 --- a/src/backends/native/minidump/sentry_minidump_macos.c +++ b/src/backends/native/minidump/sentry_minidump_macos.c @@ -1314,14 +1314,14 @@ write_module_headers_from_capture(minidump_writer_t *writer, { const size_t HEADER_PAGE_SIZE = 4096; - // Build path: {database_path}/__sentry-modheaders - const char *db_path = writer->crash_ctx->database_path; - size_t db_len = strlen(db_path); + // Build path: {run_path}/__sentry-modheaders + const char *run_path = writer->crash_ctx->run_path; + size_t run_len = strlen(run_path); char hdr_path[SENTRY_CRASH_MAX_PATH]; - if (db_len + 22 >= sizeof(hdr_path)) { + if (run_len + 22 >= sizeof(hdr_path)) { return 0; } - snprintf(hdr_path, sizeof(hdr_path), "%s/__sentry-modheaders", db_path); + snprintf(hdr_path, sizeof(hdr_path), "%s/__sentry-modheaders", run_path); int fd = open(hdr_path, O_RDONLY); if (fd < 0) { @@ -1672,12 +1672,12 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) // Clean up the capture file written by the signal handler since // we used VM regions instead. - const char *db_path = writer->crash_ctx->database_path; - size_t db_len = strlen(db_path); + const char *run_path = writer->crash_ctx->run_path; + size_t run_len = strlen(run_path); char hdr_path[SENTRY_CRASH_MAX_PATH]; - if (db_len + 22 < sizeof(hdr_path)) { + if (run_len + 22 < sizeof(hdr_path)) { snprintf( - hdr_path, sizeof(hdr_path), "%s/__sentry-modheaders", db_path); + hdr_path, sizeof(hdr_path), "%s/__sentry-modheaders", run_path); unlink(hdr_path); } diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 20681b812b..5a51c06ad1 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -312,8 +312,8 @@ typedef struct { #endif // Sentry-specific metadata paths - char database_path[SENTRY_CRASH_MAX_PATH]; // Database directory for all - // files + char database_path[SENTRY_CRASH_MAX_PATH]; // Shared across runs + char run_path[SENTRY_CRASH_MAX_PATH]; // For current run char event_path[SENTRY_CRASH_MAX_PATH]; char breadcrumb1_path[SENTRY_CRASH_MAX_PATH]; char breadcrumb2_path[SENTRY_CRASH_MAX_PATH]; diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 6fa65ecd79..714751715b 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4003,14 +4003,13 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) bool use_native_mode = (mode == SENTRY_CRASH_REPORTING_MODE_NATIVE || mode == SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP); - // Generate minidump path in database directory + // Generate minidump path in run directory char minidump_path[SENTRY_CRASH_MAX_PATH] = { 0 }; - const char *db_dir = ctx->database_path; + const char *run_dir = ctx->run_path; if (need_minidump) { int path_len = snprintf(minidump_path, sizeof(minidump_path), - "%s/sentry-minidump-%lu-%lu.dmp", db_dir, - (unsigned long)ctx->crashed_pid, (unsigned long)ctx->crashed_tid); + "%s/__sentry-crash.dmp", run_dir); if (path_len < 0 || path_len >= (int)sizeof(minidump_path)) { SENTRY_WARN("Minidump path truncated or invalid"); @@ -4082,29 +4081,19 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) sentry_path_t *ev_path = sentry__path_from_str(event_path); sentry_path_t *run_folder = ev_path ? sentry__path_dir(ev_path) : NULL; - // Acquire the run directory lock file so that process_old_runs() in a - // new SDK run will skip this directory while the daemon is still - // processing the crash. The crashed process's flock() is released on - // death, so without this the new run could delete the directory. - sentry_filelock_t *run_lock = NULL; - if (run_folder) { - sentry_path_t *lock_path = sentry__path_append_str(run_folder, ".lock"); - if (lock_path) { - run_lock = sentry__filelock_new(lock_path); - if (run_lock) { - if (!sentry__filelock_try_lock(run_lock)) { - SENTRY_WARN("daemon could not acquire run folder lock"); - sentry__filelock_free(run_lock); - run_lock = NULL; - } - } - } + // The crashing process dumps its pending logs, sessions, and transactions + // before notifying the daemon. Queue those before writing the crash + // envelope so an attachment-ref prewrite is not captured a second time. + if (run_folder && options && options->transport && options->run) { + sentry__process_run_envelopes(options, run_folder); + } else { + SENTRY_DEBUG("No run folder or transport for additional envelopes"); } - // Create envelope file in database directory + // Create envelope file in run directory char envelope_path[SENTRY_CRASH_MAX_PATH]; - int path_len = snprintf(envelope_path, sizeof(envelope_path), - "%s/sentry-envelope-%lu.env", db_dir, (unsigned long)ctx->crashed_pid); + int path_len = snprintf( + envelope_path, sizeof(envelope_path), "%s", ctx->envelope_path); if (path_len < 0 || path_len >= (int)sizeof(envelope_path)) { SENTRY_WARN("Envelope path truncated or invalid"); @@ -4112,10 +4101,6 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) if (run_folder) { sentry__path_free(run_folder); } - if (run_lock) { - sentry__filelock_unlock(run_lock); - sentry__filelock_free(run_lock); - } goto done; } @@ -4216,10 +4201,6 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) if (run_folder) { sentry__path_free(run_folder); } - if (run_lock) { - sentry__filelock_unlock(run_lock); - sentry__filelock_free(run_lock); - } goto done; } SENTRY_DEBUG("Envelope written successfully"); @@ -4335,57 +4316,9 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) sentry_value_decref(crash_event); } - // Send all other envelopes from run folder (logs, etc.) before cleanup - if (run_folder && options && options->transport && options->run) { - SENTRY_DEBUG("Checking for additional envelopes in run folder"); - sentry_pathiter_t *piter = sentry__path_iter_directory(run_folder); - if (piter) { - SENTRY_DEBUG("Iterating run folder for envelope files"); - const sentry_path_t *file_path; - int envelope_count = 0; - while ((file_path = sentry__pathiter_next(piter)) != NULL) { - // Check if this is an envelope file (ends with .envelope) - const char *path_str = file_path->path; - size_t len = strlen(path_str); - if (len > 9 && strcmp(path_str + len - 9, ".envelope") == 0) { - SENTRY_DEBUGF( - "Sending envelope from run folder: %s", path_str); - sentry_envelope_t *run_envelope - = sentry__envelope_from_path(file_path); - if (run_envelope) { - sentry__capture_envelope( - options->transport, run_envelope, options); - envelope_count++; - } else { - SENTRY_WARNF("Failed to load envelope: %s", path_str); - } - } - } - SENTRY_DEBUGF( - "Sent %d additional envelopes from run folder", envelope_count); - sentry__pathiter_free(piter); - } else { - SENTRY_DEBUG("Could not iterate run folder"); - } - } else { - SENTRY_DEBUG("No run folder or transport for additional envelopes"); - } - - // Clean up the entire run folder (contains breadcrumbs, etc.) - if (run_folder) { - SENTRY_DEBUG("Cleaning up run folder"); - sentry__path_remove_all(run_folder); - sentry__path_free(run_folder); - } + sentry__path_free(run_folder); sentry__path_free(ev_path); - // Release and clean up the lock file - if (run_lock) { - sentry__filelock_unlock(run_lock); - sentry__filelock_free(run_lock); - } - SENTRY_DEBUG("Cleaned up crash run folder and lock file"); - SENTRY_DEBUG("Crash processing completed successfully"); done: @@ -4394,6 +4327,20 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) return crash_captured; } +static void +remove_pending_run_envelopes(const sentry_path_t *run_path) +{ + sentry_pathiter_t *it = sentry__path_iter_directory(run_path); + const sentry_path_t *file; + while (it && (file = sentry__pathiter_next(it)) != NULL) { + if (sentry__path_is_file(file) && !sentry__path_is_symlink(file) + && sentry__path_ends_with(file, ".envelope")) { + sentry__path_remove(file); + } + } + sentry__pathiter_free(it); +} + /** * Check if parent process is still alive */ @@ -4472,17 +4419,14 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, } // Set up logging to file for daemon BEFORE redirecting streams - // Use same naming scheme as shared memory (PID ^ TID hash) to handle - // multiple threads in same process char log_path[SENTRY_CRASH_MAX_PATH]; FILE *log_file = NULL; - uint32_t id = (uint32_t)((app_pid ^ (app_tid & 0xFFFFFFFF)) & 0xFFFFFFFF); #if defined(SENTRY_PLATFORM_WINDOWS) // On Windows, convert UTF-8 path to wide characters for proper file // handling int log_path_len = snprintf(log_path, sizeof(log_path), - "%s\\sentry-daemon-%08x.log", ipc->shmem->database_path, id); + "%s\\sentry-daemon.log", ipc->shmem->run_path); if (log_path_len > 0 && log_path_len < (int)sizeof(log_path)) { wchar_t *wlog_path = sentry__string_to_wstr(log_path); @@ -4493,7 +4437,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, } #else int log_path_len = snprintf(log_path, sizeof(log_path), - "%s/sentry-daemon-%08x.log", ipc->shmem->database_path, id); + "%s/sentry-daemon.log", ipc->shmem->run_path); if (log_path_len > 0 && log_path_len < (int)sizeof(log_path)) { log_file = fopen(log_path, "w"); @@ -4556,6 +4500,8 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, return 1; } + sentry_options_set_database_path(options, ipc->shmem->database_path); + // Use debug logging and screenshot settings from parent process sentry_options_set_debug(options, ipc->shmem->debug_enabled); options->attach_screenshot = ipc->shmem->attach_screenshot; @@ -4597,17 +4543,18 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, options->user_agent = sentry__string_clone(ipc->shmem->user_agent); } - // Create run with database path - SENTRY_DEBUG("Creating run with database path"); - sentry_path_t *db_path = sentry__path_from_str(ipc->shmem->database_path); - if (db_path) { - options->run = sentry__run_new(db_path); + // Adopt existing run + SENTRY_DEBUG("Adopting existing run"); + sentry_path_t *run_path = sentry__path_from_str(ipc->shmem->run_path); + if (options->database_path && run_path) { + options->run + = sentry__run_new_for_daemon(options->database_path, run_path); if (options->run) { options->run->require_user_consent = ipc->shmem->require_user_consent; } - sentry__path_free(db_path); } + sentry__path_free(run_path); // Set external crash reporter if configured if (ipc->shmem->external_reporter_path[0] != '\0') { @@ -4723,13 +4670,15 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, if (rv != 0) { SENTRY_WARN("transport did not shut down cleanly"); } - dumped_envelopes = sentry__transport_dump_queue( - options->transport, options->run); - if (rv == 0 && !dumped_envelopes && options->run) { - sentry__run_clean(options->run, true); + + if (crash_processed) { + dumped_envelopes = sentry__transport_dump_queue( + options->transport, options->run); + if (rv == 0 && !dumped_envelopes && options->run) { + remove_pending_run_envelopes(options->run->run_path); + } } } - sentry_options_free(options); } if (crash_processed) { // Mark as done @@ -4743,9 +4692,14 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, // Close log file if (log_file) { + sentry__logger_disable(); fclose(log_file); } + if (options) { + sentry_options_free(options); + } + return 0; } diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 77fd119751..a2d7dc13df 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -237,26 +237,26 @@ safe_uint_to_str(char *buf, size_t buf_size, unsigned int value) } /** - * Build stack path signal-safely: "{database_path}/__sentry-stack{index}" + * Build stack path signal-safely: "{run_path}/__sentry-stack{index}" * Returns total length or 0 on error/truncation */ static size_t safe_build_stack_path( - char *dest, size_t dest_size, const char *database_path, unsigned int index) + char *dest, size_t dest_size, const char *run_path, unsigned int index) { if (!dest || dest_size == 0) { return 0; } - // Copy database path + // Copy run path size_t pos = 0; - size_t db_len = safe_strlen(database_path); - if (db_len >= dest_size) { + size_t run_len = safe_strlen(run_path); + if (run_len >= dest_size) { dest[0] = '\0'; return 0; // Would truncate } - safe_strncpy(dest, database_path, dest_size); - pos = db_len; + safe_strncpy(dest, run_path, dest_size); + pos = run_len; // Append "/__sentry-stack" const char *suffix = "/__sentry-stack"; @@ -568,11 +568,11 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) } if (actual_stack_size > 0) { - // Create stack file path in database directory + // Create stack file path in run directory // (signal-safe) char stack_path[SENTRY_CRASH_MAX_PATH]; size_t len = safe_build_stack_path( - stack_path, sizeof(stack_path), ctx->database_path, i); + stack_path, sizeof(stack_path), ctx->run_path, i); // Check for failure/truncation if (len == 0) { @@ -700,9 +700,9 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) // File format: module[0] header (4096 bytes) || module[1] header || ... { char hdr_path[SENTRY_CRASH_MAX_PATH]; - size_t pos = safe_strlen(ctx->database_path); + size_t pos = safe_strlen(ctx->run_path); if (pos + 22 < sizeof(hdr_path)) { // "/__sentry-modheaders\0" - safe_strncpy(hdr_path, ctx->database_path, sizeof(hdr_path)); + safe_strncpy(hdr_path, ctx->run_path, sizeof(hdr_path)); const char *suffix = "/__sentry-modheaders"; for (size_t si = 0; suffix[si] != '\0'; si++) { hdr_path[pos++] = suffix[si]; @@ -785,74 +785,41 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) // Dump daemon log for debugging (uses stdio, safe after page allocator // enabled) - // Extract the shm identifier for log path construction - // macOS: shm_path = "{tmpdir}/.sentry-shm-{id}", Linux: shm_name = - // "/s-{id}" -# if defined(SENTRY_PLATFORM_MACOS) - const char *shm_id_src = ipc ? ipc->shm_path : ""; -# else - const char *shm_id_src = ipc ? ipc->shm_name : ""; -# endif - if (shm_id_src[0] != '\0' && ctx && ctx->database_path[0] != '\0') { - // Extract hex ID after last '-' in shm name/path - const char *shm_id = NULL; - for (const char *p = shm_id_src; *p; p++) { - if (*p == '-') { - shm_id = p + 1; - } + if (ctx && ctx->run_path[0] != '\0') { + char log_path[SENTRY_CRASH_MAX_PATH]; + const char suffix[] = "/sentry-daemon.log"; + int len = 0; + // Manually build path string (signal-safe) + for (const char *p = ctx->run_path; + *p && len < (int)(sizeof(log_path) - sizeof(suffix)); p++) { + log_path[len++] = *p; } - - if (shm_id) { - char log_path[SENTRY_CRASH_MAX_PATH]; - int len = 0; - // Manually build path string (signal-safe) - for (const char *p = ctx->database_path; - *p && len < (int)sizeof(log_path) - 30; p++) { - log_path[len++] = *p; - } - const char *suffix = "/sentry-daemon-"; - for (const char *p = suffix; *p && len < (int)sizeof(log_path) - 15; - p++) { - log_path[len++] = *p; - } - for (const char *p = shm_id; *p && len < (int)sizeof(log_path) - 5; - p++) { - log_path[len++] = *p; - } - const char *ext = ".log"; - for (const char *p = ext; *p && len < (int)sizeof(log_path) - 1; - p++) { - log_path[len++] = *p; - } - log_path[len] = '\0'; - - // Try to open and dump log file - int fd = open(log_path, O_RDONLY); - if (fd >= 0) { - // Use sizeof()-1 for string literals (signal-safe) - ssize_t rv = write(STDERR_FILENO, "\n========== Daemon Log (", - sizeof("\n========== Daemon Log (") - 1); - (void)rv; // Ignore write errors in signal handler - rv = write(STDERR_FILENO, shm_id, safe_strlen(shm_id)); - (void)rv; - rv = write(STDERR_FILENO, ") ==========\n", - sizeof(") ==========\n") - 1); - (void)rv; - - char buf[1024]; - ssize_t n; - while ((n = read(fd, buf, sizeof(buf))) > 0) { - rv = write(STDERR_FILENO, buf, n); - (void)rv; - } - - rv = write(STDERR_FILENO, - "=========================================\n\n", - sizeof("=========================================\n\n") - - 1); + for (const char *p = suffix; *p && len < (int)sizeof(log_path) - 1; + p++) { + log_path[len++] = *p; + } + log_path[len] = '\0'; + + // Try to open and dump log file + int fd = open(log_path, O_RDONLY); + if (fd >= 0) { + // Use sizeof()-1 for string literals (signal-safe) + ssize_t rv + = write(STDERR_FILENO, "\n========== Daemon Log ==========\n", + sizeof("\n========== Daemon Log ==========\n") - 1); + (void)rv; // Ignore write errors in signal handler + + char buf[1024]; + ssize_t n; + while ((n = read(fd, buf, sizeof(buf))) > 0) { + rv = write(STDERR_FILENO, buf, n); (void)rv; - close(fd); } + + rv = write(STDERR_FILENO, "================================\n\n", + sizeof("================================\n\n") - 1); + (void)rv; + close(fd); } } diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index ffc14c2182..20f7091b9c 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -321,6 +321,13 @@ native_backend_startup( sentry_path_t *run_path = options->run->run_path; sentry_path_t *db_path = options->database_path; +#ifdef _WIN32 + strncpy_s(ctx->run_path, sizeof(ctx->run_path), run_path->path, _TRUNCATE); +#else + strncpy(ctx->run_path, run_path->path, sizeof(ctx->run_path) - 1); + ctx->run_path[sizeof(ctx->run_path) - 1] = '\0'; +#endif + // Store database path for daemon use if (db_path) { #ifdef _WIN32 @@ -585,12 +592,17 @@ native_backend_shutdown(sentry_backend_t *backend) // handler on iOS) sentry__crash_handler_shutdown(); + bool daemon_stopped = false; #if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_IOS) // Terminate daemon (Unix) if (state->daemon_pid > 0) { kill(state->daemon_pid, SIGTERM); // Wait for daemon to exit - waitpid(state->daemon_pid, NULL, 0); + pid_t wait_result; + do { + wait_result = waitpid(state->daemon_pid, NULL, 0); + } while (wait_result < 0 && errno == EINTR); + daemon_stopped = wait_result == state->daemon_pid; } #elif defined(SENTRY_PLATFORM_WINDOWS) // Terminate daemon (Windows) @@ -600,12 +612,27 @@ native_backend_shutdown(sentry_backend_t *backend) if (hDaemon) { TerminateProcess(hDaemon, 0); // Wait for daemon to exit (with timeout) - WaitForSingleObject(hDaemon, 5000); // 5 second timeout + daemon_stopped + = WaitForSingleObject(hDaemon, 5000) == WAIT_OBJECT_0; CloseHandle(hDaemon); } } #endif + if (daemon_stopped && state->ipc && state->ipc->shmem + && state->ipc->shmem->run_path[0]) { + sentry_path_t *run_path + = sentry__path_from_str(state->ipc->shmem->run_path); + sentry_path_t *lock_path = run_path + ? sentry__path_append_str(run_path, ".daemon.lock") + : NULL; + if (lock_path) { + sentry__path_remove(lock_path); + } + sentry__path_free(lock_path); + sentry__path_free(run_path); + } + // Dump daemon log file for debugging (especially useful in CI). // This bypasses the SDK logger and writes straight to stderr, so it must // only run when debug logging was enabled. When debug is off the daemon @@ -614,69 +641,32 @@ native_backend_shutdown(sentry_backend_t *backend) if (state->ipc && state->ipc->shmem && state->ipc->shmem->debug_enabled) { char log_path[SENTRY_CRASH_MAX_PATH]; int log_path_len = -1; + FILE *log_file = NULL; - // Extract the unique ID from the shm name/path to find the daemon log - // Platform-specific: shm_name on Linux/Windows, shm_path on macOS #if defined(SENTRY_PLATFORM_WINDOWS) - const wchar_t *shm_id_w = wcsrchr(state->ipc->shm_name, L'-'); - if (shm_id_w) { - shm_id_w++; // Skip the '-' - char *shm_id = sentry__string_from_wstr(shm_id_w); - if (shm_id) { - log_path_len = _snprintf(log_path, sizeof(log_path), - "%s\\sentry-daemon-%s.log", - state->ipc->shmem->database_path, shm_id); - if (log_path_len > 0 && log_path_len < (int)sizeof(log_path)) { - wchar_t *wpath = sentry__string_to_wstr(log_path); - FILE *log_file = wpath ? _wfopen(wpath, L"r") : NULL; - sentry_free(wpath); - if (log_file) { - fprintf(stderr, - "\n========== Daemon Log (%s) ==========\n", - shm_id); - char line[1024]; - while (fgets(line, sizeof(line), log_file)) { - fprintf(stderr, "%s", line); - } - fprintf(stderr, - "=========================================\n\n"); - fclose(log_file); - } - } - sentry_free(shm_id); - } + log_path_len = _snprintf(log_path, sizeof(log_path), + "%s\\sentry-daemon.log", state->ipc->shmem->run_path); + if (log_path_len > 0 && log_path_len < (int)sizeof(log_path)) { + wchar_t *wpath = sentry__string_to_wstr(log_path); + log_file = wpath ? _wfopen(wpath, L"r") : NULL; + sentry_free(wpath); } #else - // On macOS: shm_path = "{tmpdir}/.sentry-shm-{id}" - // On Linux: shm_name = "/s-{id}" - // In both cases, the ID follows the last '-' -# if defined(SENTRY_PLATFORM_MACOS) - const char *shm_id_src = state->ipc->shm_path; -# else - const char *shm_id_src = state->ipc->shm_name; -# endif - const char *shm_id = shm_id_src[0] ? strrchr(shm_id_src, '-') : NULL; - if (shm_id) { - shm_id++; // Skip the '-' - log_path_len = snprintf(log_path, sizeof(log_path), - "%s/sentry-daemon-%s.log", state->ipc->shmem->database_path, - shm_id); - if (log_path_len > 0 && log_path_len < (int)sizeof(log_path)) { - FILE *log_file = fopen(log_path, "r"); - if (log_file) { - fprintf(stderr, "\n========== Daemon Log (%s) ==========\n", - shm_id); - char line[1024]; - while (fgets(line, sizeof(line), log_file)) { - fprintf(stderr, "%s", line); - } - fprintf(stderr, - "=========================================\n\n"); - fclose(log_file); - } - } + log_path_len = snprintf(log_path, sizeof(log_path), + "%s/sentry-daemon.log", state->ipc->shmem->run_path); + if (log_path_len > 0 && log_path_len < (int)sizeof(log_path)) { + log_file = fopen(log_path, "r"); } #endif + if (log_file) { + fprintf(stderr, "\n========== Daemon Log ==========\n"); + char line[1024]; + while (fgets(line, sizeof(line), log_file)) { + fprintf(stderr, "%s", line); + } + fprintf(stderr, "================================\n\n"); + fclose(log_file); + } } // Cleanup IPC diff --git a/src/sentry_database.c b/src/sentry_database.c index 55ef63d4d5..3949d8f897 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -16,25 +16,13 @@ #include #include -sentry_run_t * -sentry__run_new(const sentry_path_t *database_path) +static sentry_run_t * +run_new_with_paths(const sentry_path_t *database_path, sentry_path_t *run_path, + sentry_path_t *lock_path) { - sentry_uuid_t uuid = sentry_uuid_new_v4(); - char run_name[46]; - sentry_uuid_as_string(&uuid, run_name); - - // `/.run` - strcpy(&run_name[36], ".run"); - sentry_path_t *run_path = sentry__path_join_str(database_path, run_name); - if (!run_path) { - return NULL; - } - - // `/.run.lock` - strcpy(&run_name[40], ".lock"); - sentry_path_t *lock_path = sentry__path_join_str(database_path, run_name); - if (!lock_path) { + if (!database_path || !run_path || !lock_path) { sentry__path_free(run_path); + sentry__path_free(lock_path); return NULL; } @@ -80,7 +68,6 @@ sentry__run_new(const sentry_path_t *database_path) run->refcount = 1; run->require_user_consent = 0; run->user_consent = SENTRY_USER_CONSENT_UNKNOWN; - run->uuid = uuid; run->run_path = run_path; run->session_path = session_path; run->external_path = external_path; @@ -102,6 +89,41 @@ sentry__run_new(const sentry_path_t *database_path) return NULL; } +sentry_run_t * +sentry__run_new(const sentry_path_t *database_path) +{ + sentry_uuid_t uuid = sentry_uuid_new_v4(); + char run_name[46]; + sentry_uuid_as_string(&uuid, run_name); + + // `/.run` + strcpy(&run_name[36], ".run"); + sentry_path_t *run_path = sentry__path_join_str(database_path, run_name); + + // `/.run.lock` + strcpy(&run_name[40], ".lock"); + sentry_path_t *lock_path = sentry__path_join_str(database_path, run_name); + + sentry_run_t *run = run_new_with_paths(database_path, run_path, lock_path); + if (run) { + run->uuid = uuid; + } + return run; +} + +sentry_run_t * +sentry__run_new_for_daemon( + const sentry_path_t *database_path, const sentry_path_t *run_path) +{ + if (!database_path || !run_path) { + return NULL; + } + sentry_path_t *owned_run_path = sentry__path_clone(run_path); + sentry_path_t *lock_path + = sentry__path_append_str(run_path, ".daemon.lock"); + return run_new_with_paths(database_path, owned_run_path, lock_path); +} + bool sentry__run_should_skip_upload(sentry_run_t *run) { @@ -215,7 +237,9 @@ sentry__run_free(sentry_run_t *run) sentry__path_free(run->session_path); sentry__path_free(run->external_path); sentry__path_free(run->cache_path); - sentry__filelock_free(run->lock); + if (run->lock) { + sentry__filelock_free(run->lock); + } sentry_free(run->installation_id); sentry_free(run); } @@ -594,6 +618,26 @@ sentry__run_clear_session(const sentry_run_t *run) return !rv; } +void +sentry__process_run_envelopes( + const sentry_options_t *options, const sentry_path_t *run_path) +{ + sentry_pathiter_t *it = sentry__path_iter_directory(run_path); + const sentry_path_t *file; + while (it && (file = sentry__pathiter_next(it)) != NULL) { + if (!sentry__path_is_file(file) || sentry__path_is_symlink(file) + || !sentry__path_ends_with(file, ".envelope")) { + continue; + } + sentry_envelope_t *envelope = sentry__envelope_from_path(file); + if (envelope) { + sentry__capture_envelope(options->transport, envelope, options); + } + sentry__path_remove(file); + } + sentry__pathiter_free(it); +} + void sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) { @@ -652,6 +696,20 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) continue; } + sentry_path_t *daemon_lockfile + = sentry__path_append_str(run_dir, ".daemon.lock"); + sentry_filelock_t *daemon_lock + = daemon_lockfile ? sentry__filelock_new(daemon_lockfile) : NULL; + if (!daemon_lock || !sentry__filelock_try_lock(daemon_lock)) { + if (daemon_lock) { + sentry__filelock_free(daemon_lock); + } + sentry__filelock_free(lock); + continue; + } + + sentry__process_run_envelopes(options, run_dir); + sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { @@ -693,12 +751,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) session_num = 0; } } - } else if (sentry__path_ends_with(file, ".envelope")) { - sentry_envelope_t *envelope = sentry__envelope_from_path(file); - if (envelope) { - sentry__capture_envelope( - options->transport, envelope, options); - } } sentry__path_remove(file); @@ -706,6 +758,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__pathiter_free(run_iter); sentry__path_remove_all(run_dir); + sentry__filelock_free(daemon_lock); sentry__filelock_free(lock); } sentry__pathiter_free(db_iter); diff --git a/src/sentry_database.h b/src/sentry_database.h index d1c94ece61..cf5f103b75 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -47,13 +47,21 @@ void sentry__run_load_installation_id(sentry_run_t *run, const sentry_path_t *database_path, const char *public_key); /** - * This creates a new application run including its associated directory and + * This creates a new run including its associated directory and * lockfile: * * `/.run/` * * `/.run.lock` */ sentry_run_t *sentry__run_new(const sentry_path_t *database_path); +/** + * This creates a run object for a crash daemon that adopts an existing run + * directory. The daemon holds a separate lock so old-run processing waits for + * both the process and its daemon to finish. + */ +sentry_run_t *sentry__run_new_for_daemon( + const sentry_path_t *database_path, const sentry_path_t *run_path); + /** * Increment the refcount and return the run pointer. */ @@ -160,6 +168,12 @@ sentry_path_t *sentry__run_make_cache_path( void sentry__process_old_runs( const sentry_options_t *options, uint64_t last_crash); +/** + * Captures and removes all envelope files from a run directory. + */ +void sentry__process_run_envelopes( + const sentry_options_t *options, const sentry_path_t *run_path); + /** * Parses a cache filename in either form: * - `.envelope` sets `*ts_out = 0`, `*count_out = -1`. diff --git a/tests/assertions.py b/tests/assertions.py index 5520bdc359..b5cce57cbc 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -710,7 +710,7 @@ def wait_for_daemon(tmp_path, started_at, timeout=None): deadline = time.time() + timeout while time.time() < deadline: - for log_path in db_dir.glob("sentry-daemon-*.log"): + for log_path in db_dir.glob("*.run/sentry-daemon.log"): try: if log_path.stat().st_mtime < started_at: continue diff --git a/tests/test_e2e_sentry.py b/tests/test_e2e_sentry.py index 4a3877b4a7..785d9f7580 100644 --- a/tests/test_e2e_sentry.py +++ b/tests/test_e2e_sentry.py @@ -405,7 +405,7 @@ def print_daemon_logs(self): return # Find daemon log files - log_files = list(db_path.glob("sentry-daemon-*.log")) + log_files = list(db_path.glob("*.run/sentry-daemon.log")) if not log_files: print(f"\n=== No daemon log files found in {db_path} ===") return diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 3e631b26f5..4cae17405e 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -188,12 +188,17 @@ def test_native_capture_minidump_generated(cmake, httpserver): ) assert waiting.result - # Check for minidump file in database directory + # Check for minidump file in run directory db_dir = tmp_path / ".sentry-native" assert db_dir.exists() - assert wait_for_file(db_dir / "*.dmp"), "Minidump file should be generated" - minidump_files = list(db_dir.glob("*.dmp")) + assert wait_for_file( + db_dir / "*.run/__sentry-crash.dmp" + ), "Minidump file should be generated" + minidump_files = list(db_dir.glob("*.run/__sentry-crash.dmp")) + assert not list(db_dir.glob("*.dmp")) + assert not list(db_dir.glob("sentry-daemon.log")) + assert not list(db_dir.glob("sentry-envelope-*")) # Verify minidump has correct header minidump_path = minidump_files[0] @@ -213,6 +218,16 @@ def test_native_capture_minidump_generated(cmake, httpserver): # Just verify it's non-zero assert version != 0, "Minidump should have non-zero version" + # The next SDK launch owns cleanup of the completed crash run. + run( + tmp_path, + "sentry_example", + ["log", "no-setup"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + assert not list(db_dir.glob("*.run")) + assert not list(db_dir.glob("*.run*.lock")) + # Both daemon envelope writers merge the breadcrumb ring files: the native # stacktrace writer builds the event from scratch, while the minidump-only @@ -690,8 +705,8 @@ def test_native_minidump_streams(cmake, httpserver): # Find minidump db_dir = tmp_path / ".sentry-native" - assert wait_for_file(db_dir / "*.dmp") - minidump_files = list(db_dir.glob("*.dmp")) + assert wait_for_file(db_dir / "*.run/__sentry-crash.dmp") + minidump_files = list(db_dir.glob("*.run/__sentry-crash.dmp")) assert len(minidump_files) > 0 dump = _parse_minidump(minidump_files[0]) @@ -897,8 +912,8 @@ def test_native_smart_mode_captures_indirect_heap_memory(cmake, httpserver): assert waiting.result db_dir = tmp_path / ".sentry-native" - assert wait_for_file(db_dir / "*.dmp") - minidump_files = list(db_dir.glob("*.dmp")) + assert wait_for_file(db_dir / "*.run/__sentry-crash.dmp") + minidump_files = list(db_dir.glob("*.run/__sentry-crash.dmp")) assert len(minidump_files) > 0 dump = _parse_minidump(minidump_files[0]) @@ -946,6 +961,42 @@ def in_any(addr, ranges): ) +def test_native_uses_existing_run(cmake): + """The daemon adopts the existing run instead of creating root artifacts.""" + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) + exe = tmp_path / ( + "sentry_example.exe" if sys.platform == "win32" else "sentry_example" + ) + child = subprocess.Popen( + [str(exe), "log", "sleep"], + cwd=tmp_path, + env=dict(os.environ, SENTRY_DSN="https://foo@sentry.invalid/42"), + ) + db_dir = tmp_path / ".sentry-native" + + try: + assert wait_for( + lambda: len(list(db_dir.glob("*.run/sentry-daemon.log"))) == 1 + and len(list(db_dir.glob("*.run.daemon.lock"))) == 1 + ) + run_dirs = list(db_dir.glob("*.run")) + daemon_logs = list(db_dir.glob("*.run/sentry-daemon.log")) + + assert len(run_dirs) == 1 + assert daemon_logs[0].parent == run_dirs[0] + assert len(list(db_dir.glob("*.run.daemon.lock"))) == 1 + assert not list(db_dir.glob("sentry-daemon.log")) + assert not list(db_dir.glob("sentry-minidump-*")) + assert not list(db_dir.glob("sentry-envelope-*")) + assert not list(db_dir.glob("__sentry-stack*")) + assert not list(db_dir.glob("__sentry-modheaders")) + finally: + child.terminate() + child.wait() + # Windows cannot remove the run until the orphaned daemon releases it. + assert wait_for(lambda: not list(db_dir.glob("*.run.daemon.lock"))) + + def test_native_cleanup(cmake): """Test that cleanup works properly""" tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) @@ -960,6 +1011,10 @@ def test_native_cleanup(cmake): # Database should exist db_dir = tmp_path / ".sentry-native" assert db_dir.exists() + assert not list(db_dir.glob("*.run")) + assert not list(db_dir.glob("*.run*.lock")) + assert not list(db_dir.glob("sentry-daemon.log")) + assert not list(db_dir.glob("sentry-minidump-*")) def test_native_no_dsn_no_crash(cmake): @@ -977,7 +1032,7 @@ def test_native_no_dsn_no_crash(cmake): # Should not create database db_dir = tmp_path / ".sentry-native" if db_dir.exists(): - minidump_files = list(db_dir.glob("*.dmp")) + minidump_files = list(db_dir.glob("*.run/*.dmp")) # Minidumps might still be generated for debugging # but won't be uploaded diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index 2673cf09f2..a0480fba8a 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -578,4 +578,6 @@ def test_tus_crash_native(cmake, httpserver): for d in os.listdir(db_dir) if d.endswith(".run") and os.path.isdir(os.path.join(db_dir, d)) ] - assert run_dirs == [] + assert len(run_dirs) == 1 + run_dir = os.path.join(db_dir, run_dirs[0]) + assert not [f for f in os.listdir(run_dir) if f.endswith(".envelope")] diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 0bf86275aa..194b2d7be7 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -5,7 +5,9 @@ * and low-level crash handling functionality. */ +#include "sentry_database.h" #include "sentry_options.h" +#include "sentry_path.h" #include "sentry_testsupport.h" #include @@ -19,6 +21,96 @@ # include "sentry_elf.h" #endif +SENTRY_TEST(daemon_adopts_existing_run) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#endif + SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(sentry__path_remove_all(options->database_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(options->database_path) == 0); + + sentry_run_t *existing_run = sentry__run_new(options->database_path); + TEST_ASSERT(!!existing_run); + sentry_run_t *adopted_run = sentry__run_new_for_daemon( + options->database_path, existing_run->run_path); + TEST_ASSERT(!!adopted_run); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + sentry_path_t *external_path + = sentry__path_join_str(options->database_path, "external"); + sentry_path_t *daemon_lock_path + = sentry__path_append_str(existing_run->run_path, ".daemon.lock"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(!!external_path); + TEST_ASSERT(!!daemon_lock_path); + + TEST_CHECK(sentry__path_eq(adopted_run->run_path, existing_run->run_path)); + TEST_CHECK(sentry__path_eq(adopted_run->cache_path, cache_path)); + TEST_CHECK(sentry__path_eq(adopted_run->external_path, external_path)); + TEST_CHECK(sentry__path_is_file(daemon_lock_path)); + + size_t run_count = 0; + sentry_pathiter_t *it = sentry__path_iter_directory(options->database_path); + const sentry_path_t *entry; + while (it && (entry = sentry__pathiter_next(it)) != NULL) { + if (sentry__path_is_dir(entry) + && sentry__path_ends_with(entry, ".run")) { + run_count++; + } + } + sentry__pathiter_free(it); + TEST_CHECK_INT_EQUAL(run_count, 1); + + sentry__run_free(adopted_run); + TEST_CHECK(!sentry__path_is_file(daemon_lock_path)); + sentry__run_clean(existing_run, true); + sentry__run_free(existing_run); + sentry__path_free(daemon_lock_path); + sentry__path_free(external_path); + sentry__path_free(cache_path); + sentry_options_free(options); +} + +SENTRY_TEST(daemon_run_blocks_old_run_cleanup) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#endif + SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(sentry__path_remove_all(options->database_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(options->database_path) == 0); + + options->run = sentry__run_new(options->database_path); + TEST_ASSERT(!!options->run); + sentry_run_t *old_run = sentry__run_new(options->database_path); + TEST_ASSERT(!!old_run); + sentry__filelock_unlock(old_run->lock); + + sentry_run_t *daemon_run + = sentry__run_new_for_daemon(options->database_path, old_run->run_path); + TEST_ASSERT(!!daemon_run); + sentry_path_t *artifact + = sentry__path_join_str(old_run->run_path, "daemon.log"); + TEST_ASSERT(!!artifact); + TEST_ASSERT(sentry__path_write_buffer(artifact, "log", 3) == 0); + + sentry__process_old_runs(options, 0); + TEST_CHECK(sentry__path_is_dir(old_run->run_path)); + TEST_CHECK(sentry__path_is_file(artifact)); + + sentry__run_free(daemon_run); + sentry__process_old_runs(options, 0); + TEST_CHECK(!sentry__path_is_dir(old_run->run_path)); + TEST_CHECK(!sentry__path_is_file(artifact)); + + sentry__path_free(artifact); + sentry__run_free(old_run); + sentry__run_clean(options->run, true); + sentry_options_free(options); +} + /** * Test minidump header structure size and alignment */ diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 7168c30acd..5d3fa5b9b6 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -114,6 +114,8 @@ XX(crash_context_transport_fields) XX(crash_marker) XX(crashed_last_run) XX(custom_logger) +XX(daemon_adopts_existing_run) +XX(daemon_run_blocks_old_run_cleanup) XX(deserialize_envelope) XX(deserialize_envelope_empty) XX(deserialize_envelope_empty_attachments) From b18b2dddd500d3a38c031e1225ccc4caa6198538 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 21:23:30 +0200 Subject: [PATCH 02/13] ++ --- src/backends/native/sentry_crash_handler.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index a2d7dc13df..887aa97681 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -160,6 +160,8 @@ get_tid(void) # endif } +// safe_strxxx are only used on macOS (for stack path and module names) +# if defined(SENTRY_PLATFORM_MACOS) /** * Safe string length (signal-safe) */ @@ -173,8 +175,6 @@ safe_strlen(const char *s) return len; } -// safe_strncpy is only used on macOS (for stack path and module names) -# if defined(SENTRY_PLATFORM_MACOS) /** * Safe string copy (signal-safe) */ From 9ae0a0ff6e0ef78f39ec999dac00924cf19157f2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 10:21:18 +0200 Subject: [PATCH 03/13] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a275f801a5..dc51de917a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Crashpad: prevent external crash reporters from bypassing revoked user consent. ([#1972](https://github.com/getsentry/sentry-native/pull/1972), [crashpad#168](https://github.com/getsentry/crashpad/pull/168)) - Native/Linux: improve startup time by avoiding zeroing a large shmem module array during crash context initialization. ([#1966](https://github.com/getsentry/sentry-native/pull/1966)) +- Native: store daemon logs, minidumps, crash envelopes, and scratch files in `.run` directories so they are cleaned up with the run instead of accumulating in the database root. ([#1976](https://github.com/getsentry/sentry-native/pull/1976)) ## 0.16.2 From 3022c92d1794c34584a646514fadf71186fed2ff Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 11:09:48 +0200 Subject: [PATCH 04/13] check .daemon.lock existence --- src/sentry_database.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 3949d8f897..0ffc3c409a 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -698,15 +698,23 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_path_t *daemon_lockfile = sentry__path_append_str(run_dir, ".daemon.lock"); - sentry_filelock_t *daemon_lock - = daemon_lockfile ? sentry__filelock_new(daemon_lockfile) : NULL; - if (!daemon_lock || !sentry__filelock_try_lock(daemon_lock)) { - if (daemon_lock) { - sentry__filelock_free(daemon_lock); - } + sentry_filelock_t *daemon_lock = NULL; + if (!daemon_lockfile) { sentry__filelock_free(lock); continue; } + if (sentry__path_is_file(daemon_lockfile)) { + daemon_lock = sentry__filelock_new(daemon_lockfile); + if (!daemon_lock || !sentry__filelock_try_lock(daemon_lock)) { + if (daemon_lock) { + sentry__filelock_free(daemon_lock); + } + sentry__filelock_free(lock); + continue; + } + } else { + sentry__path_free(daemon_lockfile); + } sentry__process_run_envelopes(options, run_dir); @@ -758,7 +766,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__pathiter_free(run_iter); sentry__path_remove_all(run_dir); - sentry__filelock_free(daemon_lock); + if (daemon_lock) { + sentry__filelock_free(daemon_lock); + } sentry__filelock_free(lock); } sentry__pathiter_free(db_iter); From 8a5f2088cf9536849710a42d5cf47ad4839ee14c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 11:24:50 +0200 Subject: [PATCH 05/13] clean up --- src/backends/native/sentry_crash_daemon.c | 3 +-- src/sentry_database.c | 8 ++++---- src/sentry_database.h | 4 ++-- tests/unit/test_native_backend.c | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 714751715b..7522074174 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4547,8 +4547,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, SENTRY_DEBUG("Adopting existing run"); sentry_path_t *run_path = sentry__path_from_str(ipc->shmem->run_path); if (options->database_path && run_path) { - options->run - = sentry__run_new_for_daemon(options->database_path, run_path); + options->run = sentry__run_adopt(options->database_path, run_path); if (options->run) { options->run->require_user_consent = ipc->shmem->require_user_consent; diff --git a/src/sentry_database.c b/src/sentry_database.c index 0ffc3c409a..ea145de1f6 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -17,7 +17,7 @@ #include static sentry_run_t * -run_new_with_paths(const sentry_path_t *database_path, sentry_path_t *run_path, +run_new(const sentry_path_t *database_path, sentry_path_t *run_path, sentry_path_t *lock_path) { if (!database_path || !run_path || !lock_path) { @@ -104,7 +104,7 @@ sentry__run_new(const sentry_path_t *database_path) strcpy(&run_name[40], ".lock"); sentry_path_t *lock_path = sentry__path_join_str(database_path, run_name); - sentry_run_t *run = run_new_with_paths(database_path, run_path, lock_path); + sentry_run_t *run = run_new(database_path, run_path, lock_path); if (run) { run->uuid = uuid; } @@ -112,7 +112,7 @@ sentry__run_new(const sentry_path_t *database_path) } sentry_run_t * -sentry__run_new_for_daemon( +sentry__run_adopt( const sentry_path_t *database_path, const sentry_path_t *run_path) { if (!database_path || !run_path) { @@ -121,7 +121,7 @@ sentry__run_new_for_daemon( sentry_path_t *owned_run_path = sentry__path_clone(run_path); sentry_path_t *lock_path = sentry__path_append_str(run_path, ".daemon.lock"); - return run_new_with_paths(database_path, owned_run_path, lock_path); + return run_new(database_path, owned_run_path, lock_path); } bool diff --git a/src/sentry_database.h b/src/sentry_database.h index cf5f103b75..83d4e5e6e2 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -47,7 +47,7 @@ void sentry__run_load_installation_id(sentry_run_t *run, const sentry_path_t *database_path, const char *public_key); /** - * This creates a new run including its associated directory and + * This creates a new application run including its associated directory and * lockfile: * * `/.run/` * * `/.run.lock` @@ -59,7 +59,7 @@ sentry_run_t *sentry__run_new(const sentry_path_t *database_path); * directory. The daemon holds a separate lock so old-run processing waits for * both the process and its daemon to finish. */ -sentry_run_t *sentry__run_new_for_daemon( +sentry_run_t *sentry__run_adopt( const sentry_path_t *database_path, const sentry_path_t *run_path); /** diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 194b2d7be7..89ab18c54b 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -32,8 +32,8 @@ SENTRY_TEST(daemon_adopts_existing_run) sentry_run_t *existing_run = sentry__run_new(options->database_path); TEST_ASSERT(!!existing_run); - sentry_run_t *adopted_run = sentry__run_new_for_daemon( - options->database_path, existing_run->run_path); + sentry_run_t *adopted_run + = sentry__run_adopt(options->database_path, existing_run->run_path); TEST_ASSERT(!!adopted_run); sentry_path_t *cache_path @@ -89,7 +89,7 @@ SENTRY_TEST(daemon_run_blocks_old_run_cleanup) sentry__filelock_unlock(old_run->lock); sentry_run_t *daemon_run - = sentry__run_new_for_daemon(options->database_path, old_run->run_path); + = sentry__run_adopt(options->database_path, old_run->run_path); TEST_ASSERT(!!daemon_run); sentry_path_t *artifact = sentry__path_join_str(old_run->run_path, "daemon.log"); From 045a7c503dc46e6e6eb18627b9234c677f6daeb7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 12:56:41 +0200 Subject: [PATCH 06/13] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d73f68d93..da0414d601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Fixes**: + +- Native: store daemon logs, minidumps, crash envelopes, and scratch files in `.run` directories so they are cleaned up with the run instead of accumulating in the database root. ([#1976](https://github.com/getsentry/sentry-native/pull/1976)) + ## 0.16.3 **Features**: @@ -15,7 +21,6 @@ - Crashpad: avoid allocating while handling a crash. The backend built the attachment `base::FilePath` inside the crash handler; if the crash corrupted the heap (e.g. via an overridden `operator new`) that allocation faults again and the report is lost. The paths are now cached at startup. ([#1984](https://github.com/getsentry/sentry-native/pull/1984)) - Crashpad: prevent external crash reporters from bypassing revoked user consent. ([#1972](https://github.com/getsentry/sentry-native/pull/1972), [crashpad#168](https://github.com/getsentry/crashpad/pull/168)) - Native/Linux: improve startup time by avoiding zeroing a large shmem module array during crash context initialization. ([#1966](https://github.com/getsentry/sentry-native/pull/1966)) -- Native: store daemon logs, minidumps, crash envelopes, and scratch files in `.run` directories so they are cleaned up with the run instead of accumulating in the database root. ([#1976](https://github.com/getsentry/sentry-native/pull/1976)) ## 0.16.2 From 9de713a4bdf8fa0986b196c2d5065649bf337c0f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 12:59:25 +0200 Subject: [PATCH 07/13] clarify the changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da0414d601..5edd60a68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Fixes**: -- Native: store daemon logs, minidumps, crash envelopes, and scratch files in `.run` directories so they are cleaned up with the run instead of accumulating in the database root. ([#1976](https://github.com/getsentry/sentry-native/pull/1976)) +- Native: store daemon logs, minidumps, crash envelopes, and scratch files in `.run` directories so they are cleaned up with the run instead of accumulating in the database root. Minidumps can still be retained with `cache_keep`, which stores `.dmp` sidecars alongside cached envelopes. ([#1976](https://github.com/getsentry/sentry-native/pull/1976)) ## 0.16.3 From 7e4b603a22f9bf3bdd69a88c1cc15b8458bb2615 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 11 May 2026 18:14:18 +0200 Subject: [PATCH 08/13] feat: Add on_crashed_last_run callback Invoke the callback during startup for envelopes associated with crashes in previous runs across the in-process, Breakpad, Crashpad, and native backends. Retain the native daemon's dedicated crash envelope in the application run as callback-only state, separate from transport queue dumps, so successfully delivered crashes are not sent again. --- CHANGELOG.md | 4 + examples/example.c | 15 ++ include/sentry.h | 27 ++++ src/backends/native/sentry_crash_context.h | 1 + src/backends/native/sentry_crash_daemon.c | 21 +-- src/backends/sentry_backend_breakpad.cpp | 8 + src/backends/sentry_backend_crashpad.cpp | 76 ++++++++- src/backends/sentry_backend_inproc.c | 8 + src/backends/sentry_backend_native.c | 38 +++++ src/sentry_backend.h | 4 + src/sentry_database.c | 65 +++++++- src/sentry_database.h | 7 + src/sentry_options.c | 8 + src/sentry_options.h | 2 + tests/test_integration_crashpad.py | 40 +++++ tests/test_integration_http.py | 56 +++++++ tests/test_integration_native.py | 61 ++++++++ tests/unit/test_cache.c | 174 +++++++++++++++++++++ tests/unit/test_native_backend.c | 11 ++ tests/unit/tests.inc | 2 + 20 files changed, 614 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d802877fba..e3c31b01fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Features**: + +- Add `on_crashed_last_run` callback for inspecting crash envelopes from previous runs. ([#1977](https://github.com/getsentry/sentry-native/pull/1977)) + **Fixes**: - Native: store daemon logs, minidumps, crash envelopes, and scratch files in `.run` directories so they are cleaned up with the run instead of accumulating in the database root. Minidumps can still be retained with `cache_keep`, which stores `.dmp` sidecars alongside cached envelopes. ([#1976](https://github.com/getsentry/sentry-native/pull/1976)) diff --git a/examples/example.c b/examples/example.c index 005a8f36dd..382be4749b 100644 --- a/examples/example.c +++ b/examples/example.c @@ -172,6 +172,16 @@ on_crash_callback( return event; } +static void +on_crashed_last_run_callback(const sentry_envelope_t *envelope, void *user_data) +{ + (void)user_data; + const char *event_id = sentry_value_as_string( + sentry_envelope_get_header(envelope, "event_id")); + printf("CRASHED_LAST_RUN:%s\n", event_id ? event_id : ""); + fflush(stdout); +} + static sentry_value_t restart_on_crash( const sentry_ucontext_t *uctx, sentry_value_t event, void *user_data) @@ -771,6 +781,11 @@ main(int argc, char **argv) sentry_options_set_on_crash(options, on_crash_callback, NULL); } + if (has_arg(argc, argv, "on-crashed-last-run")) { + sentry_options_set_on_crashed_last_run( + options, on_crashed_last_run_callback, NULL); + } + if (has_arg(argc, argv, "discarding-on-crash")) { sentry_options_set_on_crash( options, discarding_on_crash_callback, NULL); diff --git a/include/sentry.h b/include/sentry.h index 271f2d0006..6f5719a890 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1257,6 +1257,33 @@ typedef sentry_value_t (*sentry_crash_function_t)( SENTRY_API void sentry_options_set_on_crash( sentry_options_t *opts, sentry_crash_function_t func, void *data); +/** + * Type of the `on_crashed_last_run` callback. + * + * The callback is invoked synchronously during `sentry_init` for every + * available envelope associated with a crash in a previous run. This can + * include multiple crashes in multi-process and early-startup crash scenarios. + * + * The callback does not take ownership of `envelope`. The envelope is only + * valid for the duration of the callback and must not be freed. Since + * `sentry_init` has not completed yet, the callback must not call SDK functions + * that require an initialized SDK. + * + * Unlike `on_crash`, this callback runs in a healthy process and does not need + * to be signal-safe. + */ +typedef void (*sentry_crashed_last_run_function_t)( + const sentry_envelope_t *envelope, void *user_data); + +/** + * Sets the `on_crashed_last_run` callback. + * + * The native backend requires this option to be configured in the crashed run + * so its out-of-process daemon retains the crash envelope for the next launch. + */ +SENTRY_API void sentry_options_set_on_crashed_last_run(sentry_options_t *opts, + sentry_crashed_last_run_function_t func, void *user_data); + /** * Sets the DSN. */ diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 5a51c06ad1..86b267b221 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -291,6 +291,7 @@ typedef struct { // ms int cache_keep; // sentry_cache_keep_t bool require_user_consent; + bool has_on_crashed_last_run; bool enable_large_attachments; bool http_retry; uint64_t shutdown_timeout; diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 7522074174..404ef0f6ee 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4275,17 +4275,18 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) crash_captured = true; } - // Clean up temporary envelope file (keep minidump for - // inspection/debugging) + // Keep the original crash envelope for the callback on the next launch. + if (!ctx->has_on_crashed_last_run) { #if defined(SENTRY_PLATFORM_UNIX) - unlink(envelope_path); + unlink(envelope_path); #elif defined(SENTRY_PLATFORM_WINDOWS) - wchar_t *wenvelope_unlink = sentry__string_to_wstr(envelope_path); - if (wenvelope_unlink) { - _wunlink(wenvelope_unlink); - sentry_free(wenvelope_unlink); - } + wchar_t *wenvelope_unlink = sentry__string_to_wstr(envelope_path); + if (wenvelope_unlink) { + _wunlink(wenvelope_unlink); + sentry_free(wenvelope_unlink); + } #endif + } cleanup: // Send the staged session-replay envelope same-session, enriched from the @@ -4334,7 +4335,9 @@ remove_pending_run_envelopes(const sentry_path_t *run_path) const sentry_path_t *file; while (it && (file = sentry__pathiter_next(it)) != NULL) { if (sentry__path_is_file(file) && !sentry__path_is_symlink(file) - && sentry__path_ends_with(file, ".envelope")) { + && sentry__path_ends_with(file, ".envelope") + && !sentry__path_filename_matches( + file, "__sentry-crash.envelope")) { sentry__path_remove(file); } } diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index cef8f75383..f2ea63f9ba 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -140,6 +140,7 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, sentry_value_t transaction = sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED); + sentry_uuid_t event_id = sentry_uuid_nil(); bool should_handle = true; @@ -184,6 +185,9 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, sentry_envelope_t *envelope = sentry__prepare_event( options, event, nullptr, !options->on_crash_func, nullptr); + if (envelope) { + event_id = sentry__envelope_get_event_id(envelope); + } sentry_session_t *session = sentry__end_current_session_with_status( SENTRY_SESSION_STATUS_CRASHED); sentry__envelope_add_session(envelope, session); @@ -274,6 +278,10 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, // after capturing the crash event, try to dump all the in-flight // data of the previous transports sentry__transport_dump_queue(options->transport, options->run); + if (!sentry_uuid_is_nil(&event_id) + && !sentry__run_write_crash_marker(options->run, &event_id)) { + SENTRY_SIGNAL_SAFE_LOG("WARN writing run crash marker failed"); + } // and restore the old transport } SENTRY_SIGNAL_SAFE_LOG("INFO crash has been captured"); diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 9f851a4b8a..c2e56b2d04 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -603,8 +603,8 @@ report_attachments_dir(const crashpad::CrashReportDatabase::Report &report, return attachments_dir; } -// Converts a completed crashpad report into a sentry envelope by reading the -// event, breadcrumbs, and attachments from the report's attachments directory. +// Converts a crashpad report into a sentry envelope by reading the event, +// breadcrumbs, and attachments from the report's attachments directory. static sentry_envelope_t * report_to_envelope(const crashpad::CrashReportDatabase::Report &report, const sentry_options_t *options) @@ -687,8 +687,75 @@ report_to_envelope(const crashpad::CrashReportDatabase::Report &report, return envelope; } +static bool +crashpad_backend_process_old_run(sentry_backend_t *backend, + const sentry_options_t *options, const sentry_path_t *run_path) +{ + if (!options->on_crashed_last_run_func) { + return true; + } + + auto *state = static_cast(backend->data); + if (!state || !state->db) { + return false; + } + + sentry_path_t *event_path + = sentry__path_join_str(run_path, "__sentry-event"); + if (!event_path) { + return false; + } + sentry_value_t event = read_msgpack_file(event_path); + sentry_uuid_t event_id + = sentry__value_as_uuid(sentry_value_get_by_key(event, "event_id")); + sentry_value_decref(event); + if (sentry_uuid_is_nil(&event_id)) { + sentry__path_free(event_path); + return true; + } + + char event_id_str[37]; + sentry_uuid_as_string(&event_id, event_id_str); + crashpad::UUID report_id; + if (!report_id.InitializeFromString(event_id_str)) { + sentry__path_free(event_path); + return true; + } + + crashpad::CrashReportDatabase::Report report; + crashpad::CrashReportDatabase::OperationStatus status + = state->db->LookUpCrashReport(report_id, &report); + if (status == crashpad::CrashReportDatabase::kReportNotFound) { + sentry__path_free(event_path); + return true; + } + if (status != crashpad::CrashReportDatabase::kNoError) { + sentry__path_free(event_path); + return false; + } + + sentry_envelope_t *envelope = report_to_envelope(report, options); + if (!envelope || !sentry__envelope_materialize(envelope)) { + sentry_envelope_free(envelope); + sentry__path_free(event_path); + return false; + } + + // remove before invoking to prevent repeated callbacks + bool removed = sentry__path_remove(event_path) == 0; + sentry__path_free(event_path); + if (!removed) { + sentry_envelope_free(envelope); + return false; + } + options->on_crashed_last_run_func( + envelope, options->on_crashed_last_run_data); + sentry_envelope_free(envelope); + return true; +} + // Caches completed crashpad reports as sentry envelopes and removes them from -// the crashpad database. Called during startup before the handler is started. +// the crashpad database. static void process_completed_reports( crashpad_state_t *state, const sentry_options_t *options) @@ -876,7 +943,6 @@ crashpad_backend_startup( // Initialize database first, flushing the consent later on as part of // `sentry_init` will persist the upload flag. data->db = crashpad::CrashReportDatabase::Initialize(database).release(); - process_completed_reports(data, options); data->client = new (std::nothrow) crashpad::CrashpadClient; char *minidump_url = sentry__dsn_get_minidump_url(options->dsn, options->user_agent); @@ -1119,6 +1185,7 @@ crashpad_backend_prune_database(sentry_backend_t *backend) // When offline caching is enabled, the user has full control over these // parameters via the cache_max_* options. SENTRY_WITH_OPTIONS (options) { + process_completed_reports(data, options); if (options->cache_keep) { max_age = options->cache_max_age; max_size = options->cache_max_size; @@ -1223,6 +1290,7 @@ sentry__backend_new(void) backend->add_breadcrumb_func = crashpad_backend_add_breadcrumb; backend->user_consent_changed_func = crashpad_backend_user_consent_changed; backend->get_last_crash_func = crashpad_backend_last_crash; + backend->process_old_run_func = crashpad_backend_process_old_run; backend->prune_database_func = crashpad_backend_prune_database; #if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ || defined(SENTRY_PLATFORM_MACOS) diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 86854ef6dc..f997c773c9 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1075,6 +1075,7 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, sentry_value_t transaction = sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED); + sentry_uuid_t event_id = sentry_uuid_nil(); if (options->on_crash_func && !skip_hooks) { SENTRY_DEBUG("invoking `on_crash` hook"); @@ -1102,6 +1103,9 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, sentry_envelope_t *envelope = sentry__prepare_event(options, event, NULL, !options->on_crash_func && !skip_hooks, NULL); + if (envelope) { + event_id = sentry__envelope_get_event_id(envelope); + } sentry_session_t *session = sentry__end_current_session_with_status( SENTRY_SESSION_STATUS_CRASHED); sentry__envelope_add_session(envelope, session); @@ -1156,6 +1160,10 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, // after capturing the crash event, dump all the envelopes to disk sentry__transport_dump_queue(options->transport, options->run); + if (!sentry_uuid_is_nil(&event_id) + && !sentry__run_write_crash_marker(options->run, &event_id)) { + SENTRY_SIGNAL_SAFE_LOG("WARN writing run crash marker failed"); + } // Use signal-safe logging here since this may run in signal handler // context (fallback path) where stdio functions are not safe. diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 20f7091b9c..278b403336 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -181,6 +181,42 @@ typedef struct { volatile long crashed; } native_backend_state_t; +static bool +native_backend_process_old_run(sentry_backend_t *backend, + const sentry_options_t *options, const sentry_path_t *run_path) +{ + (void)backend; + + sentry_pathiter_t *it = sentry__path_iter_directory(run_path); + const sentry_path_t *file; + while (it && (file = sentry__pathiter_next(it)) != NULL) { + if (!sentry__path_is_file(file) || sentry__path_is_symlink(file) + || !sentry__path_filename_matches( + file, "__sentry-crash.envelope")) { + continue; + } + + sentry_envelope_t *envelope = options->on_crashed_last_run_func + ? sentry__envelope_from_path(file) + : NULL; + bool materialized = envelope && sentry__envelope_materialize(envelope); + // remove before invoking to prevent repeated callbacks + if ((options->on_crashed_last_run_func && !materialized) + || sentry__path_remove(file) != 0) { + sentry_envelope_free(envelope); + sentry__pathiter_free(it); + return false; + } + if (materialized) { + options->on_crashed_last_run_func( + envelope, options->on_crashed_last_run_data); + } + sentry_envelope_free(envelope); + } + sentry__pathiter_free(it); + return true; +} + static int native_backend_startup( sentry_backend_t *backend, const sentry_options_t *options) @@ -309,6 +345,7 @@ native_backend_startup( ctx->session_replay_duration = options->session_replay_duration; ctx->cache_keep = (int)options->cache_keep; ctx->require_user_consent = options->require_user_consent; + ctx->has_on_crashed_last_run = options->on_crashed_last_run_func != NULL; ctx->enable_large_attachments = options->enable_large_attachments; ctx->http_retry = options->http_retry; ctx->shutdown_timeout = options->shutdown_timeout; @@ -1131,6 +1168,7 @@ sentry__backend_new(void) backend->add_breadcrumb_func = native_backend_add_breadcrumb; backend->add_attachment_func = native_backend_add_attachment; backend->user_consent_changed_func = native_backend_user_consent_changed; + backend->process_old_run_func = native_backend_process_old_run; backend->can_capture_after_shutdown = false; return backend; diff --git a/src/sentry_backend.h b/src/sentry_backend.h index a55fc507a4..7687d8a280 100644 --- a/src/sentry_backend.h +++ b/src/sentry_backend.h @@ -3,6 +3,7 @@ #include "sentry_boot.h" +#include "sentry_path.h" #include "sentry_scope.h" /** @@ -24,6 +25,9 @@ struct sentry_backend_s { const sentry_options_t *options); void (*user_consent_changed_func)(sentry_backend_t *); uint64_t (*get_last_crash_func)(sentry_backend_t *); + // called with the run file lock held; false retains the run + bool (*process_old_run_func)(sentry_backend_t *, + const sentry_options_t *options, const sentry_path_t *run_path); void (*prune_database_func)(sentry_backend_t *); void (*add_attachment_func)(sentry_backend_t *, sentry_attachment_t *); void (*remove_attachment_func)(sentry_backend_t *, sentry_attachment_t *); diff --git a/src/sentry_database.c b/src/sentry_database.c index ea145de1f6..9b4527c39e 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -1,6 +1,7 @@ #include "sentry_database.h" #include "sentry_alloc.h" #include "sentry_attachment.h" +#include "sentry_backend.h" #include "sentry_client_report.h" #include "sentry_envelope.h" #include "sentry_json.h" @@ -618,6 +619,46 @@ sentry__run_clear_session(const sentry_run_t *run) return !rv; } +static sentry_path_t * +run_crash_marker_path( + const sentry_path_t *run_path, const sentry_uuid_t *event_id) +{ + if (!run_path || !event_id || sentry_uuid_is_nil(event_id)) { + return NULL; + } + char *filename = sentry__uuid_as_filename(event_id, ".crash"); + if (!filename) { + return NULL; + } + sentry_path_t *path = sentry__path_join_str(run_path, filename); + sentry_free(filename); + return path; +} + +bool +sentry__run_write_crash_marker( + const sentry_run_t *run, const sentry_uuid_t *event_id) +{ + if (!run || !event_id || sentry_uuid_is_nil(event_id)) { + return false; + } + + char *filename = sentry__uuid_as_filename(event_id, ".envelope"); + sentry_path_t *envelope_path + = filename ? sentry__path_join_str(run->run_path, filename) : NULL; + sentry_free(filename); + if (!envelope_path || !sentry__path_is_file(envelope_path)) { + sentry__path_free(envelope_path); + return false; + } + sentry__path_free(envelope_path); + + sentry_path_t *marker_path = run_crash_marker_path(run->run_path, event_id); + int rv = marker_path ? sentry__path_touch(marker_path) : 1; + sentry__path_free(marker_path); + return rv == 0; +} + void sentry__process_run_envelopes( const sentry_options_t *options, const sentry_path_t *run_path) @@ -631,6 +672,18 @@ sentry__process_run_envelopes( } sentry_envelope_t *envelope = sentry__envelope_from_path(file); if (envelope) { + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + sentry_path_t *marker = run_crash_marker_path(run_path, &event_id); + // remove before invoking to prevent repeated callbacks + if (marker && sentry__path_is_file(marker) + && sentry__path_remove(marker) == 0) { + if (options->on_crashed_last_run_func + && sentry__envelope_materialize(envelope)) { + options->on_crashed_last_run_func( + envelope, options->on_crashed_last_run_data); + } + } + sentry__path_free(marker); sentry__capture_envelope(options->transport, envelope, options); } sentry__path_remove(file); @@ -716,12 +769,22 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__path_free(daemon_lockfile); } + if (options->backend && options->backend->process_old_run_func + && !options->backend->process_old_run_func( + options->backend, options, run_dir)) { + sentry__filelock_free(daemon_lock); + sentry__filelock_free(lock); + continue; + } sentry__process_run_envelopes(options, run_dir); sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { - if (sentry__path_filename_matches(file, "session.json")) { + if (sentry__path_ends_with(file, ".crash")) { + // handled by sentry__process_run_envelopes above + continue; + } else if (sentry__path_filename_matches(file, "session.json")) { if (!session_envelope) { session_envelope = sentry__envelope_new(); } diff --git a/src/sentry_database.h b/src/sentry_database.h index 83d4e5e6e2..ccd85434db 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -174,6 +174,13 @@ void sentry__process_old_runs( void sentry__process_run_envelopes( const sentry_options_t *options, const sentry_path_t *run_path); +/** + * Writes `.crash` into `run` after verifying that the matching + * `.envelope` exists. + */ +bool sentry__run_write_crash_marker( + const sentry_run_t *run, const sentry_uuid_t *event_id); + /** * Parses a cache filename in either form: * - `.envelope` sets `*ts_out = 0`, `*count_out = -1`. diff --git a/src/sentry_options.c b/src/sentry_options.c index 26cd3e9d1a..e718c60596 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -231,6 +231,14 @@ sentry_options_set_on_crash( opts->on_crash_data = user_data; } +void +sentry_options_set_on_crashed_last_run(sentry_options_t *opts, + sentry_crashed_last_run_function_t func, void *user_data) +{ + opts->on_crashed_last_run_func = func; + opts->on_crashed_last_run_data = user_data; +} + void sentry_options_set_before_transaction( sentry_options_t *opts, sentry_transaction_function_t func, void *user_data) diff --git a/src/sentry_options.h b/src/sentry_options.h index 75d52220ad..02f1ddc81d 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -66,6 +66,8 @@ struct sentry_options_s { void *before_send_data; sentry_crash_function_t on_crash_func; void *on_crash_data; + sentry_crashed_last_run_function_t on_crashed_last_run_func; + void *on_crashed_last_run_data; sentry_transaction_function_t before_transaction_func; void *before_transaction_data; sentry_before_send_log_function_t before_send_log_func; diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index cb2b0aa69b..de3d2ebb2e 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -65,6 +65,46 @@ def test_crashpad_capture(cmake, httpserver): assert len(httpserver.log) == 2 +def test_crashpad_on_crashed_last_run(cmake): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) + args = ["log", "on-crashed-last-run"] + + run( + tmp_path, + "sentry_example", + ["log", "crash"], + expect_failure=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert not list((tmp_path / ".sentry-native").glob("*.run/*.crash")) + + restarted = run( + tmp_path, + "sentry_example", + [*args, "no-setup"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + callbacks = [ + line + for line in restarted.stdout.splitlines() + if line.startswith(b"CRASHED_LAST_RUN:") + ] + assert len(callbacks) == 1 + assert len(callbacks[0].partition(b":")[2]) == 36 + + restarted_again = run( + tmp_path, + "sentry_example", + [*args, "no-setup"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert b"CRASHED_LAST_RUN:" not in restarted_again.stdout + + def _setup_crashpad_proxy_test(cmake, httpserver, proxy): if proxy: proxy_process, port = start_mitmdump(proxy) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 18c2129b6b..8f778cfd55 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1048,6 +1048,62 @@ def test_native_crash_http(cmake, httpserver): assert_attachment(envelope) +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad or is_qemu, reason="test needs breakpad backend" + ), + ), + ], +) +def test_on_crashed_last_run(cmake, backend): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) + args = ["log", "on-crashed-last-run"] + + run( + tmp_path, + "sentry_example", + [*args, "crash"], + expect_failure=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + run_dirs = list((tmp_path / ".sentry-native").glob("*.run")) + assert len(run_dirs) == 1 + markers = list(run_dirs[0].glob("*.crash")) + assert len(markers) == 1 + assert (run_dirs[0] / f"{markers[0].stem}.envelope").is_file() + + restarted = run( + tmp_path, + "sentry_example", + [*args, "no-setup"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + callbacks = [ + line + for line in restarted.stdout.splitlines() + if line.startswith(b"CRASHED_LAST_RUN:") + ] + assert len(callbacks) == 1 + assert callbacks[0] == f"CRASHED_LAST_RUN:{markers[0].stem}".encode() + + restarted_again = run( + tmp_path, + "sentry_example", + [*args, "no-setup"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert b"CRASHED_LAST_RUN:" not in restarted_again.stdout + + @pytest.mark.parametrize( "backend", [ diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 4cae17405e..706939e146 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -74,6 +74,67 @@ def test_native_capture_crash(cmake, httpserver): assert_native_crash(envelope) +def test_native_on_crashed_last_run(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + args = ["log", "on-crashed-last-run"] + + with httpserver.wait(timeout=10) as waiting: + run_crash( + tmp_path, + "sentry_example", + [*args, "crash"], + env=env, + wait_for_daemon=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert waiting.result + assert len(httpserver.log) == 1 + + crash_envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert_native_crash(crash_envelope) + event_id = crash_envelope.headers["event_id"] + + db_dir = tmp_path / ".sentry-native" + run_dirs = list(db_dir.glob("*.run")) + assert len(run_dirs) == 1 + assert {path.name for path in run_dirs[0].glob("*.envelope")} == { + "__sentry-crash.envelope" + } + assert not list(run_dirs[0].glob("*.crash")) + + restarted = run( + tmp_path, + "sentry_example", + [*args, "no-setup"], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + callbacks = [ + line + for line in restarted.stdout.splitlines() + if line.startswith(b"CRASHED_LAST_RUN:") + ] + assert callbacks == [f"CRASHED_LAST_RUN:{event_id}".encode()] + assert len(httpserver.log) == 1 + assert not list(db_dir.glob("*.run")) + assert not list(db_dir.glob("*.run*.lock")) + + restarted_again = run( + tmp_path, + "sentry_example", + [*args, "no-setup"], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert b"CRASHED_LAST_RUN:" not in restarted_again.stdout + assert len(httpserver.log) == 1 + + @pytest.mark.skipif( sys.platform != "win32" or bool(os.environ.get("TEST_MINGW")), reason="WER crash tests are only available in MSVC Windows builds", diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 8d078a5b0f..37398adde8 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -41,6 +41,77 @@ set_file_mtime(const sentry_path_t *path, time_t mtime) #endif } +typedef struct { + size_t count; + size_t materialized_count; + sentry_uuid_t event_ids[4]; +} crashed_last_run_state_t; + +static void +record_crashed_last_run(const sentry_envelope_t *envelope, void *user_data) +{ + crashed_last_run_state_t *state = user_data; + sentry_value_t event_id = sentry_envelope_get_header(envelope, "event_id"); + if (state->count < 4) { + state->event_ids[state->count] + = sentry_uuid_from_string(sentry_value_as_string(event_id)); + } + if (!sentry_value_is_null(event_id) + && !sentry_value_is_null(sentry_envelope_get_event(envelope))) { + state->materialized_count++; + } + state->count++; +} + +static sentry_path_t * +write_event_envelope(const sentry_path_t *dir, const sentry_uuid_t *event_id) +{ + if (sentry__path_create_dir_all(dir) != 0) { + return NULL; + } + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry__envelope_add_event( + envelope, sentry__value_new_event_with_id(event_id)); + char *filename = sentry__uuid_as_filename(event_id, ".envelope"); + sentry_path_t *path + = filename ? sentry__path_join_str(dir, filename) : NULL; + sentry_free(filename); + int rv = path ? sentry_envelope_write_to_path(envelope, path) : 1; + sentry_envelope_free(envelope); + if (rv != 0) { + sentry__path_free(path); + return NULL; + } + return path; +} + +static sentry_path_t * +write_run_crash_marker( + const sentry_path_t *run_path, const sentry_uuid_t *event_id) +{ + char *filename = sentry__uuid_as_filename(event_id, ".crash"); + sentry_path_t *path + = filename ? sentry__path_join_str(run_path, filename) : NULL; + sentry_free(filename); + if (!path || sentry__path_touch(path) != 0) { + sentry__path_free(path); + return NULL; + } + return path; +} + +static bool +state_has_event_id( + const crashed_last_run_state_t *state, const sentry_uuid_t *event_id) +{ + for (size_t i = 0; i < state->count && i < 4; i++) { + if (memcmp(&state->event_ids[i], event_id, sizeof(*event_id)) == 0) { + return true; + } + } + return false; +} + SENTRY_TEST(cache_keep) { #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) @@ -98,6 +169,109 @@ SENTRY_TEST(cache_keep) sentry_close(); } +SENTRY_TEST(on_crashed_last_run) +{ + crashed_last_run_state_t state = { 0 }; + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_transport(options, NULL); + sentry_options_set_on_crashed_last_run( + options, record_crashed_last_run, &state); + TEST_CHECK_INT_EQUAL(sentry_init(options), 0); + + sentry_path_t *old_run1 + = sentry__path_join_str(options->database_path, "first.run"); + sentry_path_t *old_run2 + = sentry__path_join_str(options->database_path, "second.run"); + TEST_ASSERT(!!old_run1 && !!old_run2); + sentry__path_remove_all(old_run1); + sentry__path_remove_all(old_run2); + + sentry_uuid_t crash1 = sentry_uuid_new_v4(); + sentry_uuid_t crash2 = sentry_uuid_new_v4(); + sentry_uuid_t normal = sentry_uuid_new_v4(); + sentry_path_t *crash1_envelope = write_event_envelope(old_run1, &crash1); + sentry_path_t *normal_envelope = write_event_envelope(old_run1, &normal); + sentry_path_t *crash2_envelope = write_event_envelope(old_run2, &crash2); + sentry_path_t *crash1_marker = write_run_crash_marker(old_run1, &crash1); + sentry_path_t *crash2_marker = write_run_crash_marker(old_run2, &crash2); + TEST_ASSERT(!!crash1_envelope && !!normal_envelope && !!crash2_envelope); + TEST_ASSERT(!!crash1_marker && !!crash2_marker); + + sentry__process_old_runs(options, 0); + TEST_CHECK_INT_EQUAL(state.count, 2); + TEST_CHECK_INT_EQUAL(state.materialized_count, 2); + TEST_CHECK(state_has_event_id(&state, &crash1)); + TEST_CHECK(state_has_event_id(&state, &crash2)); + TEST_CHECK(!state_has_event_id(&state, &normal)); + TEST_CHECK(!sentry__path_is_dir(old_run1)); + TEST_CHECK(!sentry__path_is_dir(old_run2)); + + sentry__process_old_runs(options, 0); + TEST_CHECK_INT_EQUAL(state.count, 2); + + sentry__path_free(crash1_marker); + sentry__path_free(crash2_marker); + sentry__path_free(crash1_envelope); + sentry__path_free(normal_envelope); + sentry__path_free(crash2_envelope); + sentry__path_free(old_run1); + sentry__path_free(old_run2); + sentry_close(); +} + +SENTRY_TEST(on_crashed_last_run_cache) +{ + crashed_last_run_state_t state = { 0 }; + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_transport(options, NULL); + sentry_options_set_on_crashed_last_run( + options, record_crashed_last_run, &state); + TEST_CHECK_INT_EQUAL(sentry_init(options), 0); + sentry__path_remove_all(options->run->cache_path); + + sentry_uuid_t event_id = sentry_uuid_new_v4(); + TEST_CHECK(!sentry__run_write_crash_marker(options->run, &event_id)); + + sentry_path_t *old_run + = sentry__path_join_str(options->database_path, "old.run"); + sentry__path_remove_all(old_run); + sentry_path_t *crash_path = write_event_envelope(old_run, &event_id); + sentry_path_t *marker_path = write_run_crash_marker(old_run, &event_id); + + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry__envelope_add_event( + envelope, sentry__value_new_event_with_id(&event_id)); + TEST_CHECK(sentry__run_write_cache(options->run, envelope, -1)); + sentry_envelope_free(envelope); + + char *cache_filename = sentry__uuid_as_filename(&event_id, ".envelope"); + sentry_path_t *cache_path + = sentry__path_join_str(options->run->cache_path, cache_filename); + sentry_free(cache_filename); + TEST_ASSERT(!!old_run && !!crash_path && !!marker_path && !!cache_path); + TEST_CHECK(sentry__path_is_file(crash_path)); + TEST_CHECK(sentry__path_is_file(marker_path)); + TEST_CHECK(sentry__path_is_file(cache_path)); + + sentry__process_old_runs(options, 0); + TEST_CHECK_INT_EQUAL(state.count, 1); + TEST_CHECK_INT_EQUAL(state.materialized_count, 1); + TEST_CHECK(state_has_event_id(&state, &event_id)); + TEST_CHECK(!sentry__path_is_file(crash_path)); + TEST_CHECK(!sentry__path_is_file(marker_path)); + TEST_CHECK(sentry__path_is_file(cache_path)); + + sentry__process_old_runs(options, 0); + TEST_CHECK_INT_EQUAL(state.count, 1); + + sentry__path_remove_all(options->run->cache_path); + sentry__path_free(old_run); + sentry__path_free(crash_path); + sentry__path_free(marker_path); + sentry__path_free(cache_path); + sentry_close(); +} + SENTRY_TEST(cache_max_size) { #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 89ab18c54b..0a7114a828 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -15,6 +15,13 @@ // Include native backend headers # include "../../src/backends/native/minidump/sentry_minidump_format.h" # include "../../src/backends/native/sentry_crash_context.h" + +static void +noop_crashed_last_run(const sentry_envelope_t *envelope, void *user_data) +{ + (void)envelope; + (void)user_data; +} #endif #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) @@ -520,6 +527,8 @@ SENTRY_TEST(crash_context_options_propagation) sentry_options_set_proxy(options, "http://myproxy:3128"); sentry_options_set_shutdown_timeout(options, 12345); sentry_options_set_system_crash_reporter_enabled(options, true); + sentry_options_set_on_crashed_last_run( + options, noop_crashed_last_run, NULL); sentry_options_set_crash_upload_mode( options, SENTRY_CRASH_UPLOAD_MODE_ASYNC); sentry_options_set_transfer_timeout(options, 45000); @@ -554,6 +563,7 @@ SENTRY_TEST(crash_context_options_propagation) } ctx->shutdown_timeout = options->shutdown_timeout; ctx->system_crash_reporter_enabled = options->system_crash_reporter_enabled; + ctx->has_on_crashed_last_run = options->on_crashed_last_run_func != NULL; ctx->crash_upload_mode = options->crash_upload_mode; ctx->transfer_timeout = options->transfer_timeout; # ifdef SENTRY_PLATFORM_WINDOWS @@ -567,6 +577,7 @@ SENTRY_TEST(crash_context_options_propagation) TEST_CHECK(ctx->user_agent[0] != '\0'); TEST_CHECK_UINT64_EQUAL(ctx->shutdown_timeout, 12345); TEST_CHECK(ctx->system_crash_reporter_enabled); + TEST_CHECK(ctx->has_on_crashed_last_run); TEST_CHECK_INT_EQUAL( ctx->crash_upload_mode, SENTRY_CRASH_UPLOAD_MODE_ASYNC); TEST_CHECK_UINT64_EQUAL(ctx->transfer_timeout, 45000); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index fe24891b61..dd82d96288 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -241,6 +241,8 @@ XX(mpack_newlines) XX(mpack_removed_tags) XX(multiple_inits) XX(multiple_transactions) +XX(on_crashed_last_run) +XX(on_crashed_last_run_cache) XX(options_crash_reporting_mode_clamp) XX(options_crash_reporting_mode_default) XX(options_crash_reporting_mode_set_get) From b45ff320eb9119573a18b5443fd018fc48280dd7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 19:22:48 +0200 Subject: [PATCH 09/13] corrupt_crash_envelope_does_not_block_old_run --- src/backends/sentry_backend_native.c | 3 +- tests/unit/test_native_backend.c | 61 ++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 278b403336..495860a281 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -201,8 +201,7 @@ native_backend_process_old_run(sentry_backend_t *backend, : NULL; bool materialized = envelope && sentry__envelope_materialize(envelope); // remove before invoking to prevent repeated callbacks - if ((options->on_crashed_last_run_func && !materialized) - || sentry__path_remove(file) != 0) { + if (sentry__path_remove(file) != 0) { sentry_envelope_free(envelope); sentry__pathiter_free(it); return false; diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 0a7114a828..59ad85cbd1 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -6,6 +6,7 @@ */ #include "sentry_database.h" +#include "sentry_envelope.h" #include "sentry_options.h" #include "sentry_path.h" #include "sentry_testsupport.h" @@ -22,6 +23,14 @@ noop_crashed_last_run(const sentry_envelope_t *envelope, void *user_data) (void)envelope; (void)user_data; } + +static void +count_sent_envelopes(sentry_envelope_t *envelope, void *state) +{ + size_t *count = state; + (*count)++; + sentry_envelope_free(envelope); +} #endif #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) @@ -118,6 +127,58 @@ SENTRY_TEST(daemon_run_blocks_old_run_cleanup) sentry_options_free(options); } +SENTRY_TEST(corrupt_crash_envelope_does_not_block_old_run) +{ +#ifdef SENTRY_BACKEND_NATIVE + SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(sentry__path_remove_all(options->database_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(options->database_path) == 0); + + options->run = sentry__run_new(options->database_path); + TEST_ASSERT(!!options->run); + sentry_run_t *old_run = sentry__run_new(options->database_path); + TEST_ASSERT(!!old_run); + sentry__filelock_unlock(old_run->lock); + + sentry_path_t *crash_envelope + = sentry__path_join_str(old_run->run_path, "__sentry-crash.envelope"); + sentry_path_t *queued_envelope + = sentry__path_join_str(old_run->run_path, "queued.envelope"); + TEST_ASSERT(!!crash_envelope && !!queued_envelope); + TEST_ASSERT(sentry__path_write_buffer(crash_envelope, "garbage", 7) == 0); + + sentry_envelope_t *envelope = sentry__envelope_new(); + TEST_ASSERT(!!envelope); + sentry__envelope_add_event(envelope, + sentry_value_new_message_event(SENTRY_LEVEL_ERROR, NULL, "queued")); + TEST_ASSERT(sentry_envelope_write_to_path(envelope, queued_envelope) == 0); + sentry_envelope_free(envelope); + + size_t sent_envelopes = 0; + sentry_transport_t *transport = sentry_transport_new(count_sent_envelopes); + TEST_ASSERT(!!transport); + sentry_transport_set_state(transport, &sent_envelopes); + sentry_options_set_transport(options, transport); + sentry_options_set_on_crashed_last_run( + options, noop_crashed_last_run, NULL); + + sentry__process_old_runs(options, 0); + + TEST_CHECK_INT_EQUAL(sent_envelopes, 1); + TEST_CHECK(!sentry__path_is_dir(old_run->run_path)); + TEST_CHECK(!sentry__path_is_file(crash_envelope)); + TEST_CHECK(!sentry__path_is_file(queued_envelope)); + + sentry__path_free(queued_envelope); + sentry__path_free(crash_envelope); + sentry__run_free(old_run); + sentry__run_clean(options->run, true); + sentry_options_free(options); +#else + SKIP_TEST(); +#endif +} + /** * Test minidump header structure size and alignment */ diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index dd82d96288..a144a9c602 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -105,6 +105,7 @@ XX(concurrent_uninit) XX(cond_wait_timeout_overflow) XX(cond_wake_all) XX(continuation_no_baggage_uses_sdk_dsc) +XX(corrupt_crash_envelope_does_not_block_old_run) XX(count_sampled_events) XX(crash_context_handler_path_propagation) XX(crash_context_init) From 69222b4c2bd0cc0621274c060dcd5453b138d443 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 19:25:14 +0200 Subject: [PATCH 10/13] callback_envelope_is_not_resent_without_backend --- src/sentry_database.c | 3 +- tests/unit/test_cache.c | 62 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 9b4527c39e..d66270cf72 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -667,7 +667,8 @@ sentry__process_run_envelopes( const sentry_path_t *file; while (it && (file = sentry__pathiter_next(it)) != NULL) { if (!sentry__path_is_file(file) || sentry__path_is_symlink(file) - || !sentry__path_ends_with(file, ".envelope")) { + || !sentry__path_ends_with(file, ".envelope") + || sentry__path_filename_matches(file, "__sentry-crash.envelope")) { continue; } sentry_envelope_t *envelope = sentry__envelope_from_path(file); diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 37398adde8..4c7c889d3c 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -63,6 +63,14 @@ record_crashed_last_run(const sentry_envelope_t *envelope, void *user_data) state->count++; } +static void +count_sent_envelopes(sentry_envelope_t *envelope, void *user_data) +{ + size_t *count = user_data; + (*count)++; + sentry_envelope_free(envelope); +} + static sentry_path_t * write_event_envelope(const sentry_path_t *dir, const sentry_uuid_t *event_id) { @@ -272,6 +280,60 @@ SENTRY_TEST(on_crashed_last_run_cache) sentry_close(); } +SENTRY_TEST(callback_envelope_is_not_resent_without_backend) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#else + SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(sentry__path_remove_all(options->database_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(options->database_path) == 0); + + options->run = sentry__run_new(options->database_path); + TEST_ASSERT(!!options->run); + sentry_run_t *old_run = sentry__run_new(options->database_path); + TEST_ASSERT(!!old_run); + sentry__filelock_unlock(old_run->lock); + + sentry_path_t *callback_path + = sentry__path_join_str(old_run->run_path, "__sentry-crash.envelope"); + TEST_ASSERT(!!callback_path); + sentry_envelope_t *callback_envelope = sentry__envelope_new(); + TEST_ASSERT(!!callback_envelope); + sentry_uuid_t callback_id = sentry_uuid_new_v4(); + sentry__envelope_add_event( + callback_envelope, sentry__value_new_event_with_id(&callback_id)); + TEST_ASSERT( + sentry_envelope_write_to_path(callback_envelope, callback_path) == 0); + sentry_envelope_free(callback_envelope); + + sentry_uuid_t queued_id = sentry_uuid_new_v4(); + sentry_path_t *queued_path + = write_event_envelope(old_run->run_path, &queued_id); + TEST_ASSERT(!!queued_path); + + size_t sent_envelopes = 0; + sentry_transport_t *transport = sentry_transport_new(count_sent_envelopes); + TEST_ASSERT(!!transport); + sentry_transport_set_state(transport, &sent_envelopes); + sentry_options_set_transport(options, transport); + sentry_options_set_backend(options, NULL); + + sentry__process_old_runs(options, 0); + + TEST_CHECK_INT_EQUAL(sent_envelopes, 1); + TEST_CHECK(!sentry__path_is_dir(old_run->run_path)); + TEST_CHECK(!sentry__path_is_file(callback_path)); + TEST_CHECK(!sentry__path_is_file(queued_path)); + + sentry__path_free(queued_path); + sentry__path_free(callback_path); + sentry__run_free(old_run); + sentry__run_clean(options->run, true); + sentry_options_free(options); +#endif +} + SENTRY_TEST(cache_max_size) { #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index a144a9c602..db01ca92e5 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -79,6 +79,7 @@ XX(cache_remove_siblings) XX(cache_symlink_run) XX(cache_write_minidump) XX(cache_write_raw_with_minidump) +XX(callback_envelope_is_not_resent_without_backend) XX(capture_minidump_basic) XX(capture_minidump_discard) XX(capture_minidump_invalid_path) From 61eb8b2952faef910dc94844893f3788b13257fe Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 22:12:43 +0200 Subject: [PATCH 11/13] fix review finding --- src/backends/sentry_backend_crashpad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index c2e56b2d04..8bf00f8dbe 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -738,7 +738,7 @@ crashpad_backend_process_old_run(sentry_backend_t *backend, if (!envelope || !sentry__envelope_materialize(envelope)) { sentry_envelope_free(envelope); sentry__path_free(event_path); - return false; + return true; } // remove before invoking to prevent repeated callbacks From 00cfbae93b998e6366cacadb91ea29389819ab80 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 14:11:20 +0200 Subject: [PATCH 12/13] fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c31b01fe..60b6e7e00c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Features**: -- Add `on_crashed_last_run` callback for inspecting crash envelopes from previous runs. ([#1977](https://github.com/getsentry/sentry-native/pull/1977)) +- Add `on_crashed_last_run` callback for inspecting crash envelopes from previous runs. ([#1985](https://github.com/getsentry/sentry-native/pull/1985)) **Fixes**: From bf26eaf0da06738d338671609a38c11275f6cb35 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 14:50:22 +0200 Subject: [PATCH 13/13] old run retry --- src/sentry_database.c | 19 ++++----- tests/unit/test_cache.c | 89 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 100 insertions(+), 9 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index d66270cf72..39edcd750a 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -770,12 +770,10 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__path_free(daemon_lockfile); } - if (options->backend && options->backend->process_old_run_func - && !options->backend->process_old_run_func( - options->backend, options, run_dir)) { - sentry__filelock_free(daemon_lock); - sentry__filelock_free(lock); - continue; + bool processed = true; + if (options->backend && options->backend->process_old_run_func) { + processed = options->backend->process_old_run_func( + options->backend, options, run_dir); } sentry__process_run_envelopes(options, run_dir); @@ -823,13 +821,16 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) session_num = 0; } } + sentry__path_remove(file); + } else if (processed) { + sentry__path_remove(file); } - - sentry__path_remove(file); } sentry__pathiter_free(run_iter); - sentry__path_remove_all(run_dir); + if (processed) { + sentry__path_remove_all(run_dir); + } if (daemon_lock) { sentry__filelock_free(daemon_lock); } diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 4c7c889d3c..dade035914 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -1,9 +1,12 @@ +#include "sentry_alloc.h" +#include "sentry_backend.h" #include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" #include "sentry_path.h" #include "sentry_retry.h" +#include "sentry_session.h" #include "sentry_string.h" #include "sentry_testsupport.h" #include "sentry_uuid.h" @@ -71,6 +74,22 @@ count_sent_envelopes(sentry_envelope_t *envelope, void *user_data) sentry_envelope_free(envelope); } +#if !defined(SENTRY_PLATFORM_NX) && !defined(SENTRY_PLATFORM_PS) +typedef struct { + size_t attempts; +} old_run_retry_state_t; + +static bool +retry_old_run(sentry_backend_t *backend, const sentry_options_t *options, + const sentry_path_t *run_path) +{ + (void)options; + (void)run_path; + old_run_retry_state_t *state = backend->data; + return ++state->attempts > 1; +} +#endif + static sentry_path_t * write_event_envelope(const sentry_path_t *dir, const sentry_uuid_t *event_id) { @@ -177,6 +196,76 @@ SENTRY_TEST(cache_keep) sentry_close(); } +SENTRY_TEST(old_run_retry) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#else + SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(sentry__path_remove_all(options->database_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(options->database_path) == 0); + + options->run = sentry__run_new(options->database_path); + TEST_ASSERT(!!options->run); + sentry_run_t *old_run = sentry__run_new(options->database_path); + TEST_ASSERT(!!old_run); + sentry__filelock_unlock(old_run->lock); + + sentry_uuid_t event_id = sentry_uuid_new_v4(); + sentry_path_t *queued_envelope + = write_event_envelope(old_run->run_path, &event_id); + sentry_path_t *backend_state + = sentry__path_join_str(old_run->run_path, "backend-state"); + TEST_ASSERT(!!queued_envelope && !!backend_state); + TEST_ASSERT(sentry__path_touch(backend_state) == 0); + + static const char session_json[] + = "{\"init\":true,\"sid\":\"00000000-0000-4000-8000-000000000001\"," + "\"status\":\"ok\",\"errors\":0,\"started\":\"2020-01-01T00:00:00Z\"," + "\"duration\":0,\"attrs\":{\"release\":\"test@1.0.0\"," + "\"environment\":\"production\"}}"; + sentry_session_t *session + = sentry__session_from_json(session_json, sizeof(session_json) - 1); + TEST_ASSERT(!!session); + TEST_ASSERT(sentry__run_write_session(old_run, session)); + sentry__session_free(session); + + size_t sent_envelopes = 0; + sentry_transport_t *transport = sentry_transport_new(count_sent_envelopes); + TEST_ASSERT(!!transport); + sentry_transport_set_state(transport, &sent_envelopes); + sentry_options_set_transport(options, transport); + + old_run_retry_state_t retry_state = { 0 }; + sentry_backend_t *backend = SENTRY_MAKE(sentry_backend_t); + TEST_ASSERT(!!backend); + backend->data = &retry_state; + backend->process_old_run_func = retry_old_run; + sentry_options_set_backend(options, backend); + + sentry__process_old_runs(options, 0); + + TEST_CHECK_INT_EQUAL(retry_state.attempts, 1); + TEST_CHECK_INT_EQUAL(sent_envelopes, 2); + TEST_CHECK(sentry__path_is_dir(old_run->run_path)); + TEST_CHECK(sentry__path_is_file(backend_state)); + TEST_CHECK(!sentry__path_is_file(queued_envelope)); + TEST_CHECK(!sentry__path_is_file(old_run->session_path)); + + sentry__process_old_runs(options, 0); + + TEST_CHECK_INT_EQUAL(retry_state.attempts, 2); + TEST_CHECK_INT_EQUAL(sent_envelopes, 2); + TEST_CHECK(!sentry__path_is_dir(old_run->run_path)); + + sentry__path_free(backend_state); + sentry__path_free(queued_envelope); + sentry__run_free(old_run); + sentry__run_clean(options->run, true); + sentry_options_free(options); +#endif +} + SENTRY_TEST(on_crashed_last_run) { crashed_last_run_state_t state = { 0 }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index db01ca92e5..272fbc61cb 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -243,6 +243,7 @@ XX(mpack_newlines) XX(mpack_removed_tags) XX(multiple_inits) XX(multiple_transactions) +XX(old_run_retry) XX(on_crashed_last_run) XX(on_crashed_last_run_cache) XX(options_crash_reporting_mode_clamp)