sentry_contrib_breakpad/breakpad_integration.rs
1use sentry_core::protocol as proto;
2use std::{path::Path, time::SystemTime};
3
4pub use breakpad_handler::InstallOptions;
5
6/// Monitors the current process for crashes, writing them to disk as minidumps
7/// and reporting the crash event to Sentry.
8pub struct BreakpadIntegration {
9 crash_handler: Option<breakpad_handler::BreakpadHandler>,
10}
11
12impl BreakpadIntegration {
13 /// Creates a new Breakpad Integration, note that only *one* can exist
14 /// in the application at a time!
15 pub fn new(
16 crash_dir: impl AsRef<Path>,
17 install_options: InstallOptions,
18 hub: std::sync::Arc<sentry_core::Hub>,
19 ) -> Result<Self, crate::Error> {
20 // The paths generated by breakpad are just guids with an extension so they
21 // are utf-8 safe, however, due to how we pass the path via metadata
22 // to the transport, we require that the root crash path is also utf-8
23 if crash_dir.as_ref().to_str().is_none() {
24 return Err(crate::Error::NonUtf8Path(crash_dir.as_ref().to_owned()));
25 }
26
27 // Ensure the directory exists, breakpad should do this when writing crashdumps
28 // anyway, but then again, it's C++ code, so I have low trust
29 std::fs::create_dir_all(&crash_dir)?;
30
31 let crash_hub = std::sync::Arc::downgrade(&hub);
32 let crash_handler = breakpad_handler::BreakpadHandler::attach(
33 &crash_dir,
34 install_options,
35 Box::new(move |minidump_path: std::path::PathBuf| {
36 if let Some(crash_hub) = crash_hub.upgrade() {
37 // We **don't** do end_session_with_status as it just
38 // immediately takes the session from the scope and sends it,
39 // but we want to send the event, session update, and minidump
40 // all in the same event
41 // crash_hub.end_session_with_status(protocol::SessionStatus::Crashed);
42
43 let mut extra = std::collections::BTreeMap::new();
44 // We should never get here unless the path is valid utf-8, so this is fine
45 extra.insert(
46 "__breakpad_minidump_path".to_owned(),
47 minidump_path
48 .to_str()
49 .expect("utf-8 path")
50 .to_owned()
51 .into(),
52 );
53
54 // Create an event for crash so that we can add all of the context
55 // we can to it, the important information like stack traces/threads
56 // modules/etc is contained in the minidump recorded by breakpad
57 let event = proto::Event {
58 level: proto::Level::Fatal,
59 // We want to set the timestamp here since we aren't actually
60 // going to send the crash directly, but rather the next time
61 // this integration is initialized
62 timestamp: SystemTime::now(),
63 // This is the easiest way to indicate a session crash update
64 // in the same envelope with the crash itself. :p
65 exception: vec![proto::Exception {
66 mechanism: Some(proto::Mechanism {
67 handled: Some(false),
68 ..Default::default()
69 }),
70 ..Default::default()
71 }]
72 .into(),
73 extra,
74 ..Default::default()
75 };
76
77 crash_hub.capture_event(event);
78
79 if let Some(client) = crash_hub.client() {
80 client.close(None);
81 }
82 }
83 }),
84 )?;
85
86 let crash_dir = crash_dir.as_ref().to_owned();
87
88 Self::upload_minidumps(&crash_dir, &hub);
89
90 Ok(Self {
91 crash_handler: Some(crash_handler),
92 })
93 }
94
95 /// Called during startup to send any minidumps + metadata that have been
96 /// captured in previous sessions but (seem to) have not been sent yet
97 fn upload_minidumps(crash_dir: &Path, hub: &sentry_core::Hub) {
98 // Scan the directory the integration was initialized with to find any
99 // envelopes that have been serialized to disk and send + delete them
100 let rd = match std::fs::read_dir(crash_dir) {
101 Ok(rd) => rd,
102 Err(e) => {
103 debug_print!(
104 "Unable to read crash directory '{}': {}",
105 crash_dir.display(),
106 e
107 );
108 return;
109 }
110 };
111
112 let client = match hub.client() {
113 Some(c) => c,
114 None => return,
115 };
116
117 // The minidumps are what we care about the most, but of course, the
118 // metadata that we (hopefully) were able to capture along with the crash
119 for entry in rd.filter_map(|e| e.ok()) {
120 if entry
121 .file_name()
122 .to_str()
123 .map_or(true, |s| !s.ends_with(".dmp"))
124 {
125 continue;
126 }
127
128 let mut minidump_path = entry.path();
129 minidump_path.set_extension("metadata");
130
131 let md = crate::shared::CrashMetadata::deserialize(&minidump_path);
132 if let Err(e) = std::fs::remove_file(&minidump_path) {
133 debug_print!("failed to remove {}: {}", minidump_path.display(), e);
134 }
135
136 minidump_path.set_extension("dmp");
137
138 let envelope = crate::shared::assemble_envelope(md, &minidump_path);
139 if let Err(e) = std::fs::remove_file(&minidump_path) {
140 debug_print!("failed to remove {}: {}", minidump_path.display(), e);
141 }
142
143 client.send_envelope(envelope);
144 }
145 }
146
147 #[inline]
148 pub fn inner_handler(&self) -> &Option<breakpad_handler::BreakpadHandler> {
149 &self.crash_handler
150 }
151}
152
153impl Drop for BreakpadIntegration {
154 fn drop(&mut self) {
155 let _ = self.crash_handler.take();
156 }
157}