Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
use crate::crash_reporting::VirtualEnvironment;
|
||||
|
||||
/// Returns what virtualized environment Warp is running in, if any.
|
||||
pub fn get_virtualized_environment() -> Option<VirtualEnvironment> {
|
||||
if let Ok(output) = command::blocking::Command::new("systemd-detect-virt").output() {
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
} else {
|
||||
let value = std::str::from_utf8(&output.stdout).ok()?;
|
||||
if value == "none" {
|
||||
return None;
|
||||
}
|
||||
return Some(VirtualEnvironment {
|
||||
name: value.to_owned(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Test specifically for WSL based on existence of a particular file under
|
||||
// /proc.
|
||||
//
|
||||
// See: https://superuser.com/questions/1749781/how-can-i-check-if-the-environment-is-wsl-from-a-shell-script
|
||||
if std::path::Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() {
|
||||
return Some(VirtualEnvironment {
|
||||
name: "wsl".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use cocoa::{
|
||||
base::{id, nil, BOOL},
|
||||
foundation::NSAutoreleasePool,
|
||||
};
|
||||
use warpui::platform::mac::make_nsstring;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Functions implemented in objC files.
|
||||
extern "C" {
|
||||
fn startSentry(sentryUrl: id, environment: id, version: id, isDogfood: BOOL);
|
||||
fn stopSentry();
|
||||
#[allow(dead_code)] // Only gets called when built in debug mode.
|
||||
fn crashSentry();
|
||||
fn setUser(userId: id);
|
||||
fn recordBreadcrumb(message: id, category: id, level: id, seconds_since_epoch: f64);
|
||||
fn setTag(key: id, value: id);
|
||||
}
|
||||
|
||||
pub fn init_cocoa_sentry() {
|
||||
let endpoint = ChannelState::sentry_url();
|
||||
let environment = super::get_environment();
|
||||
|
||||
log::info!("Initializing Sentry for cocoa app with endpoint {endpoint}");
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
let dsn = make_nsstring(endpoint);
|
||||
let environment_name: &str = environment.as_ref();
|
||||
let environment = make_nsstring(environment_name);
|
||||
let release = make_nsstring(release_version());
|
||||
startSentry(
|
||||
dsn,
|
||||
environment,
|
||||
release,
|
||||
ChannelState::channel().is_dogfood() as BOOL,
|
||||
);
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uninit_cocoa_sentry() {
|
||||
unsafe {
|
||||
stopSentry();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crash() {
|
||||
unsafe {
|
||||
crashSentry();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_user_id(user_id: &str) {
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
let user_id = make_nsstring(user_id);
|
||||
setUser(user_id);
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forward_breadcrumb(rust_breadcrumb: &sentry::Breadcrumb) {
|
||||
let message = rust_breadcrumb.message.as_deref().unwrap_or("");
|
||||
let category = rust_breadcrumb.category.as_deref().unwrap_or("");
|
||||
let level = rust_breadcrumb.level.to_string();
|
||||
let unix_timestamp = rust_breadcrumb
|
||||
.timestamp
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.map_or(0., |n| n.as_secs_f64());
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
recordBreadcrumb(
|
||||
make_nsstring(message),
|
||||
make_nsstring(category),
|
||||
make_nsstring(level.as_str()),
|
||||
unix_timestamp,
|
||||
);
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tag(key: &str, value: &str) {
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
setTag(make_nsstring(key), make_nsstring(value));
|
||||
pool.drain();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mod mac;
|
||||
#[cfg(linux_or_windows)]
|
||||
mod sentry_minidump;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use sentry::{ClientInitGuard, IntoDsn, SessionMode};
|
||||
use warp_core::channel::Channel;
|
||||
use warpui::{r#async::block_on, AppContext, SingletonEntity};
|
||||
|
||||
use crate::antivirus::{AntivirusInfo, AntivirusInfoEvent};
|
||||
use crate::auth::anonymous_id::get_or_create_anonymous_id;
|
||||
use crate::auth::{AuthStateProvider, UserUid};
|
||||
use crate::channel::ChannelState;
|
||||
use crate::features::FeatureFlag;
|
||||
use crate::settings::{PrivacySettings, PrivacySettingsChangedEvent};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use regex::Regex;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::sync::Arc;
|
||||
use warpui::rendering::GPUDeviceInfo;
|
||||
use warpui::windowing::state::ApplicationStage;
|
||||
use warpui::windowing::{self, StateEvent, WindowManager};
|
||||
|
||||
#[cfg(linux_or_windows)]
|
||||
pub use sentry_minidump::run_server as run_minidump_server;
|
||||
|
||||
lazy_static! {
|
||||
/// The RAII guard returned by the call to initialize the Rust Sentry client must be kept in
|
||||
/// scope. When it is destroyed, Sentry monitoring ceases.
|
||||
static ref RUST_SENTRY_CLIENT_GUARD: Mutex<RustSentryClientGuard> =
|
||||
Mutex::new(RustSentryClientGuard::Uninitialized);
|
||||
|
||||
/// A map from sensitive error messages to "scrubbed" error messages.
|
||||
static ref ERROR_MESSAGES_TO_SCRUB: Vec<(Regex, &'static str)> = vec![
|
||||
// The following are panic messages for invalid string slicing.
|
||||
// See here for source: https://cs.github.com/rust-lang/rust/blob/9c0bc3028a575eece6d4e8fbc6624cb95b9c9893/library/core/src/str/mod.rs?q=%22byte+index+%22+repo%3Arust-lang%2Frust#L100.
|
||||
(Regex::new(r"byte index .+ is out of bounds.+").unwrap(), "byte index is out of bounds"),
|
||||
(Regex::new(r"byte index .+ is not a char boundary.+").unwrap(), "byte index is not a char boundary"),
|
||||
(Regex::new(r"begin <= end .+ when slicing.+").unwrap(), "begin <= end when slicing"),
|
||||
];
|
||||
|
||||
/// The current [`ApplicationStage`] of the application. Used when reporting the
|
||||
/// `warp.application_stage` tag to Sentry.
|
||||
static ref APPLICATION_LIFECYCLE_STAGE: RwLock<ApplicationStage> = RwLock::new(ApplicationStage::Starting);
|
||||
|
||||
/// The set of tags that we want to attach to all Sentry reports.
|
||||
static ref TAGS: RwLock<HashMap<String, String>> = Default::default();
|
||||
}
|
||||
|
||||
/// Sets a kv-pair as a Sentry tag for the rest of the application's lifetime.
|
||||
pub(crate) fn set_tag<'a, 'b>(key: impl Into<Cow<'a, str>>, value: impl Into<Cow<'b, str>>) {
|
||||
set_tag_internal(key.into(), value.into());
|
||||
}
|
||||
|
||||
/// Non-generic internal implementation of [`set_tag`].
|
||||
fn set_tag_internal(key: Cow<'_, str>, value: Cow<'_, str>) {
|
||||
// Avoid setting tags with empty values, as Sentry doesn't allow them.
|
||||
if value.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::set_tag(key.clone().into_owned(), value.clone().into_owned());
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::set_tag(key.as_ref(), value.as_ref());
|
||||
|
||||
TAGS.write().insert(key.into_owned(), value.into_owned());
|
||||
}
|
||||
|
||||
/// Sets the [`GPUDeviceInfo`] of the last opened window for use in logging as a Sentry tag.
|
||||
pub(crate) fn set_gpu_device_info(gpu_device_info: GPUDeviceInfo) {
|
||||
for (key, value) in gpu_device_info.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`AntivirusInfo`] for use in logging as a Sentry tag.
|
||||
/// Only reports the first detected antivirus product.
|
||||
pub fn set_antivirus_info(antivirus_info: &AntivirusInfo) {
|
||||
for (key, value) in antivirus_info.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current application lifecycle stage, for use in logging as a Sentry tag.
|
||||
fn set_lifecycle_stage(stage: ApplicationStage) {
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::set_tags_from(&stage);
|
||||
|
||||
*APPLICATION_LIFECYCLE_STAGE.write() = stage;
|
||||
}
|
||||
|
||||
/// Sets the detected virtual environment info, for use in logging as a Sentry
|
||||
/// tag.
|
||||
fn set_virtual_environment(env: Option<VirtualEnvironment>) {
|
||||
for (key, value) in env.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_windowing_system(windowing_system: Option<windowing::System>) {
|
||||
for (key, value) in windowing_system.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if crash reporting is currently enabled.
|
||||
fn is_crash_reporting_enabled(ctx: &mut AppContext) -> bool {
|
||||
PrivacySettings::handle(ctx)
|
||||
.as_ref(ctx)
|
||||
.is_crash_reporting_enabled
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct CrashRecoveryMetadata {
|
||||
/// Whether the Sentry event was previously _unhandled_.
|
||||
was_unhandled_event: bool,
|
||||
/// Whether the crash recovery process is currently running, indicating that an unhandled event
|
||||
/// should actually be marked as handled.
|
||||
is_crash_recovery_process_running: bool,
|
||||
}
|
||||
|
||||
impl CrashRecoveryMetadata {
|
||||
#[cfg(enable_crash_recovery)]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
was_unhandled_event: false,
|
||||
is_crash_recovery_process_running:
|
||||
crate::crash_recovery::is_crash_recovery_process_running(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(enable_crash_recovery))]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
was_unhandled_event: false,
|
||||
is_crash_recovery_process_running: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn was_unhandled_event(&mut self) {
|
||||
self.was_unhandled_event = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for CrashRecoveryMetadata {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
#[cfg(enable_crash_recovery)]
|
||||
{
|
||||
/// Converts a `bool` that is meant to be a Sentry value to it's string representation.
|
||||
/// Sentry uses `yes` or `no` as the value for booleans, so we follow that convention.
|
||||
fn bool_to_sentry_value(value: bool) -> String {
|
||||
let sentry_value = if value { "yes" } else { "no" };
|
||||
sentry_value.into()
|
||||
}
|
||||
|
||||
[
|
||||
(
|
||||
"warp.crash_recovery_process.running",
|
||||
bool_to_sentry_value(self.is_crash_recovery_process_running),
|
||||
),
|
||||
(
|
||||
"warp.handled_by_crash_recovery_process",
|
||||
bool_to_sentry_value(self.was_unhandled_event),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(not(enable_crash_recovery))]
|
||||
std::iter::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the crash reporting susbsystem. Returns whether or not crash
|
||||
/// reporting is active.
|
||||
pub(crate) fn init(ctx: &mut AppContext) -> bool {
|
||||
if !FeatureFlag::CrashReporting.is_enabled() {
|
||||
log::info!("Crash reporting FeatureFlag is disabled; not initializing sentry.");
|
||||
return false;
|
||||
}
|
||||
|
||||
let window_manager = WindowManager::handle(ctx);
|
||||
ctx.subscribe_to_model(&window_manager, |_, event, _| match event {
|
||||
StateEvent::ValueChanged { current, previous } => {
|
||||
if current.stage != previous.stage {
|
||||
set_lifecycle_stage(current.stage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let antivirus_info = AntivirusInfo::handle(ctx);
|
||||
ctx.subscribe_to_model(&antivirus_info, |antivirus_info, event, ctx| match event {
|
||||
AntivirusInfoEvent::ScannedComplete => {
|
||||
let antivirus_info = antivirus_info.as_ref(ctx);
|
||||
set_antivirus_info(antivirus_info);
|
||||
}
|
||||
});
|
||||
|
||||
let is_crash_reporting_enabled = is_crash_reporting_enabled(ctx);
|
||||
|
||||
if is_crash_reporting_enabled {
|
||||
AuthStateProvider::handle(ctx).update(ctx, |auth_state_provider, ctx| {
|
||||
init_sentry(
|
||||
auth_state_provider.get().user_id(),
|
||||
auth_state_provider.get().user_email(),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
log::info!("Crash reporting setting is disabled; not initializing sentry.");
|
||||
}
|
||||
|
||||
set_windowing_system(ctx.windows().windowing_system());
|
||||
|
||||
let privacy_settings = PrivacySettings::handle(ctx);
|
||||
ctx.subscribe_to_model(&privacy_settings, |_, event, ctx| {
|
||||
if let &PrivacySettingsChangedEvent::UpdateIsCrashReportingEnabled { new_value, .. } = event
|
||||
{
|
||||
if new_value {
|
||||
AuthStateProvider::handle(ctx).update(ctx, |auth_state_provider, ctx| {
|
||||
init_sentry(
|
||||
auth_state_provider.get().user_id(),
|
||||
auth_state_provider.get().user_email(),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
uninit_sentry();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Having initialized the SDK above, we can now set the initial value of
|
||||
// some tags.
|
||||
set_lifecycle_stage(window_manager.as_ref(ctx).stage());
|
||||
init_virtual_environment_tag(ctx);
|
||||
|
||||
is_crash_reporting_enabled
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum RustSentryClientGuard {
|
||||
#[default]
|
||||
Uninitialized,
|
||||
Initialized {
|
||||
_guard: ClientInitGuard,
|
||||
},
|
||||
}
|
||||
|
||||
/// Returns the environment used when reporting events to Sentry.
|
||||
/// This is the name of the operating system followed by the channel name (i.e. "linux_dev_release").
|
||||
fn get_environment() -> Cow<'static, str> {
|
||||
let channel = ChannelState::channel();
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "macos")] {
|
||||
let operating_system = "mac";
|
||||
} else if #[cfg(target_os = "linux")] {
|
||||
let operating_system = "linux";
|
||||
}
|
||||
else if #[cfg(target_os = "windows")] {
|
||||
let operating_system = "windows";
|
||||
} else {
|
||||
let operating_system = "";
|
||||
}
|
||||
};
|
||||
|
||||
let base_environment_name = match channel {
|
||||
Channel::Stable => "stable_release",
|
||||
Channel::Preview => "preview_release",
|
||||
Channel::Local => "local",
|
||||
Channel::Integration => "integration_test",
|
||||
Channel::Dev => "dev_release",
|
||||
Channel::Oss => "oss_release",
|
||||
};
|
||||
|
||||
if operating_system.is_empty() {
|
||||
base_environment_name.into()
|
||||
} else {
|
||||
format!("{operating_system}_{base_environment_name}").into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes Rust and Cocoa Sentry, which hooks into the panic handler for the rust app and
|
||||
/// uncaught exception handler of the mac runtime, respectively.
|
||||
///
|
||||
/// This must be called from the main thread to capture panics/crashes across the entire
|
||||
/// application.
|
||||
fn init_sentry(user_id: Option<UserUid>, email: Option<String>, ctx: &mut AppContext) {
|
||||
let key = release_version();
|
||||
|
||||
let environment = Some(get_environment());
|
||||
|
||||
log::info!("Initializing crash reporting {environment:?} with tag {key:?}...");
|
||||
|
||||
fn before_breadcrumb(crumb: sentry::Breadcrumb) -> Option<sentry::Breadcrumb> {
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::forward_breadcrumb(crumb.clone());
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::forward_breadcrumb(&crumb);
|
||||
|
||||
Some(crumb)
|
||||
}
|
||||
|
||||
/// We scrub text we send to Sentry so that we don't leak user input into
|
||||
/// crash reports.
|
||||
fn scrub_message(message: &mut String) {
|
||||
for (regex, replacement) in ERROR_MESSAGES_TO_SCRUB.iter() {
|
||||
if regex.is_match(message) {
|
||||
*message = format!("(REDACTED) {replacement}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut sentry_options = sentry_client_options();
|
||||
sentry_options.before_breadcrumb = Some(Arc::new(Box::new(before_breadcrumb)));
|
||||
sentry_options.before_send = Some(Arc::new(move |mut event| {
|
||||
let mut crash_recovery_metadata = CrashRecoveryMetadata::new();
|
||||
|
||||
for exception in event.exception.iter_mut() {
|
||||
exception.value.as_mut().map(scrub_message);
|
||||
|
||||
// If the crash recovery process is running, mark any exception as "handled".
|
||||
// The crash recovery process will attempt to the handle that crash, if
|
||||
// we crash when handling we'll report that as an unhandled event to sentry.
|
||||
if crash_recovery_metadata.is_crash_recovery_process_running {
|
||||
if let Some(mechanism) = exception.mechanism.as_mut() {
|
||||
if let Some(false) = mechanism.handled {
|
||||
crash_recovery_metadata.was_unhandled_event();
|
||||
}
|
||||
|
||||
mechanism.handled = Some(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in APPLICATION_LIFECYCLE_STAGE.read().to_sentry_tags() {
|
||||
event.tags.insert(k.to_string(), v);
|
||||
}
|
||||
for (k, v) in TAGS.read().iter() {
|
||||
event.tags.insert(k.clone(), v.clone());
|
||||
}
|
||||
|
||||
Some(event)
|
||||
}));
|
||||
|
||||
*RUST_SENTRY_CLIENT_GUARD.lock() = RustSentryClientGuard::Initialized {
|
||||
_guard: sentry::init(sentry_options),
|
||||
};
|
||||
|
||||
// Initialize the appropriate native Sentry SDK.
|
||||
#[cfg(enable_crash_recovery)]
|
||||
{
|
||||
use crate::crash_recovery::{is_crash_recovery_process_running, CrashRecovery};
|
||||
|
||||
// If the crash recovery process is running, defer initialization of Sentry native until the
|
||||
// crash recovery process is torn down. Unlike Sentry Rust, we can't easily mark events as
|
||||
// handled before they are sent to Sentry. Instead, we defer initialization to avoid
|
||||
// erroneously reporting crashes when they would be successfully handled by the crash
|
||||
// recovery process.
|
||||
if is_crash_recovery_process_running() {
|
||||
ctx.subscribe_to_model(&CrashRecovery::handle(ctx), |_handle, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
crate::crash_recovery::Event::CrashRecoveryProcessTornDown
|
||||
) {
|
||||
log::info!("Initializing Sentry native");
|
||||
sentry_minidump::init();
|
||||
|
||||
let auth_state_provider = crate::AuthStateProvider::handle(ctx).as_ref(ctx);
|
||||
let auth_state = auth_state_provider.get();
|
||||
let user_id = auth_state.user_id();
|
||||
let email = auth_state.user_email();
|
||||
set_optional_user_information(user_id, email, ctx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sentry_minidump::init()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if FeatureFlag::CocoaSentry.is_enabled() {
|
||||
init_cocoa_sentry();
|
||||
}
|
||||
|
||||
set_optional_user_information(user_id, email, ctx);
|
||||
}
|
||||
|
||||
/// Baseline Sentry client options.
|
||||
fn sentry_client_options() -> sentry::ClientOptions {
|
||||
sentry::ClientOptions {
|
||||
dsn: ChannelState::sentry_url()
|
||||
.into_dsn()
|
||||
.expect("Invalid Sentry DSN"),
|
||||
|
||||
release: Some(release_version().into()),
|
||||
environment: Some(get_environment()),
|
||||
auto_session_tracking: true,
|
||||
session_mode: SessionMode::Application,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Uninitializes sentry, effectively ending reporting on crashes and errors.
|
||||
pub fn uninit_sentry() {
|
||||
// Take the client guard out of the mutex, replacing it with
|
||||
// `Uninitialized`.
|
||||
let client_guard = std::mem::take(RUST_SENTRY_CLIENT_GUARD.lock().deref_mut());
|
||||
if matches!(client_guard, RustSentryClientGuard::Initialized { .. }) {
|
||||
log::info!("Uninitializing crash reporting...");
|
||||
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::uninit();
|
||||
#[cfg(target_os = "macos")]
|
||||
if FeatureFlag::CocoaSentry.is_enabled() {
|
||||
uninit_cocoa_sentry();
|
||||
}
|
||||
|
||||
// Drop the client guard, uninitializing the Sentry Rust SDK.
|
||||
std::mem::drop(client_guard);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes sentry hooking into the uncaught exception handler of the mac runtime
|
||||
/// which allows us to catch errors within obj-c.
|
||||
pub fn init_cocoa_sentry() {
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
{
|
||||
mac::init_cocoa_sentry();
|
||||
|
||||
for (k, v) in TAGS.read().iter() {
|
||||
mac::set_tag(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uninit_cocoa_sentry() {
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::uninit_cocoa_sentry();
|
||||
}
|
||||
|
||||
pub fn crash() {
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::crash();
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::crash();
|
||||
}
|
||||
|
||||
/// Sets the user id if `Some`, otherwise sets the current user ID to be an anonymous ID indicating
|
||||
/// the user hasn't logged in yet.
|
||||
fn set_optional_user_information(
|
||||
user_id: Option<UserUid>,
|
||||
email: Option<String>,
|
||||
ctx: &mut AppContext,
|
||||
) {
|
||||
let user_id = user_id.map(|uid| uid.as_string()).unwrap_or_else(|| {
|
||||
// If the user isn't signed in, set an anonymous ID. This allows us to
|
||||
// compute more accurate crash-free user metrics.
|
||||
let anonymous_id = get_or_create_anonymous_id(ctx);
|
||||
format!("anon.{anonymous_id}")
|
||||
});
|
||||
// Only send along emails if we're on WarpDev.
|
||||
// We try to keep PII out of Sentry as much as possible.
|
||||
let email = if ChannelState::channel() == Channel::Dev {
|
||||
email
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Set user for Rust sentry.
|
||||
sentry::configure_scope(|scope| {
|
||||
scope.set_user(Some(sentry::User {
|
||||
id: Some(user_id.clone()),
|
||||
email,
|
||||
ip_address: None,
|
||||
username: None,
|
||||
other: BTreeMap::new(),
|
||||
}));
|
||||
});
|
||||
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::set_user_id(user_id.as_str());
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::set_user_id(user_id.as_str());
|
||||
}
|
||||
|
||||
pub fn set_user_id(user_id: UserUid, email: Option<String>, ctx: &mut AppContext) {
|
||||
// On macOS, Sentry will error if we try to set a user without initializing the SDK.
|
||||
// If crash reporting was disabled, but the user enables it later, we'll set user info as part of initialization.
|
||||
if matches!(
|
||||
&*RUST_SENTRY_CLIENT_GUARD.lock(),
|
||||
RustSentryClientGuard::Initialized { .. }
|
||||
) {
|
||||
set_optional_user_information(Some(user_id), email, ctx);
|
||||
} else {
|
||||
log::info!("Sentry is not initialized; not setting Sentry user info");
|
||||
}
|
||||
}
|
||||
|
||||
fn release_version() -> &'static str {
|
||||
ChannelState::app_version().unwrap_or("<no tag>")
|
||||
}
|
||||
|
||||
/// Sets the warp.client_type Sentry tag.
|
||||
pub fn set_client_type_tag(client_id: &str) {
|
||||
set_tag("warp.client_type", client_id);
|
||||
}
|
||||
|
||||
/// Initializes the warp.virtual_env Sentry tag group.
|
||||
fn init_virtual_environment_tag(ctx: &mut AppContext) {
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
|
||||
// Compute the virtual environment in a background thread, as we don't want
|
||||
// to block application startup at all.
|
||||
std::thread::spawn(move || {
|
||||
let virt_env = VirtualEnvironment::detect();
|
||||
let _ = block_on(tx.send(virt_env));
|
||||
});
|
||||
// Once we've computed the value, we want to update the primary Sentry hub,
|
||||
// which means calling `set_virtual_environment` from the main thread.
|
||||
ctx.foreground_executor()
|
||||
.spawn(async move {
|
||||
if let Ok(virt_env) = rx.recv().await {
|
||||
set_virtual_environment(virt_env);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Represents a virtualized environment that the operating system is running
|
||||
/// under.
|
||||
#[derive(Clone)]
|
||||
struct VirtualEnvironment {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl VirtualEnvironment {
|
||||
/// Detects the current virtual environment, if any.
|
||||
fn detect() -> Option<Self> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
linux::get_virtualized_environment()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ToSentryTags {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)>;
|
||||
}
|
||||
|
||||
impl ToSentryTags for ApplicationStage {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[("warp.application_stage", self.to_string())]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for GPUDeviceInfo {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[
|
||||
("warp.gpu.device.name", self.device_name.to_string()),
|
||||
("warp.gpu.device.type", self.device_type.to_string()),
|
||||
("warp.gpu.backend", self.backend.to_string()),
|
||||
("warp.gpu.driver.name", self.driver_name.to_string()),
|
||||
("warp.gpu.driver.info", self.driver_info.to_string()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for Option<VirtualEnvironment> {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
let env = self.clone();
|
||||
[(
|
||||
"warp.virtual_env.name",
|
||||
env.map(|env| env.name).unwrap_or_else(|| "none".to_owned()),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for Option<windowing::System> {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[(
|
||||
"warp.window.system",
|
||||
self.as_ref()
|
||||
.map(|windowing_system| windowing_system.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_owned()),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for &AntivirusInfo {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[(
|
||||
"warp.window.antivirus.name",
|
||||
self.get().unwrap_or("none").into(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
//! Native crash reporting adapter that uses the [`minidumper`] crate with Sentry. This allows us
|
||||
//! to capture and report application crashes due to Unix signals like SIGSEGV (segfault)
|
||||
//! or Windows exceptions [https://learn.microsoft.com/en-us/windows/win32/debug/structured-exception-handling].
|
||||
//!
|
||||
//! This is inspired by [`sentry-rust-minidump`](https://github.com/timfish/sentry-rust-minidump),
|
||||
//! with a few important changes:
|
||||
//! * Support for starting and stopping the crash-reporting process, since users can toggle crash reporting at runtime
|
||||
//! * Startup via our command-line parsing, rather than a separate hook
|
||||
//! * Use of anonymous, temporary crash dump files, to ensure they're cleaned up
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{self, Read as _, Seek as _, Write},
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use command::blocking::Command;
|
||||
use crash_handler::{CrashContext, CrashHandler};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use sentry::{
|
||||
protocol::{Attachment, AttachmentType},
|
||||
Breadcrumb, Level,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use warp_core::report_error;
|
||||
|
||||
use super::ToSentryTags;
|
||||
|
||||
lazy_static! {
|
||||
static ref GUARD: Mutex<Option<MinidumpGuard>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
/// The minidump child process will exit if it doesn't receive a message after some time. This
|
||||
/// ensures that if the parent process exits without cleaning it up, the child won't linger
|
||||
/// forever. We ping the child every `PING_INTERVAL` to make sure it doesn't quit while the
|
||||
/// parent (this process) is running.
|
||||
const PING_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Initialize the minidump reporter.
|
||||
pub fn init() {
|
||||
let mut global_guard = GUARD.lock();
|
||||
|
||||
match MinidumpGuard::start() {
|
||||
Ok(guard) => {
|
||||
*global_guard = Some(guard);
|
||||
}
|
||||
Err(err) => {
|
||||
report_error!(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uninitialize the minidump reporter.
|
||||
pub fn uninit() {
|
||||
let maybe_guard = { GUARD.lock().take() };
|
||||
// Ensure we drop the `MinidumpGuard` after releasing the GUARD mutex. If there's an
|
||||
// error stopping the server, we should log it as a Sentry breadcrumb in the Warp
|
||||
// process, but not forward the breadcrumb to the server process.
|
||||
std::mem::drop(maybe_guard);
|
||||
}
|
||||
|
||||
/// Set a tag to include in minidump crash reports.
|
||||
pub fn set_tag(key: String, value: String) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.set_tags(HashMap::from([(key, value)]));
|
||||
}
|
||||
}
|
||||
|
||||
/// Set tags to include in minidump crash reports, using a type that implements [`ToSentryTags`].
|
||||
pub fn set_tags_from<T: ToSentryTags>(tags: &T) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
let tags = tags
|
||||
.to_sentry_tags()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v))
|
||||
.collect();
|
||||
guard.set_tags(tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user id to include in minidump crash reports.
|
||||
pub fn set_user_id(user_id: &str) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.set_user_id(user_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward a breadcrumb to attach to minidump crash reports.
|
||||
pub fn forward_breadcrumb(breadcrumb: Breadcrumb) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.add_breadcrumb(breadcrumb);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a crash report via minidump. On certain platforms, this will produce an error report
|
||||
/// without actually crashing the process.
|
||||
pub fn crash() {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.crash();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle for minidump state that must be kept in scope while crash reporting is enabled.
|
||||
pub struct MinidumpGuard {
|
||||
child: process::Child,
|
||||
client: Arc<minidumper::Client>,
|
||||
crash_handler: CrashHandler,
|
||||
}
|
||||
|
||||
/// Run the minidump server process.
|
||||
pub fn run_server(socket_path: &Path) -> anyhow::Result<()> {
|
||||
// For troubleshooting, attempt to log from the minidump server. There's not much we can really
|
||||
// do if crash reporting fails, so creating the log file itself is best-effort.
|
||||
let log_dir = warp_core::paths::state_dir().join(warp_core::paths::WARP_LOGS_DIR);
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
let log_path = log_dir.join("warp-minidump.log");
|
||||
let log_target = File::create(log_path)
|
||||
.map(|file| env_logger::Target::Pipe(Box::new(file)))
|
||||
.unwrap_or_else(|_| env_logger::Target::Stdout);
|
||||
env_logger::builder()
|
||||
.parse_default_env()
|
||||
.target(log_target)
|
||||
.init();
|
||||
|
||||
let _guard = sentry::init(super::sentry_client_options());
|
||||
|
||||
struct Handler {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
pending_crash_details: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
impl minidumper::ServerHandler for Handler {
|
||||
fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> {
|
||||
// Use an anonymous temporary file for crash dumps. The path isn't used when writing a
|
||||
// dump, so we can use an empty value.
|
||||
let file = tempfile::tempfile()?;
|
||||
Ok((file, PathBuf::new()))
|
||||
}
|
||||
|
||||
fn on_minidump_created(
|
||||
&self,
|
||||
result: Result<minidumper::MinidumpBinary, minidumper::Error>,
|
||||
) -> minidumper::LoopAction {
|
||||
if let Err(ref err) = &result {
|
||||
log::warn!("Unable to create minidump file: {err:#}");
|
||||
}
|
||||
|
||||
let crash_details = self.pending_crash_details.lock().take();
|
||||
send_crash_report(crash_details, result.ok());
|
||||
|
||||
minidumper::LoopAction::Exit
|
||||
}
|
||||
|
||||
fn on_message(&self, _kind: u32, buffer: Vec<u8>) {
|
||||
match bincode::deserialize::<MinidumpCommand>(&buffer) {
|
||||
Ok(MinidumpCommand::Shutdown) => {
|
||||
self.shutdown.store(true, Ordering::Relaxed);
|
||||
}
|
||||
Ok(MinidumpCommand::SetTags { tags }) => {
|
||||
sentry::configure_scope(|scope| {
|
||||
for (key, value) in tags {
|
||||
scope.set_tag(&key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(MinidumpCommand::SetUser { user_id }) => {
|
||||
sentry::configure_scope(|scope| {
|
||||
scope.set_user(Some(sentry::User {
|
||||
id: Some(user_id),
|
||||
..Default::default()
|
||||
}));
|
||||
});
|
||||
}
|
||||
Ok(MinidumpCommand::AddBreadcrumb { breadcrumb }) => {
|
||||
sentry::add_breadcrumb(breadcrumb);
|
||||
}
|
||||
Ok(MinidumpCommand::SetCrashDetails { details }) => {
|
||||
*self.pending_crash_details.lock() = Some(details);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Unable to deserialize minidump command: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let handler = Box::new(Handler {
|
||||
shutdown: shutdown.clone(),
|
||||
pending_crash_details: Default::default(),
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"Starting minidump server listening on {}",
|
||||
socket_path.display()
|
||||
);
|
||||
let result = minidumper::Server::with_name(socket_path)
|
||||
.context("Unable to create minidump server")?
|
||||
.run(handler, &shutdown, Some(2 * PING_INTERVAL))
|
||||
.context("Error running minidump server");
|
||||
if let Err(ref err) = result {
|
||||
log::error!("Error running minidump server: {err:#}");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Uploads a crash report to Sentry, using the current scope.
|
||||
fn send_crash_report(details: Option<String>, dump: Option<minidumper::MinidumpBinary>) {
|
||||
let message = details.as_deref().unwrap_or("Fatal exception");
|
||||
|
||||
let crash_attachment = dump.and_then(|mut dump| {
|
||||
// In most cases, the minidump contents are available in memory. If not, we can read them off disk.
|
||||
let buffer = match dump.contents {
|
||||
Some(buffer) => buffer,
|
||||
None => {
|
||||
dump.file.flush().ok()?;
|
||||
dump.file.rewind().ok()?;
|
||||
let mut buffer = Vec::new();
|
||||
dump.file.read_to_end(&mut buffer).ok()?;
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
Some(Attachment {
|
||||
buffer,
|
||||
filename: "warp-minidump.dmp".to_string(),
|
||||
ty: Some(AttachmentType::Minidump),
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
// Do not use the crash reporting server for process info.
|
||||
scope.remove_extra("event.process");
|
||||
if let Some(attachment) = crash_attachment {
|
||||
scope.add_attachment(attachment);
|
||||
}
|
||||
},
|
||||
|| sentry::capture_message(message, Level::Error),
|
||||
);
|
||||
}
|
||||
|
||||
impl MinidumpGuard {
|
||||
// NOTE: We CANNOT use `report_error`, `report_if_error`, `log`, or similar here. Those
|
||||
// all send information to Sentry, which can deadlock.
|
||||
|
||||
/// Set up minidump-backed crash reporting. This spawns a child process that reports crashes to
|
||||
/// Sentry, and a crash handler which sends crashes to that child process.
|
||||
pub fn start() -> anyhow::Result<Self> {
|
||||
let socket_name = format!("wcr-{}.sock", Uuid::new_v4().simple());
|
||||
let socket_path = if cfg!(target_os = "macos") {
|
||||
// On macOS, the maximum length of a socket path is fairly short, so use the temp directory.
|
||||
std::env::temp_dir().join(socket_name)
|
||||
} else {
|
||||
warp_core::paths::state_dir().join(socket_name)
|
||||
};
|
||||
|
||||
let child =
|
||||
Command::new(std::env::current_exe().context("Unable to get current executable path")?)
|
||||
.arg("minidump-server")
|
||||
.arg(&socket_path)
|
||||
.spawn()
|
||||
.context("Unable to spawn minidump server process")?;
|
||||
|
||||
let client = Arc::new(
|
||||
wait_for_server(socket_path.as_path()).context("Unable to create minidump client")?,
|
||||
);
|
||||
spawn_keepalive_thread(client.clone());
|
||||
|
||||
let client2 = client.clone();
|
||||
|
||||
let crash_handler = CrashHandler::attach(unsafe {
|
||||
crash_handler::make_crash_event(move |crash_context: &CrashContext| {
|
||||
if let Some(details) = format_crash_details(crash_context) {
|
||||
let _ = send_command(
|
||||
client.as_ref(),
|
||||
MinidumpCommand::SetCrashDetails { details },
|
||||
);
|
||||
}
|
||||
|
||||
// Send a ping to the minidump server, ensuring that any messages sent before the
|
||||
// crash event are flushed and processed. This mostly only matters on macOS.
|
||||
let _ = client.ping();
|
||||
|
||||
let dump_result = client.request_dump(crash_context);
|
||||
crash_handler::CrashEventResult::Handled(dump_result.is_ok())
|
||||
})
|
||||
})
|
||||
.context("Failed to attach crash signal handler")?;
|
||||
|
||||
// Ensure that the crash server process can ptrace Warp.
|
||||
#[cfg(target_os = "linux")]
|
||||
crash_handler.set_ptracer(Some(child.id()));
|
||||
|
||||
let guard = MinidumpGuard {
|
||||
child,
|
||||
client: client2,
|
||||
crash_handler,
|
||||
};
|
||||
|
||||
// Forward any existing tags to the minidump server.
|
||||
guard.set_tags(super::TAGS.read().clone());
|
||||
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
/// Send the user id for the minidump server to attach to Sentry events.
|
||||
fn set_user_id(&self, user_id: String) {
|
||||
let _ = send_command(self.client.as_ref(), MinidumpCommand::SetUser { user_id });
|
||||
}
|
||||
|
||||
/// Send tags for the minidump server to attach to Sentry events.
|
||||
fn set_tags(&self, tags: HashMap<String, String>) {
|
||||
let _ = send_command(self.client.as_ref(), MinidumpCommand::SetTags { tags });
|
||||
}
|
||||
|
||||
/// Add a breadcrumb to crash reports produced by the minidump server.
|
||||
fn add_breadcrumb(&self, breadcrumb: Breadcrumb) {
|
||||
let _ = send_command(
|
||||
self.client.as_ref(),
|
||||
MinidumpCommand::AddBreadcrumb { breadcrumb },
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulate a crash.
|
||||
pub fn crash(&self) {
|
||||
#[cfg(target_os = "linux")]
|
||||
self.crash_handler.simulate_signal(libc::SIGSEGV as _);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
self.crash_handler.simulate_exception(None);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MinidumpGuard {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the crash handler will detach it.
|
||||
// We can report errors here, as the minidump handler is no longer active.
|
||||
|
||||
// Send a graceful shutdown command before killing the child process.
|
||||
if let Err(err) = send_command(&self.client, MinidumpCommand::Shutdown) {
|
||||
log::warn!("Unable to send shutdown command to minidump child process: {err:#}");
|
||||
}
|
||||
|
||||
if let Err(err) = self.child.kill() {
|
||||
log::warn!("Unable to kill minidump child process: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the minidump server to start and return a client handle.
|
||||
///
|
||||
/// Creating a [`minidumper::Client`] will fail unless the server has started.
|
||||
fn wait_for_server(socket_path: &Path) -> anyhow::Result<minidumper::Client> {
|
||||
let start = instant::Instant::now();
|
||||
|
||||
let mut last_error = None;
|
||||
while start.elapsed() < Duration::from_secs(1) {
|
||||
match minidumper::Client::with_name(socket_path) {
|
||||
Ok(client) => {
|
||||
return Ok(client);
|
||||
}
|
||||
Err(err) => {
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match last_error {
|
||||
Some(err) => Err(err.into()),
|
||||
None => Err(anyhow::anyhow!("Unable to connect to minidump server")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a thread that periodically pings the minidump server to prevent it from idling out.
|
||||
fn spawn_keepalive_thread(client: Arc<minidumper::Client>) {
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("minidump-keepalive".to_string())
|
||||
.spawn(move || loop {
|
||||
// Assume that if a ping fails, the server was shut down - the only purpose of this thread
|
||||
// is to prevent an idle timeout.
|
||||
if client.ping().is_err() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(PING_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
/// Use `client` to send a command to the minidump server.
|
||||
fn send_command(client: &minidumper::Client, command: MinidumpCommand) -> anyhow::Result<()> {
|
||||
let message = bincode::serialize(&command).context("Failed to serialize minidump command")?;
|
||||
client
|
||||
.send_message(0, message)
|
||||
.context("Failed to send minidump command")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum MinidumpCommand {
|
||||
Shutdown,
|
||||
SetTags { tags: HashMap<String, String> },
|
||||
SetUser { user_id: String },
|
||||
AddBreadcrumb { breadcrumb: Breadcrumb },
|
||||
SetCrashDetails { details: String },
|
||||
}
|
||||
|
||||
/// Format details from a [`CrashContext`] into a Sentry error message. This information should
|
||||
/// already be in the minidump, but it's useful to surface prominently in Sentry.
|
||||
fn format_crash_details(crash_context: &CrashContext) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
Some(format!("Killed by signal {} / {}", crash_context.siginfo.ssi_signo, crash_context.siginfo.ssi_code))
|
||||
} else if #[cfg(target_os = "windows")] {
|
||||
Some(format!("Exception {}", crash_context.exception_code))
|
||||
} else if #[cfg(target_os = "macos")] {
|
||||
crash_context.exception.as_ref().map(|exception| {
|
||||
format!("Exception {} ({} / {:?})", exception.kind, exception.code, exception.subcode)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user