#![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")]
#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
#![deny(unsafe_code)]
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::{exit, Command};
use std::sync::{Arc, Once, OnceLock};
use std::time::Duration;
use minidumper_child::{ClientHandle, MinidumperChild};
use sentry_core::protocol::{Attachment, AttachmentType, Breadcrumb, Event, User, Value};
use sentry_core::{sentry_debug, Client, ClientOptions, Hub, Integration, Level, Scope};
const DEFAULT_SERVER_ENV_VAR: &str = "_SENTRY_CRASH_REPORTER_SERVER";
const DEFAULT_FLUSH_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_PROCESS_NAME: &str = "Crash Reporter (Sentry Rust SDK)";
type OnProcess = dyn Fn(&mut Command) + Send + Sync + 'static;
type BeforeCapture = dyn Fn(&mut Scope, &Path) + Send + Sync + 'static;
#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)]
enum ScopeUpdate {
AddBreadcrumb(Breadcrumb),
SetUser(Option<User>),
SetExtra(String, Option<Value>),
SetTag(String, Option<String>),
}
pub struct MinidumpIntegration {
crashes_dir: Option<PathBuf>,
server_env_var: String,
inherit_args: bool,
process_name: Option<OsString>,
on_process: Option<Arc<OnProcess>>,
before_capture: Option<Arc<BeforeCapture>>,
flush_timeout: Duration,
client_connect_timeout: Option<Duration>,
server_stale_timeout: Option<Duration>,
handle: OnceLock<ClientHandle>,
}
impl std::fmt::Debug for MinidumpIntegration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MinidumpIntegration")
.field("crashes_dir", &self.crashes_dir)
.field("server_env_var", &self.server_env_var)
.field("inherit_args", &self.inherit_args)
.field("process_name", &self.process_name)
.field("flush_timeout", &self.flush_timeout)
.field("client_connect_timeout", &self.client_connect_timeout)
.field("server_stale_timeout", &self.server_stale_timeout)
.finish_non_exhaustive()
}
}
impl Default for MinidumpIntegration {
fn default() -> Self {
Self {
crashes_dir: None,
server_env_var: DEFAULT_SERVER_ENV_VAR.to_owned(),
inherit_args: true,
process_name: Some(OsString::from(DEFAULT_PROCESS_NAME)),
on_process: None,
before_capture: None,
flush_timeout: DEFAULT_FLUSH_TIMEOUT,
client_connect_timeout: None,
server_stale_timeout: None,
handle: OnceLock::new(),
}
}
}
impl MinidumpIntegration {
pub fn new() -> Self {
Self::default()
}
pub fn is_crash_reporter_process(&self) -> bool {
std::env::var_os(&self.server_env_var).is_some()
}
#[must_use]
pub fn crashes_dir<P>(mut self, dir: P) -> Self
where
P: Into<PathBuf>,
{
self.crashes_dir = Some(dir.into());
self
}
#[must_use]
pub fn server_env_var<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.server_env_var = name.into();
self
}
#[must_use]
pub fn inherit_args(mut self, inherit: bool) -> Self {
self.inherit_args = inherit;
self
}
#[must_use]
pub fn process_name<S>(mut self, name: S) -> Self
where
S: Into<OsString>,
{
self.process_name = Some(name.into());
self
}
#[must_use]
pub fn on_process<F>(mut self, f: F) -> Self
where
F: Fn(&mut Command) + Send + Sync + 'static,
{
self.on_process = Some(Arc::new(f));
self
}
#[must_use]
pub fn before_capture<F>(mut self, f: F) -> Self
where
F: Fn(&mut Scope, &Path) + Send + Sync + 'static,
{
self.before_capture = Some(Arc::new(f));
self
}
#[must_use]
pub fn flush_timeout(mut self, timeout: Duration) -> Self {
self.flush_timeout = timeout;
self
}
#[must_use]
pub fn client_connect_timeout(mut self, timeout: Duration) -> Self {
self.client_connect_timeout = Some(timeout);
self
}
#[must_use]
pub fn server_stale_timeout(mut self, timeout: Duration) -> Self {
self.server_stale_timeout = Some(timeout);
self
}
fn send(&self, update: &ScopeUpdate) {
if let Some(handle) = self.handle.get() {
if let Ok(buffer) = serde_json::to_vec(update) {
handle.send_message(0, buffer).ok();
}
}
}
pub fn add_breadcrumb(&self, breadcrumb: Breadcrumb) {
self.send(&ScopeUpdate::AddBreadcrumb(breadcrumb));
}
pub fn set_user(&self, user: Option<User>) {
self.send(&ScopeUpdate::SetUser(user));
}
pub fn set_extra(&self, key: String, value: Option<Value>) {
self.send(&ScopeUpdate::SetExtra(key, value));
}
pub fn set_tag(&self, key: String, value: Option<String>) {
self.send(&ScopeUpdate::SetTag(key, value));
}
fn build_child(&self) -> MinidumperChild {
let mut child = MinidumperChild::new().with_server_env_var(self.server_env_var.clone());
if let Some(dir) = &self.crashes_dir {
child = child.with_crashes_dir(dir.clone());
}
if let Some(timeout) = self.client_connect_timeout {
child = child.with_client_connect_timeout(timeout);
}
if let Some(timeout) = self.server_stale_timeout {
child = child.with_server_stale_timeout(timeout);
}
let inherit_args = self.inherit_args;
let process_name = self.process_name.clone();
let on_process = self.on_process.clone();
child = child.on_process(move |command| {
if inherit_args {
command.args(std::env::args_os().skip(1));
}
#[cfg(unix)]
if let Some(name) = process_name {
use std::os::unix::process::CommandExt;
command.arg0(name);
}
#[cfg(not(unix))]
let _ = process_name;
if let Some(on_process) = &on_process {
on_process(command);
}
});
child = child.on_message(|_kind, buffer| {
if let Ok(update) = serde_json::from_slice::<ScopeUpdate>(&buffer) {
let hub = Hub::current();
match update {
ScopeUpdate::AddBreadcrumb(b) => hub.add_breadcrumb(b),
ScopeUpdate::SetUser(u) => hub.configure_scope(|scope| scope.set_user(u)),
ScopeUpdate::SetExtra(k, v) => hub.configure_scope(|scope| match v {
Some(v) => scope.set_extra(&k, v),
None => scope.remove_extra(&k),
}),
ScopeUpdate::SetTag(k, v) => hub.configure_scope(|scope| match v {
Some(v) => scope.set_tag(&k, &v),
None => scope.remove_tag(&k),
}),
}
}
});
let flush_timeout = self.flush_timeout;
let before_capture = self.before_capture.clone();
child.on_minidump(move |buffer, path| {
let hub = Hub::current();
hub.with_scope(
|scope| {
scope.remove_extra("event.process");
let filename = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "minidump.dmp".to_string());
scope.add_attachment(Attachment {
buffer,
filename,
ty: Some(AttachmentType::Minidump),
..Default::default()
});
if let Some(before_capture) = &before_capture {
before_capture(scope, path);
}
},
|| {
hub.capture_event(Event {
level: Level::Fatal,
..Default::default()
});
},
);
if let Some(client) = hub.client() {
client.flush(Some(flush_timeout));
}
})
}
}
fn run_crash_reporter(
child: MinidumperChild,
options: &ClientOptions,
flush_timeout: Duration,
) -> ! {
let mut reporter_options = options.clone();
reporter_options.integrations.retain(|i| {
i.as_ref()
.as_any()
.downcast_ref::<MinidumpIntegration>()
.is_none()
});
let client = Client::with_options(reporter_options);
let hub = Hub::current();
hub.bind_client(Some(Arc::new(client)));
hub.configure_scope(|scope| {
scope.set_extra("event.process", Value::String("crash-reporter".to_string()));
});
match child.spawn() {
Ok(_handle) => exit(0),
Err(err) => {
hub.capture_error(&err);
if let Some(client) = hub.client() {
client.flush(Some(flush_timeout));
}
exit(1);
}
}
}
impl Integration for MinidumpIntegration {
fn name(&self) -> &'static str {
"minidump"
}
fn setup(&self, options: &mut ClientOptions) {
static SETUP: Once = Once::new();
if options.dsn.is_none() {
return;
}
let mut setup_run = false;
SETUP.call_once(|| {
if self.is_crash_reporter_process() {
run_crash_reporter(self.build_child(), options, self.flush_timeout);
}
setup_run = true;
if self.handle.get().is_some() {
return;
}
match self.build_child().spawn() {
Ok(handle) => {
let _ = self.handle.set(handle);
}
Err(err) => {
sentry_debug!("could not start crash reporter: {err}");
}
}
});
if !setup_run {
sentry_debug!(
"Warning: MinidumpIntegration was initialized more than once! \
The minidump integration can be initialized at most once per process."
)
}
}
}