Crash reports: replayable, bounded, local, reachable by agents (#384)
Every panic (in a command, in the kernel, anywhere else) and every unclean exit (abort, stack overflow, kill) leaves a report in a local crash folder: message, file:line, a trimmed backtrace, version, commit, system and renderer, the command and recent commands, and when possible the design and replay.json that reproduce it (solvecraft-cli crashes replay <id>). - Cost: nothing until the app installs the hook (tests, CLI and web never do); then one small journal line per top-level command of the design on screen, taken back with a truncate when the command changed nothing. - Unclean exits: each run holds a lock on its journal; a journal nobody holds at the next start belongs to an app that died. Autosaves restart the journal from the autosaved design. - Bounded: a crash's signature (place + message, numbers ignored) is counted, not rewritten; at most 5 new reports per run, 20 reports, 50 MB, 90 days; recovered kernel panics are pruned first. - Agents: diagnostics.crash_reports / crash_report / crash_reports.clear commands, the MCP crash_reports tool, solvecraft-cli crashes, solvecraft --crash-log; diagnostics.crash_test (with SOLVECRAFT_CRASH_TEST) checks it. - People: the Crash Reports window opens after a crash (Help > Crash Reports…) with a reviewed issue draft per report; nothing is sent. - Release builds keep line tables for file:line in backtraces. Tests: child processes that really panic in a command, in the kernel, on a thread, and abort; dedup counts, limits, hostile folders, journal parsing, the UI window and draft, the MCP tool. docs/crash-reports.md. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
This commit is contained in:
24 files changed
+1998
-9
No files matched your search
@@ -1,6 +1,6 @@
|
||||
//! SolveCraft desktop app.
|
||||
//!
|
||||
//! Usage: `solvecraft [--control <port>] [--sample] [design.solvecraft | part.step]`
|
||||
//! Usage: `solvecraft [--control <port>] [--sample] [--crash-log] [design.solvecraft | part.step]`
|
||||
//!
|
||||
//! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a
|
||||
//! `.3mf`/`.stl` file as a new design holding mesh bodies.
|
||||
@@ -30,6 +30,8 @@ struct App {
|
||||
|
||||
impl eframe::App for App {
|
||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// The design on screen is the one whose commands a crash report replays.
|
||||
self.solve.session.crash_journal = solvecraft_engine::crash::enabled();
|
||||
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
|
||||
// designs first; the window closes once they are answered (`quit_requested`).
|
||||
if ctx.input(|i| i.viewport().close_requested()) && !self.solve.quit_requested {
|
||||
@@ -54,6 +56,7 @@ impl eframe::App for App {
|
||||
}
|
||||
fn on_exit(&mut self) {
|
||||
solvecraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solvecraft_engine::crash::end_session();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,13 +164,37 @@ fn main() -> eframe::Result {
|
||||
println!("solidcraft {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
"--crash-log" => {
|
||||
match solvecraft_engine::crash::default_dir() {
|
||||
Some(dir) => {
|
||||
println!("crash reports: {}", dir.display());
|
||||
for r in solvecraft_engine::crash::list(&dir).iter().take(5) {
|
||||
println!(" {} {} {}", r.id, r.kind, r.message);
|
||||
}
|
||||
println!("more: solidcraft-cli crashes");
|
||||
}
|
||||
None => println!("no crash folder on this system"),
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("usage: solidcraft [--control PORT] [--sample] [design.solvecraft | part.step]");
|
||||
println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [design.solvecraft | part.step]");
|
||||
return Ok(());
|
||||
}
|
||||
_ => files.push(a),
|
||||
}
|
||||
}
|
||||
// Crashes leave a report in the crash folder (solvecraft_engine::crash, docs/crash-reports.md).
|
||||
let crashed = solvecraft_engine::crash::default_dir()
|
||||
.map(|dir| {
|
||||
let build = solvecraft_engine::crash::Build {
|
||||
version: solvecraft_ui_egui::help::VERSION,
|
||||
commit: solvecraft_ui_egui::help::COMMIT,
|
||||
built: solvecraft_ui_egui::help::BUILD_DATE,
|
||||
};
|
||||
solvecraft_engine::crash::install(dir, build)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: app_viewport()?,
|
||||
depth_buffer: DEPTH_BITS,
|
||||
@@ -205,6 +232,11 @@ fn main() -> eframe::Result {
|
||||
app.set_wgpu(rs, DEPTH_BITS, STENCIL_BITS, rs.msaa_samples);
|
||||
// A saved anti-aliasing the device can't do: back to the default, with a notice.
|
||||
solvecraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
|
||||
let info = rs.adapter.get_info();
|
||||
solvecraft_engine::crash::set_environment(&format!(
|
||||
"Renderer: {:?} {} ({}), MSAA {}x",
|
||||
info.backend, info.name, info.driver, rs.msaa_samples
|
||||
));
|
||||
}
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
@@ -232,6 +264,8 @@ fn main() -> eframe::Result {
|
||||
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
|
||||
solvecraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
|
||||
}
|
||||
// Tell the user about crashes since the last start (after the recovery offer is set up).
|
||||
solvecraft_ui_egui::crash_ui::announce(&mut app, &crashed);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||
Ok(Box::new(App {
|
||||
|
||||
Reference in new issue
Block a user