libdd_crashtracker/crash_info/
error_data.rs1use super::stacktrace::StackTrace;
4#[cfg(unix)]
5use alloc::rc::Rc;
6#[cfg(unix)]
7use blazesym::helper::ElfResolver;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10#[cfg(unix)]
11use std::collections::HashMap;
12#[cfg(unix)]
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
16pub struct ErrorData {
17 pub is_crash: bool,
18 pub kind: ErrorKind,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub message: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub thread_name: Option<String>,
23 pub source_type: SourceType,
24 pub stack: StackTrace,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub threads: Option<Vec<ThreadData>>,
27}
28
29#[cfg(unix)]
30pub struct CachedElfResolvers<'a> {
31 symbolizer: &'a mut blazesym::symbolize::Symbolizer,
32 elf_resolvers: HashMap<PathBuf, Rc<ElfResolver>>,
33}
34
35#[cfg(unix)]
36impl<'a> CachedElfResolvers<'a> {
37 pub fn new(symbolizer: &'a mut blazesym::symbolize::Symbolizer) -> Self {
38 Self {
39 symbolizer,
40 elf_resolvers: HashMap::new(),
41 }
42 }
43
44 pub fn get_or_insert(&mut self, file_path: &PathBuf) -> anyhow::Result<Rc<ElfResolver>> {
45 use anyhow::Context;
46 let entry = self.elf_resolvers.entry(file_path.clone());
47
48 match entry {
49 std::collections::hash_map::Entry::Occupied(o) => Ok(o.get().clone()),
50 std::collections::hash_map::Entry::Vacant(v) => {
51 let resolver = ElfResolver::open(file_path).with_context(|| {
52 format!(
53 "ElfResolver::open failed for '{}'",
54 file_path.to_string_lossy()
55 )
56 });
57
58 match resolver {
59 Ok(resolver) => {
60 let resolver = Rc::new(resolver);
61 let _ = self
64 .symbolizer
65 .register_elf_resolver(file_path.as_path(), Rc::clone(&resolver));
66 v.insert(Rc::clone(&resolver));
67 Ok(resolver)
68 }
69 Err(e) => Err(e),
70 }
71 }
72 }
73 }
74}
75
76#[cfg(unix)]
77impl ErrorData {
78 pub fn normalize_ips(&mut self, pid: u32) -> anyhow::Result<()> {
79 let mut symbolizer = blazesym::symbolize::Symbolizer::new();
80 let mut elf_resolvers = CachedElfResolvers::new(&mut symbolizer);
81 let normalizer = blazesym::normalize::Normalizer::builder()
82 .enable_vma_caching(true)
83 .enable_build_ids(true)
84 .enable_build_id_caching(true)
85 .build();
86 self.normalize_ips_impl(pid, &normalizer, &mut elf_resolvers)
87 }
88
89 pub(crate) fn normalize_ips_impl(
90 &mut self,
91 pid: u32,
92 normalizer: &blazesym::normalize::Normalizer,
93 elf_resolvers: &mut CachedElfResolvers,
94 ) -> anyhow::Result<()> {
95 let mut errors = 0;
96 let pid = pid.into();
97 self.stack
98 .normalize_ips(normalizer, pid, elf_resolvers)
99 .unwrap_or_else(|_| errors += 1);
100
101 if let Some(threads) = &mut self.threads {
102 for thread in threads {
103 thread
104 .stack
105 .normalize_ips(normalizer, pid, elf_resolvers)
106 .unwrap_or_else(|_| errors += 1);
107 }
108 }
109 anyhow::ensure!(
110 errors == 0,
111 "Failed to normalize ips, see frame comments for details"
112 );
113 Ok(())
114 }
115
116 pub(crate) fn create_symbolizer_source<'a>(
117 pid: u32,
118 ) -> blazesym::symbolize::source::Source<'a> {
119 let mut process = blazesym::symbolize::source::Process::new(pid.into());
120 process.map_files = false;
122 blazesym::symbolize::source::Source::Process(process)
123 }
124
125 pub(crate) fn create_normalizer() -> blazesym::normalize::Normalizer {
126 blazesym::normalize::Normalizer::builder()
127 .enable_vma_caching(true)
128 .enable_build_ids(true)
129 .enable_build_id_caching(true)
130 .build()
131 }
132
133 pub fn resolve_names(&mut self, pid: u32) -> anyhow::Result<()> {
134 let src = Self::create_symbolizer_source(pid);
135 let symbolizer = blazesym::symbolize::Symbolizer::new();
136 self.resolve_names_impl(&symbolizer, &src)
137 }
138
139 pub(crate) fn resolve_names_impl(
140 &mut self,
141 symbolizer: &blazesym::symbolize::Symbolizer,
142 src: &blazesym::symbolize::source::Source,
143 ) -> anyhow::Result<()> {
144 let mut errors = 0;
145 self.stack
146 .resolve_names(src, symbolizer)
147 .unwrap_or_else(|_| errors += 1);
148
149 if let Some(threads) = &mut self.threads {
150 for thread in threads {
151 thread
152 .stack
153 .resolve_names(src, symbolizer)
154 .unwrap_or_else(|_| errors += 1);
155 }
156 }
157 anyhow::ensure!(
158 errors == 0,
159 "Failed to resolve names, see frame comments for details"
160 );
161 Ok(())
162 }
163}
164
165impl ErrorData {
166 pub fn demangle_names(&mut self) -> anyhow::Result<()> {
167 let mut errors = 0;
168 self.stack.demangle_names().unwrap_or_else(|_| errors += 1);
169 if let Some(threads) = &mut self.threads {
170 for thread in threads {
171 thread
172 .stack
173 .demangle_names()
174 .unwrap_or_else(|_| errors += 1);
175 }
176 }
177 anyhow::ensure!(
178 errors == 0,
179 "Failed to demangle names, see frame comments for details"
180 );
181 Ok(())
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
186pub enum SourceType {
187 Crashtracking,
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
191#[repr(C)]
192pub enum ErrorKind {
193 Panic,
194 UnhandledException,
195 UnixSignal,
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
199pub struct ThreadData {
200 pub crashed: bool,
201 pub name: String,
202 pub stack: StackTrace,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub state: Option<String>,
205}
206
207#[cfg(test)]
208impl super::test_utils::TestInstance for ErrorData {
209 fn test_instance(seed: u64) -> Self {
210 Self {
211 is_crash: true,
212 kind: ErrorKind::UnixSignal,
213 message: Some(format!("Test crash message for seed {seed}")),
214 thread_name: Some(format!("test-thread-{seed}")),
215 source_type: SourceType::Crashtracking,
216 stack: StackTrace::test_instance(seed),
217 threads: None,
218 }
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::crash_info::test_utils::TestInstance;
226
227 #[test]
228 fn error_data_without_threads_omits_threads_field() {
229 let error = ErrorData {
230 is_crash: true,
231 kind: ErrorKind::UnixSignal,
232 message: None,
233 thread_name: None,
234 source_type: SourceType::Crashtracking,
235 stack: StackTrace::missing(),
236 threads: None,
237 };
238 let json = serde_json::to_value(&error).unwrap();
239 assert!(json.get("threads").is_none());
240 }
241
242 #[test]
243 fn error_data_threads_serializes_as_thread_array() {
244 let thread = ThreadData {
245 crashed: false,
246 name: "worker".to_string(),
247 stack: StackTrace::test_instance(1),
248 state: Some("S".to_string()),
249 };
250 let error = ErrorData {
251 is_crash: true,
252 kind: ErrorKind::UnixSignal,
253 message: None,
254 thread_name: None,
255 source_type: SourceType::Crashtracking,
256 stack: StackTrace::missing(),
257 threads: Some(vec![thread.clone()]),
258 };
259 let json = serde_json::to_value(&error).unwrap();
260 let threads = json["threads"].as_array().unwrap();
261 assert_eq!(threads.len(), 1);
262 assert_eq!(threads[0]["crashed"], false);
263 assert_eq!(threads[0]["name"], "worker");
264 assert_eq!(threads[0]["state"], "S");
265 assert!(threads[0]["stack"].is_object());
266 }
267}