From d85575097083ce17cc879b84738eb8b9e8e6f339 Mon Sep 17 00:00:00 2001 From: Franz Dietrich Date: Sun, 20 Sep 2026 11:11:59 +0200 Subject: [PATCH] Headless SVG Export Macroquad Panic Clarification Clarified and improved error reporting when drawing routines invoke Macroquad window or rendering functions (such as `screen_width()` or `screen_height()`) during headless SVG export (`--export-svg`). [export.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/src/export.rs) - Added `ExportError::Execution(String)` variant to represent drawing execution failures and panics. - Implemented `std::fmt::Display` and `std::error::Error` for `ExportError`. - Created `PanicHookGuard` RAII struct ensuring any installed panic hooks are automatically restored after execution. - In `run_headless_svg_export`: - Chains onto the existing panic hook to preserve standard panic backtrace and line number information. - Detects if the panic originated from uninitialized Macroquad context (`THREAD_ID.is_some()`). - Emits a clear, prominent diagnostic banner explaining that window/GUI functions are unavailable in headless mode. - Catches the panic via `std::panic::catch_unwind` and returns `Err(ExportError::Execution(...))`. - In `handle_svg_export`, uses `{e}` (Display) rather than `{e:?}` (Debug) for cleaner error reporting. [lib.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib-macros/src/lib.rs) - Updated the expanded `main` function generated by `#[turtle_main]` to format errors with `{}` (Display) instead of `{:?}` (Debug). [sierpinski_triangle.rs](file:///home/dietrich/Projekte/Source/turtlers/turtle-lib/examples/sierpinski_triangle.rs) - Added documentation notes to the module header and `sierpinski_triangle_auto` noting that the example requires an active graphics window and cannot be exported to SVG headlessly because it queries window dimensions. --- Executed: ```bash RUST_BACKTRACE=1 cargo run --example sierpinski_triangle --features svg -- --export-svg sier.svg ``` Output: ``` thread 'main' (49702) panicked at /home/dietrich/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/macroquad-0.4.16/src/lib.rs:172:13: assertion failed: THREAD_ID.is_some() stack backtrace: 0: __rustc::rust_begin_unwind ... 5: macroquad::window::screen_width 6: sierpinski_triangle::sierpinski_triangle_auto at ./turtle-lib/examples/sierpinski_triangle.rs:100:20 7: sierpinski_triangle::draw_sierpinski at ./turtle-lib/examples/sierpinski_triangle.rs:39:5 ... ================================================================================ Headless SVG Export Note: A Macroquad window/rendering function (e.g. `screen_width()`, `screen_height()`, or input check) was called while running in headless export mode. Headless export does not initialize a graphics window. To resolve this: - Use relative turtle commands or fixed coordinates instead of window queries, or - Run the program in windowed GUI mode without the `--export-svg` flag. ================================================================================ Error exporting SVG: execution error: Drawing function called Macroquad window/GUI functions (e.g. `screen_width()`, `screen_height()`) which are unavailable in headless SVG export mode. ``` - Process exited cleanly with exit code 1. Executed: ```bash cargo run --example koch --features svg -- --export-svg /tmp/koch.svg ``` Output: ``` SVG exported successfully to: /tmp/koch.svg ``` Exit code 0. ```bash cargo test --package turtle-lib cargo clippy --package turtle-lib --features svg -- -Wclippy::pedantic -Aclippy::cast_precision_loss -Aclippy::cast_sign_loss -Aclippy::cast_possible_truncation ``` - 17 unit tests + 34 doctests passed (1 ignored doctest). - Clippy completed with zero warnings. --- turtle-lib-macros/src/lib.rs | 2 +- turtle-lib/examples/sierpinski_triangle.rs | 7 ++ turtle-lib/src/export.rs | 112 +++++++++++++++++++-- 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/turtle-lib-macros/src/lib.rs b/turtle-lib-macros/src/lib.rs index b9faed1..e2724ba 100644 --- a/turtle-lib-macros/src/lib.rs +++ b/turtle-lib-macros/src/lib.rs @@ -259,7 +259,7 @@ fn turtle_main_impl( return; } Err(e) => { - eprintln!("Error exporting SVG: {:?}", e); + eprintln!("Error exporting SVG: {}", e); std::process::exit(1); } } diff --git a/turtle-lib/examples/sierpinski_triangle.rs b/turtle-lib/examples/sierpinski_triangle.rs index e451f6c..7992bb8 100644 --- a/turtle-lib/examples/sierpinski_triangle.rs +++ b/turtle-lib/examples/sierpinski_triangle.rs @@ -17,6 +17,10 @@ //! That is relatively easy to implement, as long as you follow these steps and let recursion do //! the rest. Another little bonus this example provides is the ability to customize the drawing //! size: the triangle will stay correctly sized and positioned automatically. +//! +//! Note: This example queries `screen_width()` and `screen_height()` to calculate positioning +//! relative to the window. Consequently, it requires an active graphics window and does not +//! support headless SVG export (`--export-svg`). use macroquad::window::{screen_height, screen_width}; use turtle_lib::*; @@ -86,6 +90,9 @@ fn sierpinski_triangle(turtle: &mut TurtlePlan, level: u8, size: f32) { /// `level` is still required, it can't be computed automatically. However, given the used /// canvas size, it will compute the appropriate size and start point so the triangle gets /// centered and occupies as much drawing space as possible while staying in bounds. +/// +/// Note: Because this function queries window dimensions, it requires an active graphics +/// window and cannot be run in headless SVG export mode. fn sierpinski_triangle_auto(turtle: &mut TurtlePlan, level: u8) { let size = TRIANGLE_SIZE; diff --git a/turtle-lib/src/export.rs b/turtle-lib/src/export.rs index 0baf558..1a20016 100644 --- a/turtle-lib/src/export.rs +++ b/turtle-lib/src/export.rs @@ -7,9 +7,29 @@ use crate::TurtlePlan; pub enum ExportError { Io(std::io::Error), Format(String), + Execution(String), // Additional formats can be added here. } +impl std::fmt::Display for ExportError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(err) => write!(f, "I/O error: {err}"), + Self::Format(msg) => write!(f, "format error: {msg}"), + Self::Execution(msg) => write!(f, "execution error: {msg}"), + } + } +} + +impl std::error::Error for ExportError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(err) => Some(err), + Self::Format(_) | Self::Execution(_) => None, + } + } +} + #[derive(Clone, Copy, Debug)] pub enum DrawingFormat { #[cfg(feature = "svg")] @@ -41,25 +61,105 @@ pub fn parse_svg_export_arg() -> Option { None } +#[cfg(feature = "svg")] +type PanicHookFn = Box) + Sync + Send + 'static>; + +#[cfg(feature = "svg")] +struct PanicHookGuard { + prev_hook: Option>, +} + +#[cfg(feature = "svg")] +impl Drop for PanicHookGuard { + fn drop(&mut self) { + if let Some(prev) = self.prev_hook.take() { + std::panic::set_hook(Box::new(move |info| prev(info))); + } + } +} + /// Headless SVG export that executes drawing commands and writes an SVG file /// without opening a graphics window and without calling `std::process::exit`. /// /// # Errors /// -/// Returns `ExportError` if file I/O fails or if the `svg` feature is not enabled. +/// Returns `ExportError` if file I/O fails, drawing execution panics, or if the `svg` feature is not enabled. pub fn run_headless_svg_export(mut build_commands: F, filename: &str) -> Result<(), ExportError> where F: FnMut(&mut TurtlePlan), { #[cfg(feature = "svg")] { + use std::panic::AssertUnwindSafe; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + let mut turtle = crate::create_turtle_plan(); - build_commands(&mut turtle); - let mut app = crate::TurtleApp::new(); - app.execute_immediate(0, turtle); + let is_mq_panic = Arc::new(AtomicBool::new(false)); + let is_mq_clone = Arc::clone(&is_mq_panic); - app.export_drawing(filename, crate::export::DrawingFormat::Svg) + let prev_hook = Arc::new(std::panic::take_hook()); + let prev_hook_for_closure = Arc::clone(&prev_hook); + + let guard = PanicHookGuard { + prev_hook: Some(prev_hook), + }; + + std::panic::set_hook(Box::new(move |info| { + prev_hook_for_closure(info); + + let msg = if let Some(s) = info.payload().downcast_ref::<&str>() { + *s + } else if let Some(s) = info.payload().downcast_ref::() { + s.as_str() + } else { + "" + }; + + let loc_file = info.location().map_or("", std::panic::Location::file); + let is_context_panic = msg.contains("THREAD_ID.is_some()") + || (loc_file.contains("macroquad") && msg.contains("assertion failed")); + + if is_context_panic { + is_mq_clone.store(true, Ordering::SeqCst); + eprintln!("\n================================================================================"); + eprintln!("Headless SVG Export Note:"); + eprintln!("A Macroquad window/rendering function (e.g. `screen_width()`, `screen_height()`,"); + eprintln!("or input check) was called while running in headless export mode."); + eprintln!("Headless export does not initialize a graphics window. To resolve this:"); + eprintln!(" - Use relative turtle commands or fixed coordinates instead of window queries, or"); + eprintln!(" - Run the program in windowed GUI mode without the `--export-svg` flag."); + eprintln!("================================================================================\n"); + } + })); + + let result = std::panic::catch_unwind(AssertUnwindSafe(|| { + build_commands(&mut turtle); + })); + + drop(guard); + + match result { + Ok(()) => { + let mut app = crate::TurtleApp::new(); + app.execute_immediate(0, turtle); + + app.export_drawing(filename, crate::export::DrawingFormat::Svg) + } + Err(payload) => { + let err_msg = if is_mq_panic.load(Ordering::SeqCst) { + "Drawing function called Macroquad window/GUI functions (e.g. `screen_width()`, `screen_height()`) which are unavailable in headless SVG export mode.".to_string() + } else if let Some(s) = payload.downcast_ref::<&str>() { + (*s).to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "Drawing function panicked during execution".to_string() + }; + Err(ExportError::Execution(err_msg)) + } + } } #[cfg(not(feature = "svg"))] @@ -86,7 +186,7 @@ where std::process::exit(0); } Err(e) => { - eprintln!("Error exporting SVG: {e:?}"); + eprintln!("Error exporting SVG: {e}"); std::process::exit(1); } }