panic_guard.rs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. //! Panic-to-error boundary for Tauri commands.
  2. //!
  3. //! Third-party parsers (docx-rs, calamine, …) are
  4. //! known to panic on malformed input instead of returning Err. Under
  5. //! `panic = "abort"` that kills the whole app; even with `panic =
  6. //! "unwind"`, letting a panic propagate through the `extern "C"` Tauri
  7. //! command boundary is UB. These helpers catch panics at the command
  8. //! boundary and convert them into a Tauri Err the frontend can display.
  9. use std::any::Any;
  10. use std::panic::{catch_unwind, AssertUnwindSafe};
  11. /// Run a synchronous command body, converting any panic into an Err.
  12. pub fn run_guarded<T, F>(label: &str, f: F) -> Result<T, String>
  13. where
  14. F: FnOnce() -> Result<T, String>,
  15. {
  16. match catch_unwind(AssertUnwindSafe(f)) {
  17. Ok(r) => r,
  18. Err(payload) => Err(report(label, payload)),
  19. }
  20. }
  21. /// Run an async command body, converting any panic into an Err.
  22. pub async fn run_guarded_async<T, Fut>(label: &str, fut: Fut) -> Result<T, String>
  23. where
  24. Fut: std::future::Future<Output = Result<T, String>>,
  25. {
  26. use futures::FutureExt;
  27. match AssertUnwindSafe(fut).catch_unwind().await {
  28. Ok(r) => r,
  29. Err(payload) => Err(report(label, payload)),
  30. }
  31. }
  32. fn report(label: &str, payload: Box<dyn Any + Send>) -> String {
  33. let msg = if let Some(s) = payload.downcast_ref::<String>() {
  34. s.clone()
  35. } else if let Some(s) = payload.downcast_ref::<&str>() {
  36. (*s).to_string()
  37. } else {
  38. "(non-string panic payload)".to_string()
  39. };
  40. eprintln!("[panic_guard] command '{label}' panicked: {msg}");
  41. format!("Internal error in {label}: {msg}")
  42. }
  43. #[cfg(test)]
  44. mod tests {
  45. use super::*;
  46. #[test]
  47. fn sync_catches_string_panic() {
  48. let result: Result<(), String> =
  49. run_guarded("test", || panic!("boom from String"));
  50. let err = result.expect_err("panic should produce Err");
  51. assert!(err.contains("boom from String"), "got: {err}");
  52. assert!(err.starts_with("Internal error in test"), "got: {err}");
  53. }
  54. #[test]
  55. fn sync_catches_panic_with_non_string_payload() {
  56. let result: Result<(), String> =
  57. run_guarded("test", || std::panic::panic_any(42_u32));
  58. let err = result.expect_err("panic should produce Err");
  59. assert!(err.contains("non-string panic payload"), "got: {err}");
  60. }
  61. #[test]
  62. fn sync_passes_through_err() {
  63. let result: Result<i32, String> =
  64. run_guarded("test", || Err("regular error".to_string()));
  65. assert_eq!(result.unwrap_err(), "regular error");
  66. }
  67. #[test]
  68. fn sync_passes_through_ok() {
  69. let result = run_guarded("test", || Ok::<_, String>(7));
  70. assert_eq!(result.unwrap(), 7);
  71. }
  72. #[tokio::test]
  73. async fn async_catches_panic() {
  74. let result: Result<(), String> = run_guarded_async("test", async {
  75. panic!("async boom");
  76. })
  77. .await;
  78. let err = result.expect_err("panic should produce Err");
  79. assert!(err.contains("async boom"), "got: {err}");
  80. }
  81. #[tokio::test]
  82. async fn async_catches_panic_after_await_point() {
  83. let result: Result<(), String> = run_guarded_async("test", async {
  84. tokio::task::yield_now().await;
  85. panic!("post-await boom");
  86. })
  87. .await;
  88. let err = result.expect_err("panic should produce Err");
  89. assert!(err.contains("post-await boom"), "got: {err}");
  90. }
  91. }