1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -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" version = "0.34.1"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"arboard",
"bincode 2.0.1", "bincode 2.0.1",
"eframe", "eframe",
"egui", "egui",

View File

@@ -70,10 +70,15 @@ impl Inspector {
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("kittest_inspector")); .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) let mut child = Command::new(&bin)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::inherit()) .stderr(Stdio::null())
.spawn() .spawn()
.map_err(InspectorError::Launch)?; .map_err(InspectorError::Launch)?;

View File

@@ -25,7 +25,7 @@ required-features = ["app"]
default = ["app"] default = ["app"]
## Build the eframe inspector binary. ## 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] [dependencies]
accesskit = { workspace = true, features = ["serde"] } 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 } egui_extras = { workspace = true, features = ["image"], optional = true }
fs4 = { workspace = true, optional = true } fs4 = { workspace = true, optional = true }
image = { workspace = true, optional = true, features = ["gif"] } 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] [lints]
workspace = true workspace = true

View File

@@ -4,8 +4,6 @@
//! Communication is over stdin/stdout: the harness pipes [`HarnessMessage`]s into our stdin //! 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. //! 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::io::{self, BufReader, BufWriter};
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
@@ -47,6 +45,15 @@ impl SkipState {
} }
fn main() -> eframe::Result<()> { 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 // 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 // here until that window closes. Held for the lifetime of `_lock`; the OS releases the
// flock when the file descriptor is dropped on exit. // 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; return;
}; };
if let Err(err) = write_message(&mut writer, &InspectorReply::Continue { events }) { if let Err(err) = write_message(&mut writer, &InspectorReply::Continue { events }) {
eprintln!("kittest_inspector: write failed: {err}"); log_diag(&format!("write failed: {err}"));
return; return;
} }
} }
@@ -101,7 +108,7 @@ fn run_io(ui_tx: &mpsc::Sender<WorkerEvent>, release_rx: &ReleaseRx) {
} }
Err(err) => { Err(err) => {
if err.kind() != io::ErrorKind::UnexpectedEof { if err.kind() != io::ErrorKind::UnexpectedEof {
eprintln!("kittest_inspector: read failed: {err}"); log_diag(&format!("read failed: {err}"));
} }
let _ = ui_tx.send(WorkerEvent::Disconnected); let _ = ui_tx.send(WorkerEvent::Disconnected);
return; return;
@@ -423,20 +430,27 @@ fn controls_panel(app: &mut InspectorApp, ui: &mut egui::Ui) {
if ui if ui
.add_enabled(total > 0, egui::Button::new("📋 Copy as GIF")) .add_enabled(total > 0, egui::Button::new("📋 Copy as GIF"))
.on_hover_text( .on_hover_text(
"Encode the whole history as a GIF, write it to the system temp dir, \ "Encode the whole history as a GIF and put it on the system clipboard \
and copy the resulting path to the clipboard.", as a file reference — paste into Slack / Discord / Finder etc.",
) )
.clicked() .clicked()
{ {
let message = match save_history_as_gif(&app.history, 10.0) { log_diag("Copy as GIF clicked");
Ok(path) => { // Run the copy on a detached worker so a slow encode doesn't stall the UI.
ui.ctx().copy_text(path.display().to_string()); // (We can't `catch_unwind` under `panic = abort`, but the panic hook still
format!("Copied path to clipboard: {}", path.display()) // logs what happened, and the real broken-pipe cause is fixed upstream.)
} let history = app.history.clone();
Err(err) => format!("Failed to save GIF: {err}"), let _ = std::thread::Builder::new()
}; .name("kittest_inspector_copy_gif".into())
eprintln!("kittest_inspector: {message}"); .spawn(move || match copy_history_as_gif(&history, 10.0) {
app.status_message = Some(message); 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() { if let Some(msg) = app.status_message.as_deref() {
ui.weak(msg); 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 /// 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 /// temp dir, and put a *file reference* for that path onto the system clipboard via arboard.
/// `frame_rate`, last frame held for one second so the loop point is obvious. /// Pasting into Slack / Discord / GitHub / Finder etc. attaches the GIF with animation intact.
fn save_history_as_gif( /// 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], history: &[Frame],
frame_rate: f32, frame_rate: f32,
) -> Result<std::path::PathBuf, String> { ) -> Result<std::path::PathBuf, String> {
@@ -1034,6 +1050,10 @@ fn save_history_as_gif(
if history.is_empty() { if history.is_empty() {
return Err("history is empty".into()); return Err("history is empty".into());
} }
log_diag(&format!(
"encoding {} frame(s) @ {frame_rate} fps",
history.len()
));
let ts = std::time::SystemTime::now() let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .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. // Stable-across-processes temp path is fine here: each invocation wants a fresh file.
#[expect(clippy::disallowed_methods)] #[expect(clippy::disallowed_methods)]
let path = std::env::temp_dir().join(format!("kittest_inspector_{ts}.gif")); 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) let file = std::fs::File::create(&path)
.map_err(|err| format!("couldn't create {}: {err}", path.display()))?; .map_err(|err| format!("couldn't create {}: {err}", path.display()))?;
@@ -1078,6 +1099,18 @@ fn save_history_as_gif(
.encode_frame(anim_frame) .encode_frame(anim_frame)
.map_err(|err| format!("encode frame {i}: {err}"))?; .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) Ok(path)
} }
@@ -1102,10 +1135,10 @@ fn acquire_single_instance_lock() -> Option<std::fs::File> {
{ {
Ok(f) => f, Ok(f) => f,
Err(err) => { Err(err) => {
eprintln!( log_diag(&format!(
"kittest_inspector: couldn't open lock file {}: {err} (running without single-instance guard)", "couldn't open lock file {}: {err} (running without single-instance guard)",
path.display() path.display()
); ));
return None; return None;
} }
}; };
@@ -1113,8 +1146,29 @@ fn acquire_single_instance_lock() -> Option<std::fs::File> {
match FileExt::lock_exclusive(&file) { match FileExt::lock_exclusive(&file) {
Ok(()) => Some(file), Ok(()) => Some(file),
Err(err) => { Err(err) => {
eprintln!("kittest_inspector: failed to acquire lock: {err}"); log_diag(&format!("failed to acquire lock: {err}"));
None 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}");
}
}