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:
@@ -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",
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user