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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 54 additions & 25 deletions examples/ffi/crashtracking.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define INIT_FROM_SLICE(s) {.ptr = s.ptr, .len = s.len}
#define INIT_FROM_SLICE(s) \
{ .ptr = s.ptr, .len = s.len }

void example_segfault_handler(int signal) {
printf("Segmentation fault caught. Signal number: %d\n", signal);
exit(-1);
}
static ddog_CharSlice slice(const char *s) { return (ddog_CharSlice){.ptr = s, .len = strlen(s)}; }

void handle_result(ddog_VoidResult result) {
if (result.tag == DDOG_VOID_RESULT_ERR) {
Expand All @@ -34,30 +33,58 @@ uintptr_t handle_uintptr_t_result(ddog_crasht_Result_Usize result) {
}

int main(int argc, char **argv) {
if (signal(SIGSEGV, example_segfault_handler) == SIG_ERR) {
perror("Error setting up signal handler");
return -1;
// Receiver binary path: CLI arg > env var > hardcoded default
const char *receiver_path = NULL;
if (argc >= 2) {
receiver_path = argv[1];
} else {
receiver_path = getenv("DDOG_CRASHT_TEST_RECEIVER");
}
if (!receiver_path || receiver_path[0] == '\0') {
receiver_path = "/tmp/libdatadog/bin/libdatadog-crashtracking-receiver";
}

// Output directory: env var > hardcoded default
const char *output_dir = getenv("DDOG_CRASHT_TEST_OUTPUT_DIR");
if (!output_dir || output_dir[0] == '\0') {
output_dir = "/tmp/crashreports";
}

// Build output file paths
char report_path[512];
char stderr_path[512];
char stdout_path[512];
snprintf(report_path, sizeof(report_path), "%s/crashreport.json", output_dir);
snprintf(stderr_path, sizeof(stderr_path), "%s/stderr.txt", output_dir);
snprintf(stdout_path, sizeof(stdout_path), "%s/stdout.txt", output_dir);

// Forward the dynamic-linker search path to the receiver process.
#ifdef __APPLE__
const char *ld_search_path_var = "DYLD_LIBRARY_PATH";
#else
const char *ld_search_path_var = "LD_LIBRARY_PATH";
#endif
const char *ld_library_path = getenv(ld_search_path_var);
ddog_crasht_EnvVar env_vars[1];
ddog_crasht_Slice_EnvVar env_slice = {.ptr = NULL, .len = 0};
if (ld_library_path && ld_library_path[0] != '\0') {
env_vars[0].key = slice(ld_search_path_var);
env_vars[0].val = slice(ld_library_path);
env_slice.ptr = env_vars;
env_slice.len = 1;
}

ddog_crasht_ReceiverConfig receiver_config = {
.args = {},
.env = {},
//.path_to_receiver_binary = DDOG_CHARSLICE_C("SET ME TO THE ACTUAL PATH ON YOUR MACHINE"),
// E.g. on my machine, where I run ./build-profiling-ffi.sh /tmp/libdatadog
.path_to_receiver_binary =
DDOG_CHARSLICE_C("/tmp/libdatadog/bin/libdatadog-crashtracking-receiver"),
.optional_stderr_filename = DDOG_CHARSLICE_C("/tmp/crashreports/stderr.txt"),
.optional_stdout_filename = DDOG_CHARSLICE_C("/tmp/crashreports/stdout.txt"),
.env = env_slice,
.path_to_receiver_binary = slice(receiver_path),
.optional_stderr_filename = slice(stderr_path),
.optional_stdout_filename = slice(stdout_path),
};

struct ddog_Endpoint *endpoint =
ddog_endpoint_from_filename(DDOG_CHARSLICE_C("/tmp/crashreports/crashreport.json"));
// Alternatively:
// struct ddog_Endpoint * endpoint =
// ddog_endpoint_from_url(DDOG_CHARSLICE_C("http://localhost:8126"));
struct ddog_Endpoint *endpoint = ddog_endpoint_from_filename(slice(report_path));

// Get the default signals and explicitly use them.
// We could also pass an empty list here, which would also use the default signals.
struct ddog_crasht_Slice_CInt signals = ddog_crasht_default_signals();
ddog_crasht_Config config = {
.create_alt_stack = false,
Expand All @@ -79,8 +106,10 @@ int main(int argc, char **argv) {
handle_result(ddog_crasht_begin_op(DDOG_CRASHT_OP_TYPES_PROFILER_COLLECTING_SAMPLE));
handle_uintptr_t_result(ddog_crasht_insert_span_id(0, 42));
handle_uintptr_t_result(ddog_crasht_insert_trace_id(1, 1));
handle_uintptr_t_result(ddog_crasht_insert_additional_tag(DDOG_CHARSLICE_C("This is a very informative extra bit of info")));
handle_uintptr_t_result(ddog_crasht_insert_additional_tag(DDOG_CHARSLICE_C("This message will for sure help us debug the crash")));
handle_uintptr_t_result(ddog_crasht_insert_additional_tag(
DDOG_CHARSLICE_C("This is a very informative extra bit of info")));
handle_uintptr_t_result(ddog_crasht_insert_additional_tag(
DDOG_CHARSLICE_C("This message will for sure help us debug the crash")));

#ifdef EXPLICIT_RAISE_SEGV
// Test raising SEGV explicitly, to ensure chaining works
Expand All @@ -91,8 +120,8 @@ int main(int argc, char **argv) {
char *bug = NULL;
*bug = 42;

// At this point, we expect the following files to be written into /tmp/crashreports
// foo.txt foo.txt.telemetry stderr.txt stdout.txt
// The crash handler should intercept the SIGSEGV, invoke the receiver,
// and write the crash report to output_dir before the process terminates.
return 0;
}

Expand Down
158 changes: 120 additions & 38 deletions tools/src/bin/ffi_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,58 +121,102 @@ fn skip_examples() -> &'static HashMap<&'static str, &'static str> {
static MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
MAP.get_or_init(|| {
HashMap::from([
("crashtracking", "intentionally crashes"),
("exporter", "requires CLI arguments"),
("exporter_manager", "Flaky because SIGPIPE thing"),
])
})
}

struct ExpectedCrash {
/// Expected Unix signal number (ex 11 for SIGSEGV).
#[cfg(unix)]
signal: i32,
/// File that should exist in the work directory after the crash,
/// confirming the crash handler ran successfully
output_file: &'static str,
}

fn expected_crashes() -> &'static HashMap<&'static str, ExpectedCrash> {
static MAP: OnceLock<HashMap<&'static str, ExpectedCrash>> = OnceLock::new();
MAP.get_or_init(|| {
HashMap::from([(
"crashtracking",
ExpectedCrash {
#[cfg(unix)]
signal: 11, // SIGSEGV
output_file: "crashreport.json",
},
)])
})
}

/// Locate the crashtracking receiver binary and library directory.
///
/// They may live in either "release/" (local/ffi_test build) or "artifacts/"
/// (CI pre-built). Returns `(receiver_binary_path, lib_dir)`
fn find_receiver_paths(project_root: &Path) -> (PathBuf, PathBuf) {
let make_paths = |dir: &str| {
let base = project_root.join(dir);
(
base.join("bin").join("libdatadog-crashtracking-receiver"),
base.join("lib"),
)
};
["release", "artifacts"]
.iter()
.map(|dir| make_paths(dir))
.find(|(bin, _)| bin.exists())
.unwrap_or_else(|| make_paths("release"))
}

/// Build the library search-path env var for the receiver process.
///
/// The C test binary is dynamically linked against libdatadog_profiling.{so,dylib}
/// which is not on the system library path. Returns `(var_name, value)`
fn library_search_path_env(lib_dir: &Path) -> (String, String) {
#[cfg(target_os = "macos")]
let search_path_var = "DYLD_LIBRARY_PATH";
#[cfg(not(target_os = "macos"))]
let search_path_var = "LD_LIBRARY_PATH";

let lib_path = match std::env::var(search_path_var) {
Ok(existing) if !existing.is_empty() => {
format!("{}:{}", lib_dir.display(), existing)
}
_ => lib_dir.display().to_string(),
};
(search_path_var.to_string(), lib_path)
}

/// Per-test environment variables. The runner sets these before spawning
/// the test executable so that tests which need external resources (e.g. the
/// receiver binary) can find them without hard-coding paths.
fn per_test_env(name: &str, project_root: &Path) -> Vec<(String, String)> {
fn per_test_env(name: &str, project_root: &Path, work_dir: &Path) -> Vec<(String, String)> {
match name {
"crashtracking_unhandled_exception" => {
// The receiver binary and shared library may live in either
// "release/" (local/ffi_test build) or "artifacts/" (CI pre-built).
// Check both and use whichever exists.
let make_paths = |dir: &str| {
let base = project_root.join(dir);
"crashtracking" => {
let (receiver, lib_dir) = find_receiver_paths(project_root);
let (search_var, search_val) = library_search_path_env(&lib_dir);
vec![
(
base.join("bin").join("libdatadog-crashtracking-receiver"),
base.join("lib"),
)
};
let (receiver, lib_dir) = ["release", "artifacts"]
.iter()
.map(|dir| make_paths(dir))
.find(|(bin, _)| bin.exists())
.unwrap_or_else(|| make_paths("release"));

// The C test binary is dynamically linked against libdatadog_profiling.{so,dylib}
// which is not on the system library path. Set the platform-specific linker
// search path so the binary can load, and the C test forwards it via getenv()
// into the receiver's explicit execve environment.
// Linux → LD_LIBRARY_PATH
// macOS → DYLD_LIBRARY_PATH
#[cfg(target_os = "macos")]
let search_path_var = "DYLD_LIBRARY_PATH";
#[cfg(not(target_os = "macos"))]
let search_path_var = "LD_LIBRARY_PATH";

let lib_path = match std::env::var(search_path_var) {
Ok(existing) if !existing.is_empty() => {
format!("{}:{}", lib_dir.display(), existing)
}
_ => lib_dir.display().to_string(),
};
"DDOG_CRASHT_TEST_RECEIVER".to_string(),
receiver.display().to_string(),
),
(
"DDOG_CRASHT_TEST_OUTPUT_DIR".to_string(),
work_dir.display().to_string(),
),
(search_var, search_val),
]
}
"crashtracking_unhandled_exception" => {
let (receiver, lib_dir) = find_receiver_paths(project_root);
let (search_var, search_val) = library_search_path_env(&lib_dir);
vec![
(
"DDOG_CRASHT_TEST_RECEIVER".to_string(),
receiver.display().to_string(),
),
(search_path_var.to_string(), lib_path),
(search_var, search_val),
]
}
_ => vec![],
Expand Down Expand Up @@ -428,9 +472,46 @@ fn format_exit_status(status: &std::process::ExitStatus) -> String {
fn determine_status(
exit_status: Option<std::process::ExitStatus>,
is_expected_failure: bool,
expected_crash: Option<&ExpectedCrash>,
work_dir: &Path,
) -> TestStatus {
match exit_status {
Some(status) => {
// Check for expected crash first
if let Some(crash) = expected_crash {
let crashed_with_expected_signal = {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status.signal() == Some(crash.signal)
}
#[cfg(not(unix))]
{
!status.success()
}
};

if crashed_with_expected_signal {
let output_path = work_dir.join(crash.output_file);
if output_path.exists() {
return TestStatus::Passed;
}
return TestStatus::Failed(format!(
"crashed as expected but output file '{}' not found",
crash.output_file
));
}
if status.success() {
return TestStatus::Failed(
"expected crash but process exited successfully".to_string(),
);
}
return TestStatus::Failed(format!(
"expected crash signal but got {}",
format_exit_status(&status)
));
}

let success = status.success();
match (success, is_expected_failure) {
(true, false) => TestStatus::Passed,
Expand All @@ -451,7 +532,8 @@ fn run_test(
timeout: Duration,
) -> TestResult {
let is_expected_failure = expected_failures().contains_key(name);
let env_vars = per_test_env(name, project_root);
let expected_crash = expected_crashes().get(name);
let env_vars = per_test_env(name, project_root, work_dir);
let start = Instant::now();

let child = match spawn_test(exe_path, work_dir, &env_vars) {
Expand All @@ -467,7 +549,7 @@ fn run_test(
};

let (exit_status, output) = wait_with_output(child, timeout);
let status = determine_status(exit_status, is_expected_failure);
let status = determine_status(exit_status, is_expected_failure, expected_crash, work_dir);

TestResult {
name: name.to_string(),
Expand Down
Loading