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.
This commit is contained in:
@@ -259,7 +259,7 @@ fn turtle_main_impl(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error exporting SVG: {:?}", e);
|
eprintln!("Error exporting SVG: {}", e);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
//! That is relatively easy to implement, as long as you follow these steps and let recursion do
|
//! 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
|
//! 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.
|
//! 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 macroquad::window::{screen_height, screen_width};
|
||||||
use turtle_lib::*;
|
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
|
/// `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
|
/// 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.
|
/// 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) {
|
fn sierpinski_triangle_auto(turtle: &mut TurtlePlan, level: u8) {
|
||||||
let size = TRIANGLE_SIZE;
|
let size = TRIANGLE_SIZE;
|
||||||
|
|
||||||
|
|||||||
+104
-4
@@ -7,9 +7,29 @@ use crate::TurtlePlan;
|
|||||||
pub enum ExportError {
|
pub enum ExportError {
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Format(String),
|
Format(String),
|
||||||
|
Execution(String),
|
||||||
// Additional formats can be added here.
|
// 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)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum DrawingFormat {
|
pub enum DrawingFormat {
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
@@ -41,26 +61,106 @@ pub fn parse_svg_export_arg() -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
type PanicHookFn = Box<dyn Fn(&std::panic::PanicHookInfo<'_>) + Sync + Send + 'static>;
|
||||||
|
|
||||||
|
#[cfg(feature = "svg")]
|
||||||
|
struct PanicHookGuard {
|
||||||
|
prev_hook: Option<std::sync::Arc<PanicHookFn>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
/// Headless SVG export that executes drawing commands and writes an SVG file
|
||||||
/// without opening a graphics window and without calling `std::process::exit`.
|
/// without opening a graphics window and without calling `std::process::exit`.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # 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<F>(mut build_commands: F, filename: &str) -> Result<(), ExportError>
|
pub fn run_headless_svg_export<F>(mut build_commands: F, filename: &str) -> Result<(), ExportError>
|
||||||
where
|
where
|
||||||
F: FnMut(&mut TurtlePlan),
|
F: FnMut(&mut TurtlePlan),
|
||||||
{
|
{
|
||||||
#[cfg(feature = "svg")]
|
#[cfg(feature = "svg")]
|
||||||
{
|
{
|
||||||
let mut turtle = crate::create_turtle_plan();
|
use std::panic::AssertUnwindSafe;
|
||||||
build_commands(&mut turtle);
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
let mut turtle = crate::create_turtle_plan();
|
||||||
|
|
||||||
|
let is_mq_panic = Arc::new(AtomicBool::new(false));
|
||||||
|
let is_mq_clone = Arc::clone(&is_mq_panic);
|
||||||
|
|
||||||
|
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::<String>() {
|
||||||
|
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();
|
let mut app = crate::TurtleApp::new();
|
||||||
app.execute_immediate(0, turtle);
|
app.execute_immediate(0, turtle);
|
||||||
|
|
||||||
app.export_drawing(filename, crate::export::DrawingFormat::Svg)
|
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::<String>() {
|
||||||
|
s.clone()
|
||||||
|
} else {
|
||||||
|
"Drawing function panicked during execution".to_string()
|
||||||
|
};
|
||||||
|
Err(ExportError::Execution(err_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "svg"))]
|
#[cfg(not(feature = "svg"))]
|
||||||
{
|
{
|
||||||
@@ -86,7 +186,7 @@ where
|
|||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error exporting SVG: {e:?}");
|
eprintln!("Error exporting SVG: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user