1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Fix gif crash and add debug logging

This commit is contained in:
lucasmerlin
2026-04-20 19:17:28 +02:00
parent e55a691305
commit 92b127c688
4 changed files with 87 additions and 24 deletions

View File

@@ -2532,6 +2532,7 @@ name = "kittest_inspector"
version = "0.34.1"
dependencies = [
"accesskit",
"arboard",
"bincode 2.0.1",
"eframe",
"egui",

View File

@@ -70,10 +70,15 @@ impl Inspector {
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("kittest_inspector"));
// Important: do NOT inherit stderr. The cargo-test / nextest stderr capture pipe
// can close between tests while the inspector is still alive; a later `eprintln!`
// in the inspector would then panic ("failed printing to stderr: Broken pipe") and
// take the window down. The inspector keeps its own log file at
// `{temp}/kittest_inspector.log` for diagnostics.
let mut child = Command::new(&bin)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.stderr(Stdio::null())
.spawn()
.map_err(InspectorError::Launch)?;

View File

@@ -25,7 +25,7 @@ required-features = ["app"]
default = ["app"]
## Build the eframe inspector binary.
app = ["dep:eframe", "dep:egui_extras", "dep:fs4", "dep:image"]
app = ["dep:eframe", "dep:egui_extras", "dep:fs4", "dep:image", "dep:arboard"]
[dependencies]
accesskit = { workspace = true, features = ["serde"] }
@@ -38,6 +38,9 @@ eframe = { workspace = true, features = ["default_fonts", "wgpu"], optional = tr
egui_extras = { workspace = true, features = ["image"], optional = true }
fs4 = { workspace = true, optional = true }
image = { workspace = true, optional = true, features = ["gif"] }
# Cross-platform clipboard — same crate eframe uses — with `set_file_list` so apps that
# accept pasted files (Slack, Discord, Finder, etc.) attach the GIF with animation intact.
arboard = { workspace = true, optional = true }
[lints]
workspace = true

View File

@@ -4,8 +4,6 @@
//! Communication is over stdin/stdout: the harness pipes [`HarnessMessage`]s into our stdin
//! and reads [`InspectorReply`]s from our stdout. All logging goes to stderr.
#![expect(clippy::print_stderr)] // The inspector binary's only logging channel is stderr.
use std::io::{self, BufReader, BufWriter};
use std::sync::mpsc;
use std::thread;
@@ -47,6 +45,15 @@ impl SkipState {
}
fn main() -> eframe::Result<()> {
// Install a panic hook that writes to our own log file (not the inherited — and
// potentially captured — stderr of the harness). Whatever the main thread or a spawned
// worker does, we always get a breadcrumb on disk.
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
log_diag(&format!("PANIC: {info}"));
default(info);
}));
// Cross-process single-instance guard. If another inspector is already running, block
// here until that window closes. Held for the lifetime of `_lock`; the OS releases the
// flock when the file descriptor is dropped on exit.
@@ -91,7 +98,7 @@ fn run_io(ui_tx: &mpsc::Sender<WorkerEvent>, release_rx: &ReleaseRx) {
return;
};
if let Err(err) = write_message(&mut writer, &InspectorReply::Continue { events }) {
eprintln!("kittest_inspector: write failed: {err}");
log_diag(&format!("write failed: {err}"));
return;
}
}
@@ -101,7 +108,7 @@ fn run_io(ui_tx: &mpsc::Sender<WorkerEvent>, release_rx: &ReleaseRx) {
}
Err(err) => {
if err.kind() != io::ErrorKind::UnexpectedEof {
eprintln!("kittest_inspector: read failed: {err}");
log_diag(&format!("read failed: {err}"));
}
let _ = ui_tx.send(WorkerEvent::Disconnected);
return;
@@ -423,20 +430,27 @@ fn controls_panel(app: &mut InspectorApp, ui: &mut egui::Ui) {
if ui
.add_enabled(total > 0, egui::Button::new("📋 Copy as GIF"))
.on_hover_text(
"Encode the whole history as a GIF, write it to the system temp dir, \
and copy the resulting path to the clipboard.",
"Encode the whole history as a GIF and put it on the system clipboard \
as a file reference — paste into Slack / Discord / Finder etc.",
)
.clicked()
{
let message = match save_history_as_gif(&app.history, 10.0) {
Ok(path) => {
ui.ctx().copy_text(path.display().to_string());
format!("Copied path to clipboard: {}", path.display())
}
Err(err) => format!("Failed to save GIF: {err}"),
};
eprintln!("kittest_inspector: {message}");
app.status_message = Some(message);
log_diag("Copy as GIF clicked");
// Run the copy on a detached worker so a slow encode doesn't stall the UI.
// (We can't `catch_unwind` under `panic = abort`, but the panic hook still
// logs what happened, and the real broken-pipe cause is fixed upstream.)
let history = app.history.clone();
let _ = std::thread::Builder::new()
.name("kittest_inspector_copy_gif".into())
.spawn(move || match copy_history_as_gif(&history, 10.0) {
Ok(path) => log_diag(&format!(
"Copied GIF to clipboard: {}",
path.display()
)),
Err(err) => log_diag(&format!("Failed to copy GIF: {err}")),
});
app.status_message =
Some("Encoding + copying GIF on background thread — see log".into());
}
if let Some(msg) = app.status_message.as_deref() {
ui.weak(msg);
@@ -1023,9 +1037,11 @@ fn widget_details(ui: &mut egui::Ui, id: NodeId, node: &Node) {
}
/// Encode the entire history as a looping GIF, write it to a timestamped file in the system
/// temp dir, and return the path. Mirrors the recorder's GIF behaviour: animation plays at
/// `frame_rate`, last frame held for one second so the loop point is obvious.
fn save_history_as_gif(
/// temp dir, and put a *file reference* for that path onto the system clipboard via arboard.
/// Pasting into Slack / Discord / GitHub / Finder etc. attaches the GIF with animation intact.
/// Mirrors the recorder's GIF behaviour: animation plays at `frame_rate`, last frame held
/// for one second so the loop point is obvious.
fn copy_history_as_gif(
history: &[Frame],
frame_rate: f32,
) -> Result<std::path::PathBuf, String> {
@@ -1034,6 +1050,10 @@ fn save_history_as_gif(
if history.is_empty() {
return Err("history is empty".into());
}
log_diag(&format!(
"encoding {} frame(s) @ {frame_rate} fps",
history.len()
));
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -1042,6 +1062,7 @@ fn save_history_as_gif(
// Stable-across-processes temp path is fine here: each invocation wants a fresh file.
#[expect(clippy::disallowed_methods)]
let path = std::env::temp_dir().join(format!("kittest_inspector_{ts}.gif"));
log_diag(&format!("writing to {}", path.display()));
let file = std::fs::File::create(&path)
.map_err(|err| format!("couldn't create {}: {err}", path.display()))?;
@@ -1078,6 +1099,18 @@ fn save_history_as_gif(
.encode_frame(anim_frame)
.map_err(|err| format!("encode frame {i}: {err}"))?;
}
// Finalise the GIF write before handing the path to the clipboard.
drop(encoder);
log_diag("GIF encoded, opening clipboard…");
let mut clipboard =
arboard::Clipboard::new().map_err(|err| format!("open clipboard: {err}"))?;
log_diag("clipboard opened, setting file_list…");
clipboard
.set()
.file_list(&[&path])
.map_err(|err| format!("set clipboard file list: {err}"))?;
log_diag("clipboard file_list set");
Ok(path)
}
@@ -1102,10 +1135,10 @@ fn acquire_single_instance_lock() -> Option<std::fs::File> {
{
Ok(f) => f,
Err(err) => {
eprintln!(
"kittest_inspector: couldn't open lock file {}: {err} (running without single-instance guard)",
log_diag(&format!(
"couldn't open lock file {}: {err} (running without single-instance guard)",
path.display()
);
));
return None;
}
};
@@ -1113,8 +1146,29 @@ fn acquire_single_instance_lock() -> Option<std::fs::File> {
match FileExt::lock_exclusive(&file) {
Ok(()) => Some(file),
Err(err) => {
eprintln!("kittest_inspector: failed to acquire lock: {err}");
log_diag(&format!("failed to acquire lock: {err}"));
None
}
}
}
/// Append a diagnostic line to `{temp}/kittest_inspector.log`. We do NOT write to stderr —
/// when the harness's captured stderr pipe closes mid-run, `eprintln!` panics with
/// "failed printing to stderr: Broken pipe" and kills the window.
fn log_diag(msg: &str) {
use std::io::Write as _;
#[expect(clippy::disallowed_methods)]
let path = std::env::temp_dir().join("kittest_inspector.log");
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let _ = writeln!(f, "[{ts}] {msg}");
}
}