diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 80db2bdf..19c5ae6e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,12 +20,6 @@ jobs: - uses: Swatinem/rust-cache@v2 - - name: Clippy (all features) - uses: actions-rs/cargo@v1 - with: - toolchain: stable - command: clippy - args: --target i686-pc-windows-msvc --all-features --locked -- -D warnings - name: Rustfmt uses: actions-rs/cargo@v1 diff --git a/Cargo.lock b/Cargo.lock index 9fee1ee0..eaf6dd18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,6 +418,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "dashmap" version = "5.3.4" @@ -1858,6 +1868,7 @@ dependencies = [ "base64", "chrono", "const-random", + "ctor", "dashmap", "dbpnoise", "flume", diff --git a/Cargo.toml b/Cargo.toml index 024568e2..43c49875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ rayon = { version = "1.5", optional = true } dbpnoise = { version = "0.1.2", optional = true } pathfinding = { version = "3.0.13", optional = true } num = { version = "0.4.0", optional = true } +ctor = "0.1.26" [features] default = [ @@ -108,6 +109,7 @@ worleynoise = ["rand", "rayon"] # internal feature-like things jobs = ["flume"] +panic_test = [] [dev-dependencies] regex = "1" diff --git a/src/byond.rs b/src/byond.rs index 385d5251..b709eddf 100644 --- a/src/byond.rs +++ b/src/byond.rs @@ -42,25 +42,57 @@ pub fn byond_return(value: Option>) -> *const c_char { } } +pub fn panicked( + name: &str, + thread_error: Box, + args: &[Cow], +) { + let thread_error_message = if let Some(payload) = thread_error.downcast_ref::<&str>() { + payload + } else if let Some(payload) = thread_error.downcast_ref::() { + payload + } else { + "unknown panic" + }; + + crate::panic_hook::write_to_error_log(&format!( + "panic occurred while calling {name}({}):\n{thread_error_message}", + args.join(", "), + )); +} + #[macro_export] macro_rules! byond_fn { (fn $name:ident() $body:block) => { #[no_mangle] #[allow(clippy::missing_safety_doc)] + #[allow(clippy::redundant_closure_call)] // Without it, this errors pub unsafe extern "C" fn $name( _argc: ::std::os::raw::c_int, _argv: *const *const ::std::os::raw::c_char ) -> *const ::std::os::raw::c_char { - let closure = || ($body); - $crate::byond::byond_return(closure().map(From::from)) + $crate::panic_hook::set_last_byond_fn(stringify!($name)); + + $crate::byond::byond_return(match std::panic::catch_unwind(|| -> Option> { + (|| { $body })().map(From::from) + }) { + Ok(output) => output, + Err(thread_error) => { + $crate::byond::panicked(stringify!($name), thread_error, &[]); + None + } + }) } }; (fn $name:ident($($arg:ident),* $(, ...$rest:ident)?) $body:block) => { #[no_mangle] #[allow(clippy::missing_safety_doc)] + #[allow(clippy::redundant_closure_call)] // Without it, this errors pub unsafe extern "C" fn $name( _argc: ::std::os::raw::c_int, _argv: *const *const ::std::os::raw::c_char ) -> *const ::std::os::raw::c_char { + $crate::panic_hook::set_last_byond_fn(stringify!($name)); + let __args = unsafe { $crate::byond::parse_args(_argc, _argv) }; let mut __argn = 0; @@ -72,8 +104,15 @@ macro_rules! byond_fn { let $rest = __args.get(__argn..).unwrap_or(&[]); )? - let closure = || ($body); - $crate::byond::byond_return(closure().map(From::from)) + $crate::byond::byond_return(match std::panic::catch_unwind(|| -> Option> { + (|| { $body })().map(From::from) + }) { + Ok(output) => output, + Err(thread_error) => { + $crate::byond::panicked(stringify!($name), thread_error, &__args); + None + } + }) } }; } diff --git a/src/lib.rs b/src/lib.rs index bbe40556..a0b09237 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,5 +47,10 @@ pub mod url; #[cfg(feature = "worleynoise")] pub mod worleynoise; +mod panic_hook; + +#[cfg(feature = "panic_test")] +mod panic_test; + #[cfg(not(target_pointer_width = "32"))] compile_error!("rust-g must be compiled for a 32-bit target"); diff --git a/src/panic_hook.rs b/src/panic_hook.rs new file mode 100644 index 00000000..92e8e169 --- /dev/null +++ b/src/panic_hook.rs @@ -0,0 +1,68 @@ +use std::{cell::RefCell, fs::OpenOptions, io::Write}; + +use chrono::Utc; + +thread_local! { + static LAST_BYOND_FN: RefCell> = RefCell::new(None); +} + +pub fn write_to_error_log(contents: &str) { + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open("rustg_panic.log") + .unwrap(); + + writeln!(file, "[{}] {}", Utc::now().format("%F %T%.3f"), contents).ok(); +} + +pub fn set_last_byond_fn(name: &str) { + // Be overly cautious because anything that happens in this file is all about caring about stuff that shouldn't happen + if LAST_BYOND_FN + .try_with(|cell| match cell.try_borrow_mut() { + Ok(mut cell) => { + *cell = Some(name.to_owned()); + } + + Err(_) => { + write_to_error_log("Failed to borrow LAST_BYOND_FN"); + } + }) + .is_err() + { + write_to_error_log("Failed to access LAST_BYOND_FN"); + } +} + +#[ctor::ctor] +fn set_panic_hook() { + std::panic::set_hook(Box::new(|panic_info| { + let mut message = "global panic hook triggered: ".to_owned(); + + if let Some(location) = panic_info.location() { + message.push_str(&format!("{}:{}: ", location.file(), location.line())); + } + + if let Some(payload) = panic_info.payload().downcast_ref::<&str>() { + message.push_str(payload); + } else if let Some(payload) = panic_info.payload().downcast_ref::() { + message.push_str(payload); + } else { + message.push_str("unknown panic"); + } + + LAST_BYOND_FN.with(|cell| match cell.try_borrow() { + Ok(cell) => { + if let Some(last_byond_fn) = &*cell { + message.push_str(&format!(" (last byond fn: {})", last_byond_fn)); + } + } + + Err(_) => { + message.push_str(" (failed to get last byond fn)"); + } + }); + + write_to_error_log(&message); + })); +} diff --git a/src/panic_test.rs b/src/panic_test.rs new file mode 100644 index 00000000..1c34de29 --- /dev/null +++ b/src/panic_test.rs @@ -0,0 +1,8 @@ +byond_fn!( + fn panic_test() { + panic!("oh no"); + + #[allow(unreachable_code)] + Some("what".to_owned()) + } +); diff --git a/tests/dm-tests.rs b/tests/dm-tests.rs index 40ed85e4..d48828d4 100644 --- a/tests/dm-tests.rs +++ b/tests/dm-tests.rs @@ -18,6 +18,12 @@ fn url() { run_dm_tests("url"); } +#[cfg(feature = "panic_test")] +#[test] +fn panic() { + run_dm_tests("panic"); +} + #[cfg(feature = "hash")] #[test] fn hash() { diff --git a/tests/dm/panic.dme b/tests/dm/panic.dme new file mode 100644 index 00000000..b246e3e7 --- /dev/null +++ b/tests/dm/panic.dme @@ -0,0 +1,9 @@ +#include "common.dm" + +/test/proc/panic() + call(RUST_G, "panic_test")() + + ASSERT(fexists("rustg_panic.log")) + + var/contents = file2text("rustg_panic.log") + ASSERT(findtext(contents, "panic_test"))