Skip to main content

futu_opend/
crash_log.rs

1//! v1.4.110 P1-2: crash log + panic hook helpers
2//!
3//! 抽自 main.rs lines 20-123 (v1.4.97 P1-D-D 实装).
4//! Aligned with C++ NNCrashCenter pattern.
5
6/// v1.4.97 P1-D-D: directory holding dated crash logs.
7fn crash_log_dir() -> std::path::PathBuf {
8    // Reuse same `~/.futu-opend-rs/` root as credentials/keys.json (CLAUDE.md
9    // §登录 device_id 生命周期). Don't introduce new XDG / dirs crate dep.
10    let home = std::env::var_os("HOME")
11        .map(std::path::PathBuf::from)
12        .unwrap_or_else(|| std::path::PathBuf::from("/tmp"));
13    home.join(".futu-opend-rs").join("crashes")
14}
15
16/// v1.4.97 P1-D-D: synchronously write a crash log file.
17///
18/// Best-effort: if mkdir/write fails, emit a small stderr diagnostic without
19/// the panic payload. Panic in panic-hook is never recoverable.
20pub fn write_crash_log_file(info: &std::panic::PanicHookInfo<'_>) {
21    let dir = crash_log_dir();
22    if let Err(err) = std::fs::create_dir_all(&dir) {
23        eprintln!(
24            "futu-opend: failed to create crash log directory {}: {}",
25            dir.display(),
26            err
27        );
28    }
29
30    // dated filename `{YYYYMMDDhhmmss}.log` aligns with C++ NNCrashCenter
31    // (NNCrashCenter.cpp:6-54). Avoid chrono dep here — sync formatting via
32    // std::time::SystemTime + manual local-broken-down-time would require
33    // libc::localtime_r unsafe; prefer simple unix epoch + version + thread.
34    let now_secs = crash_log_unix_secs_or_zero();
35    let path = dir.join(format!("crash-{now_secs}.log"));
36
37    let location = info
38        .location()
39        .map(|l| format!("{}:{}", l.file(), l.line()))
40        .unwrap_or_else(|| "<unknown>".to_string());
41    let payload = info
42        .payload()
43        .downcast_ref::<&str>()
44        .copied()
45        .or_else(|| info.payload().downcast_ref::<String>().map(|s| s.as_str()))
46        .unwrap_or("<non-string panic payload>");
47    let thread = std::thread::current()
48        .name()
49        .unwrap_or("<unnamed>")
50        .to_string();
51    // v1.4.104 external reviewer OBS-P3-001 fix: force capture (不依赖 RUST_BACKTRACE env).
52    // 之前 `Backtrace::capture()` 只在 RUST_BACKTRACE=1 时返实 backtrace, 否则
53    // "disabled backtrace" 占位 → debug 价值大减. force_capture() 总返实 stack.
54    // crash 路径已是 "已经挂了" panic, 多花几 ms 抓 stack 是值得的.
55    let backtrace = std::backtrace::Backtrace::force_capture();
56
57    let body = format!(
58        "v1.4.97 P1-D-D crash report\n\
59         daemon_version: {}\n\
60         unix_timestamp: {}\n\
61         os: {}\n\
62         arch: {}\n\
63         thread: {}\n\
64         location: {}\n\
65         payload: {}\n\
66         backtrace:\n{}\n",
67        env!("CARGO_PKG_VERSION"),
68        now_secs,
69        std::env::consts::OS,
70        std::env::consts::ARCH,
71        thread,
72        location,
73        payload,
74        backtrace,
75    );
76    if let Err(err) = std::fs::write(&path, body) {
77        eprintln!(
78            "futu-opend: failed to write crash log {}: {}",
79            path.display(),
80            err
81        );
82    }
83}
84
85fn crash_log_unix_secs_or_zero() -> u64 {
86    match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
87        Ok(elapsed) => elapsed.as_secs(),
88        Err(err) => {
89            eprintln!(
90                "futu-opend: system wall clock is before UNIX_EPOCH while writing crash log; \
91                 using zero timestamp fallback: {err}"
92            );
93            0
94        }
95    }
96}
97
98/// v1.4.97 P1-D-D: at startup, scan crash dir for previous crashes, emit a
99/// WARN log with the latest filename so ops sees "daemon previously crashed
100/// at <ts>" without manual file inspection. Best-effort, silent on errors.
101pub fn warn_if_previous_crash() {
102    let dir = crash_log_dir();
103    let Ok(entries) = std::fs::read_dir(&dir) else {
104        return;
105    };
106    // Find latest crash-*.log by mtime
107    let mut latest: Option<(std::time::SystemTime, std::path::PathBuf)> = None;
108    for entry in entries.flatten() {
109        let path = entry.path();
110        if !path
111            .file_name()
112            .and_then(|s| s.to_str())
113            .is_some_and(|s| s.starts_with("crash-") && s.ends_with(".log"))
114        {
115            continue;
116        }
117        let Ok(meta) = entry.metadata() else { continue };
118        let Ok(mtime) = meta.modified() else { continue };
119        if latest.as_ref().is_none_or(|(t, _)| mtime > *t) {
120            latest = Some((mtime, path));
121        }
122    }
123    if let Some((_, path)) = latest {
124        // Use eprintln since this runs before tracing init.
125        eprintln!(
126            "⚠️  v1.4.97 P1-D-D: previous crash log detected at {}",
127            path.display()
128        );
129        eprintln!("    inspect for last-known panic payload + backtrace.");
130    }
131}
132
133#[cfg(test)]
134mod tests;