1const DEFAULT_CRASH_LOG_MAX_FILES: usize = 20;
9
10fn crash_log_dir() -> std::path::PathBuf {
12 let home = std::env::var_os("HOME")
15 .map(std::path::PathBuf::from)
16 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
17 home.join(".futu-opend-rs").join("crashes")
18}
19
20pub fn write_crash_log_file(info: &std::panic::PanicHookInfo<'_>) {
25 let dir = crash_log_dir();
26 if let Err(err) = std::fs::create_dir_all(&dir) {
27 eprintln!(
28 "futu-opend: failed to create crash log directory {}: {}",
29 dir.display(),
30 err
31 );
32 }
33
34 let now_secs = crash_log_unix_secs_or_zero();
39 let path = dir.join(format!("crash-{now_secs}.log"));
40
41 let location = info
42 .location()
43 .map(|l| format!("{}:{}", l.file(), l.line()))
44 .unwrap_or_else(|| "<unknown>".to_string());
45 let payload = info
46 .payload()
47 .downcast_ref::<&str>()
48 .copied()
49 .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.as_str()))
50 .unwrap_or("<non-string panic payload>");
51 let thread = std::thread::current()
52 .name()
53 .unwrap_or("<unnamed>")
54 .to_string();
55 let backtrace = std::backtrace::Backtrace::force_capture();
60
61 let body = format!(
62 "v1.4.97 P1-D-D crash report\n\
63 daemon_version: {}\n\
64 unix_timestamp: {}\n\
65 os: {}\n\
66 arch: {}\n\
67 thread: {}\n\
68 location: {}\n\
69 payload: {}\n\
70 backtrace:\n{}\n",
71 env!("CARGO_PKG_VERSION"),
72 now_secs,
73 std::env::consts::OS,
74 std::env::consts::ARCH,
75 thread,
76 location,
77 payload,
78 backtrace,
79 );
80 match std::fs::write(&path, body) {
81 Ok(()) => prune_old_crash_logs(&dir, DEFAULT_CRASH_LOG_MAX_FILES),
82 Err(err) => {
83 eprintln!(
84 "futu-opend: failed to write crash log {}: {}",
85 path.display(),
86 err
87 );
88 }
89 }
90}
91
92fn prune_old_crash_logs(dir: &std::path::Path, max_files: usize) {
93 if max_files == 0 {
94 return;
95 }
96 let Ok(entries) = std::fs::read_dir(dir) else {
97 return;
98 };
99 let mut crash_logs = Vec::new();
100 for entry in entries.flatten() {
101 let path = entry.path();
102 if !path
103 .file_name()
104 .and_then(|s| s.to_str())
105 .is_some_and(|s| s.starts_with("crash-") && s.ends_with(".log"))
106 {
107 continue;
108 }
109 let Ok(file_type) = entry.file_type() else {
110 continue;
111 };
112 if !file_type.is_file() {
113 continue;
114 }
115 let modified = entry
116 .metadata()
117 .and_then(|meta| meta.modified())
118 .unwrap_or(std::time::UNIX_EPOCH);
119 crash_logs.push((modified, path));
120 }
121
122 if crash_logs.len() <= max_files {
123 return;
124 }
125 crash_logs.sort_by(|(a_time, a_path), (b_time, b_path)| {
126 a_time.cmp(b_time).then_with(|| a_path.cmp(b_path))
127 });
128 let remove_count = crash_logs.len() - max_files;
129 for (_, path) in crash_logs.into_iter().take(remove_count) {
130 if let Err(err) = std::fs::remove_file(&path) {
131 eprintln!(
132 "futu-opend: failed to prune old crash log {}: {}",
133 path.display(),
134 err
135 );
136 }
137 }
138}
139
140fn crash_log_unix_secs_or_zero() -> u64 {
141 match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
142 Ok(elapsed) => elapsed.as_secs(),
143 Err(err) => {
144 eprintln!(
145 "futu-opend: system wall clock is before UNIX_EPOCH while writing crash log; \
146 using zero timestamp fallback: {err}"
147 );
148 0
149 }
150 }
151}
152
153pub fn warn_if_previous_crash() {
157 let dir = crash_log_dir();
158 let Ok(entries) = std::fs::read_dir(&dir) else {
159 return;
160 };
161 let mut latest: Option<(std::time::SystemTime, std::path::PathBuf)> = None;
163 for entry in entries.flatten() {
164 let path = entry.path();
165 if !path
166 .file_name()
167 .and_then(|s| s.to_str())
168 .is_some_and(|s| s.starts_with("crash-") && s.ends_with(".log"))
169 {
170 continue;
171 }
172 let Ok(meta) = entry.metadata() else { continue };
173 let Ok(mtime) = meta.modified() else { continue };
174 if latest.as_ref().is_none_or(|(t, _)| mtime > *t) {
175 latest = Some((mtime, path));
176 }
177 }
178 if let Some((_, path)) = latest {
179 eprintln!(
181 "⚠️ v1.4.97 P1-D-D: previous crash log detected at {}",
182 path.display()
183 );
184 eprintln!(" inspect for last-known panic payload + backtrace.");
185 }
186}
187
188#[cfg(test)]
189mod tests;