concinnity_engine/crash/
mod.rs1mod hook;
12mod memory;
13mod report;
14mod ring;
15mod write;
16
17#[cfg(any(target_os = "macos", target_os = "windows"))]
18mod minidump;
19#[cfg(any(target_os = "macos", target_os = "windows"))]
20mod native;
21
22pub use ring::RingLayer;
23
24use std::path::{Path, PathBuf};
25use std::sync::Mutex;
26
27static REPORT_DIR: Mutex<Option<PathBuf>> = Mutex::new(None);
31
32pub fn install(dir: Option<&Path>) {
42 *REPORT_DIR.lock().unwrap_or_else(|p| p.into_inner()) = dir.map(Path::to_path_buf);
43 hook::install();
44 #[cfg(any(target_os = "macos", target_os = "windows"))]
45 native::install();
46 #[cfg(backend_metal)]
47 note("backend", "metal");
48 #[cfg(backend_dx)]
49 note("backend", "directx");
50 #[cfg(backend_vk)]
51 note("backend", "vulkan");
52 #[cfg(not(any(backend_metal, backend_dx, backend_vk)))]
53 note("backend", "none");
54}
55
56const MAX_NOTES: usize = 16;
57const MAX_NOTE_KEY_BYTES: usize = 32;
58const MAX_NOTE_VALUE_BYTES: usize = 128;
59
60static NOTES: Mutex<Vec<(String, String)>> = Mutex::new(Vec::new());
63
64pub fn note(key: &str, value: &str) {
68 let key = clamp(key, MAX_NOTE_KEY_BYTES);
69 let value = clamp(value, MAX_NOTE_VALUE_BYTES);
70 let mut notes = NOTES.lock().unwrap_or_else(|p| p.into_inner());
71 if let Some(slot) = notes.iter_mut().find(|(k, _)| *k == key) {
72 slot.1 = value;
73 } else if notes.len() < MAX_NOTES {
74 notes.push((key, value));
75 }
76}
77
78pub(crate) fn report_device_lost(detail: &str) {
82 let report = report::CrashReport::gather(report::ReportKind::DeviceLost, detail.to_string());
83 if write::emit(&report).is_none() {
84 tracing::warn!("crash report for device loss could not be written");
85 }
86}
87
88pub(crate) fn report_dir() -> Option<PathBuf> {
90 REPORT_DIR.lock().ok()?.clone()
91}
92
93pub(crate) fn notes_snapshot() -> Vec<(String, String)> {
94 NOTES.lock().unwrap_or_else(|p| p.into_inner()).clone()
95}
96
97#[cfg(any(target_os = "macos", target_os = "windows"))]
99pub(crate) fn try_notes_snapshot() -> Vec<(String, String)> {
100 match NOTES.try_lock() {
101 Ok(notes) => notes.clone(),
102 Err(_) => Vec::new(),
103 }
104}
105
106fn clamp(s: &str, cap: usize) -> String {
107 let mut end = s.len().min(cap);
108 while !s.is_char_boundary(end) {
109 end -= 1;
110 }
111 s[..end].to_string()
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
121 fn notes_replace_by_key_and_stay_bounded() {
122 note("probe-key", "first");
123 note("probe-key", "second");
124 let snap = notes_snapshot();
125 assert_eq!(snap.iter().filter(|(k, _)| k == "probe-key").count(), 1);
126 assert!(snap.contains(&("probe-key".to_string(), "second".to_string())));
127
128 note("probe-long", &"v".repeat(1000));
129 let snap = notes_snapshot();
130 let long = snap.iter().find(|(k, _)| k == "probe-long").unwrap();
131 assert_eq!(long.1.len(), MAX_NOTE_VALUE_BYTES);
132
133 for i in 0..2 * MAX_NOTES {
134 note(&format!("probe-fill-{i}"), "x");
135 }
136 assert!(notes_snapshot().len() <= MAX_NOTES);
137 }
138}