From ef3ba86cf742cf4acff52dd8951c0b35ba7b6dd8 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sun, 7 Jan 2024 15:06:50 -0500 Subject: [PATCH 01/38] feat(native): restart program after panicking on Unix-like systems --- src/main.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index b0e2b024..d6a65e41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -178,7 +178,7 @@ fn main() { } // Set up hooks for formatting errors and panics - color_eyre::config::HookBuilder::default() + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() .panic_section(format!("Luminol version: {}", git_version::git_version!())) .add_frame_filter(Box::new(|frames| { let filters = &[ @@ -209,8 +209,25 @@ fn main() { }) }) })) + .into_hooks(); + eyre_hook .install() .expect("failed to install color-eyre hooks"); + std::panic::set_hook(Box::new(move |info| { + eprintln!("{}", panic_hook.panic_report(info).to_string()); + + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + + let mut args = std::env::args_os(); + + if let Some(arg0) = args.next() { + let error = std::process::Command::new(arg0).args(args).exec(); + eprintln!("Failed to restart Luminol:{error:?}"); + } + } + })); // Log to stderr as well as Luminol's log. let (log_term_tx, log_term_rx) = luminol_term::unbounded(); From 02c78b54963392fecc8aff21c9b0d9e004b3ccd5 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sun, 7 Jan 2024 15:56:21 -0500 Subject: [PATCH 02/38] fix(native): use `std::env::current_exe()` to get executable path The program's first argument is not guaranteed to be a path to the current executable. Also, at least on Linux, `std::env::current_exe()` is able to correctly find the new location of the executable if the user moves the executable before the panic handler runs. --- src/main.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index d6a65e41..b0d6a429 100644 --- a/src/main.rs +++ b/src/main.rs @@ -221,11 +221,14 @@ fn main() { use std::os::unix::process::CommandExt; let mut args = std::env::args_os(); - - if let Some(arg0) = args.next() { - let error = std::process::Command::new(arg0).args(args).exec(); - eprintln!("Failed to restart Luminol:{error:?}"); - } + let arg0 = args.next(); + let exe_path = std::env::current_exe().map_or_else( + |_| arg0.expect("could not get path to current executable"), + |exe_path| exe_path.into_os_string(), + ); + + let error = std::process::Command::new(exe_path).args(args).exec(); + eprintln!("Failed to restart Luminol: {error:?}"); } })); From 175a6418c27f45d3d8fff8f5bd953bebf82438d4 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sun, 7 Jan 2024 16:42:45 -0500 Subject: [PATCH 03/38] feat(native): add fallback restart code for non-Unix systems --- src/main.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index b0d6a429..6321bb09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -216,20 +216,27 @@ fn main() { std::panic::set_hook(Box::new(move |info| { eprintln!("{}", panic_hook.panic_report(info).to_string()); + let mut args = std::env::args_os(); + let arg0 = args.next(); + let exe_path = std::env::current_exe().map_or_else( + |_| arg0.expect("could not get path to current executable"), + |exe_path| exe_path.into_os_string(), + ); + #[cfg(unix)] { use std::os::unix::process::CommandExt; - let mut args = std::env::args_os(); - let arg0 = args.next(); - let exe_path = std::env::current_exe().map_or_else( - |_| arg0.expect("could not get path to current executable"), - |exe_path| exe_path.into_os_string(), - ); - let error = std::process::Command::new(exe_path).args(args).exec(); eprintln!("Failed to restart Luminol: {error:?}"); } + + #[cfg(not(unix))] + { + if let Err(error) = std::process::Command::new(exe_path).args(args).spawn() { + eprintln!("Failed to restart Luminol: {error:?}"); + } + } })); // Log to stderr as well as Luminol's log. From a413e9fc6929c2d5bc162db150dcc1e3fc5e4ca0 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sun, 7 Jan 2024 21:25:38 -0500 Subject: [PATCH 04/38] feat(native): persist panic reports by saving to temporary files --- Cargo.lock | 1 + Cargo.toml | 2 ++ crates/filesystem/Cargo.toml | 2 +- src/main.rs | 22 +++++++++++++++++++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98a7be87..e70e71f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2999,6 +2999,7 @@ dependencies = [ "rfd", "steamworks", "strum", + "tempfile", "tokio", "tracing", "tracing-log 0.1.4", diff --git a/Cargo.toml b/Cargo.toml index a37f9af1..966e9585 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,7 @@ qp-trie = "0.8.2" itertools = "0.11.0" rfd = "0.12.0" +tempfile = "3.8.1" rand = "0.8.5" @@ -192,6 +193,7 @@ tokio = { version = "1.33", features = [ "rt-multi-thread", "parking_lot", ] } # *sigh* +tempfile.workspace = true luminol-term.workspace = true # Set poll promise features here based on the target diff --git a/crates/filesystem/Cargo.toml b/crates/filesystem/Cargo.toml index 71aa3104..46048037 100644 --- a/crates/filesystem/Cargo.toml +++ b/crates/filesystem/Cargo.toml @@ -53,7 +53,7 @@ qp-trie.workspace = true winreg = "0.51.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tempfile = "3.8.1" +tempfile.workspace = true async-fs = "2.1.0" [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/src/main.rs b/src/main.rs index 6321bb09..a4038270 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,8 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(target_arch = "wasm32", no_main)] // there is no main function in web builds +use std::io::{Read, Write}; + #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -122,6 +124,16 @@ impl std::io::Write for LogWriter { #[cfg(not(target_arch = "wasm32"))] fn main() { + // Load the panic report from the previous run if it exists + if let Some(path) = std::env::var_os("LUMINOL_PANIC_REPORT_FILE") { + if let Ok(mut file) = std::fs::File::open(&path) { + let mut buffer = String::new(); + let _success = file.read_to_string(&mut buffer).is_ok(); + // TODO: use this report to open a panic reporter + } + let _ = std::fs::remove_file(path); + } + #[cfg(feature = "steamworks")] let steamworks = match steam::Steamworks::new() { Ok(s) => s, @@ -214,7 +226,8 @@ fn main() { .install() .expect("failed to install color-eyre hooks"); std::panic::set_hook(Box::new(move |info| { - eprintln!("{}", panic_hook.panic_report(info).to_string()); + let report = panic_hook.panic_report(info).to_string(); + eprintln!("{report}"); let mut args = std::env::args_os(); let arg0 = args.next(); @@ -223,6 +236,13 @@ fn main() { |exe_path| exe_path.into_os_string(), ); + let mut file = tempfile::NamedTempFile::new().expect("failed to create temporary file"); + file.write_all(report.as_bytes()) + .expect("failed to write to temporary file"); + file.flush().expect("failed to flush temporary file"); + let (_, path) = file.keep().expect("failed to persist temporary file"); + std::env::set_var("LUMINOL_PANIC_REPORT_FILE", path); + #[cfg(unix)] { use std::os::unix::process::CommandExt; From 1d7942864c623ca493fed48b210fd524f7c23bf4 Mon Sep 17 00:00:00 2001 From: white-axe Date: Mon, 8 Jan 2024 23:06:18 -0500 Subject: [PATCH 05/38] perf(web): panic channel now uses oneshot instead of flume --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 21 ++++++++++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e70e71f0..4f9db39d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2978,7 +2978,6 @@ dependencies = [ "color-eyre", "egui", "egui_extras", - "flume", "futures-lite 2.1.0", "git-version", "image 0.24.7", @@ -2994,6 +2993,7 @@ dependencies = [ "luminol-ui", "luminol-web", "once_cell", + "oneshot", "parking_lot", "poll-promise", "rfd", diff --git a/Cargo.toml b/Cargo.toml index 966e9585..10cb58ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -215,7 +215,7 @@ wasm-bindgen = "0.2.87" wasm-bindgen-futures = "0.4" js-sys = "0.3" -flume.workspace = true +oneshot.workspace = true luminol-web = { version = "0.4.0", path = "crates/web/" } diff --git a/src/main.rs b/src/main.rs index a4038270..b841b441 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(target_arch = "wasm32", no_main)] // there is no main function in web builds +#[cfg(not(target_arch = "wasm32"))] use std::io::{Read, Write}; #[cfg(target_arch = "wasm32")] @@ -47,10 +48,10 @@ mod steam; struct CopyWriter(A, B); #[cfg(not(target_arch = "wasm32"))] -impl std::io::Write for CopyWriter +impl Write for CopyWriter where - A: std::io::Write, - B: std::io::Write, + A: Write, + B: Write, { fn write(&mut self, buf: &[u8]) -> std::io::Result { Ok(self.0.write(buf)?.min(self.1.write(buf)?)) @@ -79,7 +80,7 @@ static CONTEXT: once_cell::sync::OnceCell = once_cell::sync::Once struct LogWriter(luminol_term::termwiz::escape::parser::Parser); #[cfg(not(target_arch = "wasm32"))] -impl std::io::Write for LogWriter { +impl Write for LogWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { LOG_BYTE_SENDER .get() @@ -339,10 +340,11 @@ static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex: #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn luminol_main_start(fallback: bool) { - let (panic_tx, panic_rx) = flume::unbounded(); + let (panic_tx, panic_rx) = oneshot::channel(); + let panic_tx = std::sync::Arc::new(parking_lot::Mutex::new(Some(panic_tx))); wasm_bindgen_futures::spawn_local(async move { - if panic_rx.recv_async().await.is_ok() { + if panic_rx.await.is_ok() { let _ = web_sys::window().map(|window| window.alert_with_message("Luminol has crashed! Please check your browser's developer console for more details.")); } }); @@ -358,7 +360,12 @@ pub fn luminol_main_start(fallback: bool) { web_sys::console::log_1(&js_sys::JsString::from( panic_hook.panic_report(info).to_string(), )); - let _ = panic_tx.send(()); + + if let Some(mut panic_tx) = panic_tx.try_lock() { + if let Some(panic_tx) = panic_tx.take() { + let _ = panic_tx.send(()); + } + } })); let window = web_sys::window().expect("could not get `window` object (make sure you're running this in the main thread of a web browser)"); From 334cef6897daa6fafd7fb6056782258f6be26575 Mon Sep 17 00:00:00 2001 From: white-axe Date: Tue, 9 Jan 2024 00:36:33 -0500 Subject: [PATCH 06/38] feat(web): restart program and persist panic report after panicking --- assets/main.js | 71 ++++++++++++++++++++++++++++---------------------- index.html | 2 +- src/main.rs | 39 ++++++++++++++++++++------- 3 files changed, 70 insertions(+), 42 deletions(-) diff --git a/assets/main.js b/assets/main.js index 0bc1d068..9f6de497 100644 --- a/assets/main.js +++ b/assets/main.js @@ -15,37 +15,46 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -// Check if the user's browser supports WebGPU -console.log('Checking for WebGPU support in web workers…'); -const worker = new Worker('/webgpu-test-worker.js'); -const promise = new Promise(function (resolve) { - worker.onmessage = function (e) { - resolve(e.data); - }; -}); -worker.postMessage(null); -const gpu = await promise; -worker.terminate(); -if (gpu) { - console.log('WebGPU is supported. Using WebGPU backend if available.'); -} else { - console.log('No support detected. Using WebGL backend if available.'); -} +window.restartLuminol = async function() { + // We need to reload luminol.js every time by invalidating the cache, + // otherwise it'll just reload the same WebAssembly module every time + // instead of reinstantiating it + const invalidator = crypto.randomUUID(); -// If WebGPU is supported, always use luminol.js -// If WebGPU is not supported, use luminol_webgl.js if it's available or fallback to luminol.js -let fallback = false; -let luminol; -if (gpu) { - luminol = await import('/luminol.js'); -} else { - try { - luminol = await import('/luminol_webgl.js'); - fallback = true; - } catch (e) { - luminol = await import('/luminol.js'); + // Check if the user's browser supports WebGPU + console.log('Checking for WebGPU support in web workers…'); + const worker = new Worker('/webgpu-test-worker.js'); + const promise = new Promise(function (resolve) { + worker.onmessage = function (e) { + resolve(e.data); + }; + }); + worker.postMessage(null); + const gpu = await promise; + worker.terminate(); + if (gpu) { + console.log('WebGPU is supported. Using WebGPU backend if available.'); + } else { + console.log('No support detected. Using WebGL backend if available.'); } -} -await luminol.default(fallback ? '/luminol_webgl_bg.wasm' : '/luminol_bg.wasm'); -luminol.luminol_main_start(fallback); + // If WebGPU is supported, always use luminol.js + // If WebGPU is not supported, use luminol_webgl.js if it's available or fallback to luminol.js + let fallback = false; + let luminol; + if (gpu) { + luminol = await import(`/luminol.js?v=${invalidator}`); + } else { + try { + luminol = await import(`/luminol_webgl.js?v=${invalidator}`); + fallback = true; + } catch (e) { + luminol = await import(`/luminol.js?v=${invalidator}`); + } + } + + await luminol.default(fallback ? '/luminol_webgl_bg.wasm' : '/luminol_bg.wasm'); + luminol.luminol_main_start(fallback); +}; + +await window.restartLuminol(); diff --git a/index.html b/index.html index edd3bc29..6b7e9e28 100644 --- a/index.html +++ b/index.html @@ -126,7 +126,7 @@ - +
diff --git a/src/main.rs b/src/main.rs index b841b441..d24359dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -322,6 +322,15 @@ fn main() { .expect("failed to start luminol"); } +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen( + inline_js = "let report = null; export function get_panic_report() { return report; }; export function set_panic_report(r) { report = r; window.restartLuminol() };" +)] +extern "C" { + fn get_panic_report() -> Option; + fn set_panic_report(r: String); +} + #[cfg(target_arch = "wasm32")] const CANVAS_ID: &str = "luminol-canvas"; @@ -340,12 +349,15 @@ static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex: #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn luminol_main_start(fallback: bool) { + // TODO: use this report to open a panic reporter + let _report = get_panic_report(); + let (panic_tx, panic_rx) = oneshot::channel(); let panic_tx = std::sync::Arc::new(parking_lot::Mutex::new(Some(panic_tx))); wasm_bindgen_futures::spawn_local(async move { - if panic_rx.await.is_ok() { - let _ = web_sys::window().map(|window| window.alert_with_message("Luminol has crashed! Please check your browser's developer console for more details.")); + if let Ok(report) = panic_rx.await { + set_panic_report(report); } }); @@ -357,29 +369,36 @@ pub fn luminol_main_start(fallback: bool) { .install() .expect("failed to install color-eyre hooks"); std::panic::set_hook(Box::new(move |info| { - web_sys::console::log_1(&js_sys::JsString::from( - panic_hook.panic_report(info).to_string(), - )); + let report = panic_hook.panic_report(info).to_string(); + web_sys::console::log_1(&report.as_str().into()); + // Send the panic report to the main thread to be persisted + // We need to send the panic report to the main thread because JavaScript global variables + // are thread-local and this panic handler runs on the thread that panicked if let Some(mut panic_tx) = panic_tx.try_lock() { if let Some(panic_tx) = panic_tx.take() { - let _ = panic_tx.send(()); + let _ = panic_tx.send(report); } } })); - let window = web_sys::window().expect("could not get `window` object (make sure you're running this in the main thread of a web browser)"); + let window = web_sys::window().expect("could not get `window` object"); let prefers_color_scheme_dark = window .match_media("(prefers-color-scheme: dark)") .unwrap() .map(|x| x.matches()); - let canvas = window + let document = window .document() - .expect("could not get `window.document` object (make sure you're running this in a web browser)") + .expect("could not get `window.document` object"); + let canvas = document + .create_element("canvas") + .expect("could not create canvas element") + .unchecked_into::(); + document .get_element_by_id(CANVAS_ID) .expect(format!("could not find HTML element with ID '{CANVAS_ID}'").as_str()) - .unchecked_into::(); + .replace_children_with_node_1(&canvas); let offscreen_canvas = canvas .transfer_control_to_offscreen() .expect("could not transfer canvas control to offscreen"); From 04506ea870e7bf04ea3690a7e12cc258d6f85af6 Mon Sep 17 00:00:00 2001 From: white-axe Date: Tue, 9 Jan 2024 00:56:49 -0500 Subject: [PATCH 07/38] perf(web): terminate worker thread after panicking --- src/main.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index d24359dc..5267eb19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -324,7 +324,7 @@ fn main() { #[cfg(target_arch = "wasm32")] #[wasm_bindgen( - inline_js = "let report = null; export function get_panic_report() { return report; }; export function set_panic_report(r) { report = r; window.restartLuminol() };" + inline_js = "let report = null; export function get_panic_report() { return report; }; export function set_panic_report(r) { report = r; window.restartLuminol(); };" )] extern "C" { fn get_panic_report() -> Option; @@ -352,14 +352,21 @@ pub fn luminol_main_start(fallback: bool) { // TODO: use this report to open a panic reporter let _report = get_panic_report(); + let worker_cell = std::rc::Rc::new(once_cell::unsync::OnceCell::::new()); let (panic_tx, panic_rx) = oneshot::channel(); let panic_tx = std::sync::Arc::new(parking_lot::Mutex::new(Some(panic_tx))); - wasm_bindgen_futures::spawn_local(async move { - if let Ok(report) = panic_rx.await { - set_panic_report(report); - } - }); + { + let worker_cell = worker_cell.clone(); + wasm_bindgen_futures::spawn_local(async move { + if let Ok(report) = panic_rx.await { + if let Some(worker) = worker_cell.get() { + worker.terminate(); + } + set_panic_report(report); + } + }); + } // Set up hooks for formatting errors and panics let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() @@ -461,6 +468,7 @@ pub fn luminol_main_start(fallback: bool) { worker_options.type_(web_sys::WorkerType::Module); let worker = web_sys::Worker::new_with_options("/worker.js", &worker_options) .expect("failed to spawn web worker"); + worker_cell.set(worker.clone()).unwrap(); let message = js_sys::Array::new(); message.push(&JsValue::from(fallback)); From 3711ce73ff8477414c432375e930a1ebc8a8847b Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 10 Jan 2024 12:49:53 -0500 Subject: [PATCH 08/38] perf(web): unregister event listeners on panic --- crates/eframe/src/web/events.rs | 2 ++ crates/eframe/src/web/mod.rs | 16 +++++++++- crates/eframe/src/web/panic_handler.rs | 7 +++- crates/eframe/src/web/web_runner.rs | 44 ++++++++++++++++++-------- src/main.rs | 37 +++++++++++++++++----- 5 files changed, 83 insertions(+), 23 deletions(-) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 9b314858..4b09cc1a 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -747,6 +747,8 @@ pub(crate) fn install_canvas_events(state: &MainState) -> Result<(), JsValue> { let mut options = web_sys::MutationObserverInit::new(); options.attributes(true); observer.observe_with_options(&state.canvas, &options)?; + // We don't need to unregister this mutation observer on panic because it auto-deregisters + // when the target (the canvas) is removed from the DOM and garbage-collected callback.forget(); } diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 031aabfe..6a0146fc 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -374,7 +374,17 @@ impl MainState { // Add the event listener to the target target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?; - closure.forget(); + // Add a hook to unregister this event listener after panicking + EVENTS_TO_UNSUBSCRIBE.with_borrow_mut(|events| { + events.push(web_runner::EventToUnsubscribe::TargetEvent( + web_runner::TargetEvent { + target: target.clone(), + event_name: event_name.to_string(), + closure, + }, + )); + }); + Ok(()) } } @@ -433,3 +443,7 @@ enum WebRunnerOutput { } static PANIC_LOCK: once_cell::sync::OnceCell<()> = once_cell::sync::OnceCell::new(); + +thread_local! { + static EVENTS_TO_UNSUBSCRIBE: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); +} diff --git a/crates/eframe/src/web/panic_handler.rs b/crates/eframe/src/web/panic_handler.rs index 9656ba28..cadeed2a 100644 --- a/crates/eframe/src/web/panic_handler.rs +++ b/crates/eframe/src/web/panic_handler.rs @@ -13,13 +13,18 @@ pub struct PanicHandler(Arc>); impl PanicHandler { /// Install a panic hook. - pub fn install() -> Self { + pub fn install(panic_tx: Arc>>>) -> Self { let handler = Self(Arc::new(Mutex::new(Default::default()))); let handler_clone = handler.clone(); let previous_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic_info| { let _ = super::PANIC_LOCK.set(()); + if let Some(mut panic_tx) = panic_tx.try_lock() { + if let Some(panic_tx) = panic_tx.take() { + let _ = panic_tx.send(()); + } + } let summary = PanicSummary::new(panic_info); diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index c0d827bb..deab0682 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; use wasm_bindgen::prelude::*; @@ -29,13 +29,13 @@ pub struct WebRunner { impl WebRunner { /// Will install a panic handler that will catch and log any panics #[allow(clippy::new_without_default)] - pub fn new() -> Self { + pub fn new(panic_tx: Arc>>>) -> Self { #[cfg(not(web_sys_unstable_apis))] log::warn!( "eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work." ); - let panic_handler = PanicHandler::install(); + let panic_handler = PanicHandler::install(panic_tx); Self { panic_handler, @@ -46,7 +46,25 @@ impl WebRunner { /// Set up the event listeners on the main thread in order to do things like respond to /// mouse events and resize the canvas to fill the screen. - pub fn setup_main_thread_hooks(state: super::MainState) -> Result<(), JsValue> { + pub fn setup_main_thread_hooks( + state: super::MainState, + ) -> Result>>>, JsValue> { + let (panic_tx, panic_rx) = oneshot::channel(); + + wasm_bindgen_futures::spawn_local(async move { + let _ = panic_rx.await; + super::EVENTS_TO_UNSUBSCRIBE.with_borrow_mut(|events| { + for event in events.drain(..) { + if let Err(e) = event.unsubscribe() { + log::warn!( + "Failed to unsubscribe from event: {}", + super::string_from_js_value(&e), + ); + } + } + }); + }); + { events::install_canvas_events(&state)?; events::install_document_events(&state)?; @@ -93,7 +111,7 @@ impl WebRunner { } }); - Ok(()) + Ok(Arc::new(parking_lot::Mutex::new(Some(panic_tx)))) } /// Create the application, install callbacks, and start running the app. @@ -225,19 +243,19 @@ impl WebRunner { // ---------------------------------------------------------------------------- -struct TargetEvent { - target: web_sys::EventTarget, - event_name: String, - closure: Closure, +pub(super) struct TargetEvent { + pub(super) target: web_sys::EventTarget, + pub(super) event_name: String, + pub(super) closure: Closure, } #[allow(unused)] -struct IntervalHandle { - handle: i32, - closure: Closure, +pub(super) struct IntervalHandle { + pub(super) handle: i32, + pub(super) closure: Closure, } -enum EventToUnsubscribe { +pub(super) enum EventToUnsubscribe { TargetEvent(TargetEvent), #[allow(unused)] diff --git a/src/main.rs b/src/main.rs index 5267eb19..2306f34c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -341,11 +341,17 @@ struct WorkerData { prefers_color_scheme_dark: Option, fs_worker_channels: luminol_filesystem::web::WorkerChannels, runner_worker_channels: luminol_eframe::web::WorkerChannels, + runner_panic_tx: std::sync::Arc>>>, } #[cfg(target_arch = "wasm32")] static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex::new(None); +#[cfg(target_arch = "wasm32")] +thread_local! { + static BEFORE_UNLOAD_EVENT: std::cell::RefCell>> = std::cell::RefCell::new(None); +} + #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn luminol_main_start(fallback: bool) { @@ -363,6 +369,16 @@ pub fn luminol_main_start(fallback: bool) { if let Some(worker) = worker_cell.get() { worker.terminate(); } + + BEFORE_UNLOAD_EVENT.with_borrow_mut(|closure| { + if let (Some(window), Some(closure)) = (web_sys::window(), closure.take()) { + let _ = window.remove_event_listener_with_callback( + "beforeunload", + closure.as_ref().unchecked_ref(), + ); + } + }); + set_panic_report(report); } }); @@ -429,12 +445,13 @@ pub fn luminol_main_start(fallback: bool) { let (runner_worker_channels, runner_main_channels) = luminol_eframe::web::channels(); luminol_filesystem::host::setup_main_thread_hooks(fs_main_channels); - luminol_eframe::WebRunner::setup_main_thread_hooks(luminol_eframe::web::MainState { - inner: Default::default(), - canvas: canvas.clone(), - channels: runner_main_channels, - }) - .expect("unable to setup web runner main thread hooks"); + let runner_panic_tx = + luminol_eframe::WebRunner::setup_main_thread_hooks(luminol_eframe::web::MainState { + inner: Default::default(), + canvas: canvas.clone(), + channels: runner_main_channels, + }) + .expect("unable to setup web runner main thread hooks"); let modified = luminol_core::ModifiedState::default(); @@ -444,6 +461,7 @@ pub fn luminol_main_start(fallback: bool) { prefers_color_scheme_dark, fs_worker_channels, runner_worker_channels, + runner_panic_tx, }); // Show confirmation dialogue if the user tries to close the browser tab while there are @@ -460,7 +478,9 @@ pub fn luminol_main_start(fallback: bool) { window .add_event_listener_with_callback("beforeunload", closure.as_ref().unchecked_ref()) .expect("failed to add beforeunload listener"); - closure.forget(); + BEFORE_UNLOAD_EVENT.with_borrow_mut(|event| { + *event = Some(closure); + }); } let mut worker_options = web_sys::WorkerOptions::new(); @@ -490,13 +510,14 @@ pub async fn luminol_worker_start(canvas: web_sys::OffscreenCanvas) { prefers_color_scheme_dark, fs_worker_channels, runner_worker_channels, + runner_panic_tx, } = WORKER_DATA.lock().take().unwrap(); luminol_filesystem::host::FileSystem::setup_worker_channels(fs_worker_channels); let web_options = luminol_eframe::WebOptions::default(); - luminol_eframe::WebRunner::new() + luminol_eframe::WebRunner::new(runner_panic_tx) .start( canvas, web_options, From eb52d77b87e9c3a85f702f4b036cbe9be76d3027 Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 10 Jan 2024 12:57:47 -0500 Subject: [PATCH 09/38] chore(web): remove extra use in web_runner.rs to reduce conflicts --- crates/eframe/src/web/web_runner.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index deab0682..fd631587 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, rc::Rc, sync::Arc}; +use std::{cell::RefCell, rc::Rc}; use wasm_bindgen::prelude::*; @@ -29,7 +29,7 @@ pub struct WebRunner { impl WebRunner { /// Will install a panic handler that will catch and log any panics #[allow(clippy::new_without_default)] - pub fn new(panic_tx: Arc>>>) -> Self { + pub fn new(panic_tx: std::sync::Arc>>>) -> Self { #[cfg(not(web_sys_unstable_apis))] log::warn!( "eframe compiled without RUSTFLAGS='--cfg=web_sys_unstable_apis'. Copying text won't work." @@ -48,7 +48,7 @@ impl WebRunner { /// mouse events and resize the canvas to fill the screen. pub fn setup_main_thread_hooks( state: super::MainState, - ) -> Result>>>, JsValue> { + ) -> Result>>>, JsValue> { let (panic_tx, panic_rx) = oneshot::channel(); wasm_bindgen_futures::spawn_local(async move { @@ -111,7 +111,7 @@ impl WebRunner { } }); - Ok(Arc::new(parking_lot::Mutex::new(Some(panic_tx)))) + Ok(std::sync::Arc::new(parking_lot::Mutex::new(Some(panic_tx)))) } /// Create the application, install callbacks, and start running the app. From c2b79157646a8ad97a6bcf002e2ebbdb3df775b4 Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 10 Jan 2024 14:13:30 -0500 Subject: [PATCH 10/38] refactor(web): remove unnecessary static variable from main.rs --- src/main.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2306f34c..a5135b1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -347,11 +347,6 @@ struct WorkerData { #[cfg(target_arch = "wasm32")] static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex::new(None); -#[cfg(target_arch = "wasm32")] -thread_local! { - static BEFORE_UNLOAD_EVENT: std::cell::RefCell>> = std::cell::RefCell::new(None); -} - #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn luminol_main_start(fallback: bool) { @@ -359,25 +354,30 @@ pub fn luminol_main_start(fallback: bool) { let _report = get_panic_report(); let worker_cell = std::rc::Rc::new(once_cell::unsync::OnceCell::::new()); + let before_unload_cell = std::rc::Rc::new(std::cell::RefCell::new( + None::>, + )); let (panic_tx, panic_rx) = oneshot::channel(); let panic_tx = std::sync::Arc::new(parking_lot::Mutex::new(Some(panic_tx))); { let worker_cell = worker_cell.clone(); + let before_unload_cell = before_unload_cell.clone(); + wasm_bindgen_futures::spawn_local(async move { if let Ok(report) = panic_rx.await { if let Some(worker) = worker_cell.get() { worker.terminate(); } - BEFORE_UNLOAD_EVENT.with_borrow_mut(|closure| { - if let (Some(window), Some(closure)) = (web_sys::window(), closure.take()) { - let _ = window.remove_event_listener_with_callback( - "beforeunload", - closure.as_ref().unchecked_ref(), - ); - } - }); + if let (Some(window), Some(closure)) = + (web_sys::window(), before_unload_cell.take()) + { + let _ = window.remove_event_listener_with_callback( + "beforeunload", + closure.as_ref().unchecked_ref(), + ); + } set_panic_report(report); } @@ -478,9 +478,7 @@ pub fn luminol_main_start(fallback: bool) { window .add_event_listener_with_callback("beforeunload", closure.as_ref().unchecked_ref()) .expect("failed to add beforeunload listener"); - BEFORE_UNLOAD_EVENT.with_borrow_mut(|event| { - *event = Some(closure); - }); + *before_unload_cell.borrow_mut() = Some(closure); } let mut worker_options = web_sys::WorkerOptions::new(); From 0e582894156dba7354b0e816fb5a88a633bd564c Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 10 Jan 2024 14:50:08 -0500 Subject: [PATCH 11/38] fix(web): set `clippy::missing_safety_doc` to `warn` Setting this to `forbid` seems to break clippy when using wasm-bindgen. I'm setting this to `warn` so we can see clippy lints for crates that depend on luminol-web in web builds. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 10cb58ed..d581ae0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ doc_markdown = "allow" missing_panics_doc = "allow" too_many_lines = "allow" # you must provide a safety doc. -missing_safety_doc = "forbid" +missing_safety_doc = "warn" [workspace.package] version = "0.4.0" From dcf8e5ef8c632bdf4396177449026b446ed04a18 Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 10 Jan 2024 15:05:38 -0500 Subject: [PATCH 12/38] chore(web): fix clippy warnings --- crates/components/src/lib.rs | 1 + crates/eframe/src/web/storage.rs | 2 +- crates/egui-wgpu/src/lib.rs | 2 +- crates/filesystem/src/project.rs | 26 +++++++++++--------------- crates/filesystem/src/web/mod.rs | 6 +++--- crates/filesystem/src/web/util.rs | 6 +++--- crates/graphics/src/lib.rs | 1 + src/main.rs | 1 + 8 files changed, 22 insertions(+), 23 deletions(-) diff --git a/crates/components/src/lib.rs b/crates/components/src/lib.rs index 1c69b545..f5f1c432 100644 --- a/crates/components/src/lib.rs +++ b/crates/components/src/lib.rs @@ -21,6 +21,7 @@ // it with Steamworks API by Valve Corporation, containing parts covered by // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] #![feature(is_sorted)] /// Syntax highlighter diff --git a/crates/eframe/src/web/storage.rs b/crates/eframe/src/web/storage.rs index 09af42a2..e92bb673 100644 --- a/crates/eframe/src/web/storage.rs +++ b/crates/eframe/src/web/storage.rs @@ -34,7 +34,7 @@ pub(crate) async fn load_memory(_: &egui::Context, _: &super::WorkerChannels) {} #[cfg(feature = "persistence")] pub(crate) fn save_memory(ctx: &egui::Context, channels: &super::WorkerChannels) { - match ctx.memory(|mem| ron::to_string(mem)) { + match ctx.memory(ron::to_string) { Ok(ron) => { let (oneshot_tx, oneshot_rx) = oneshot::channel(); channels.send(super::WebRunnerOutput::StorageSet( diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 8522d32f..c698a563 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -7,7 +7,7 @@ #![allow(unsafe_code)] // Luminol's wgpu resources are not Send or Sync on web. // We are doing this here to reduce merge conflicts, since it's likely wgpu will fix this. -#![allow(clippy::arc_with_non_send_sync)] +#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] pub use wgpu; diff --git a/crates/filesystem/src/project.rs b/crates/filesystem/src/project.rs index 4f51fb39..747bb429 100644 --- a/crates/filesystem/src/project.rs +++ b/crates/filesystem/src/project.rs @@ -450,21 +450,17 @@ impl FileSystem { global_config: &mut luminol_config::global::Config, ) -> Result { let entries = host.read_dir("")?; - if entries - .iter() - .find(|e| { - if let Some(extension) = e.path.extension() { - e.metadata.is_file - && (extension == "rxproj" - || extension == "rvproj" - || extension == "rvproj2" - || extension == "lumproj") - } else { - false - } - }) - .is_none() - { + if !entries.iter().any(|e| { + if let Some(extension) = e.path.extension() { + e.metadata.is_file + && (extension == "rxproj" + || extension == "rvproj" + || extension == "rvproj2" + || extension == "lumproj") + } else { + false + } + }) { return Err(Error::InvalidProjectFolder.into()); }; diff --git a/crates/filesystem/src/web/mod.rs b/crates/filesystem/src/web/mod.rs index 45e3299e..25cdcb5f 100644 --- a/crates/filesystem/src/web/mod.rs +++ b/crates/filesystem/src/web/mod.rs @@ -146,7 +146,7 @@ impl FileSystem { /// Returns whether or not the user's browser supports the JavaScript File System API. pub fn filesystem_supported() -> bool { - send_and_recv(|tx| FileSystemCommand::Supported(tx)) + send_and_recv(FileSystemCommand::Supported) } /// Attempts to prompt the user to choose a directory from their local machine using the @@ -159,7 +159,7 @@ impl FileSystem { if !Self::filesystem_supported() { return Err(Error::Wasm32FilesystemNotSupported).wrap_err(c); } - send_and_await(|tx| FileSystemCommand::DirPicker(tx)) + send_and_await(FileSystemCommand::DirPicker) .await .map(|(key, name)| Self { key, @@ -315,7 +315,7 @@ impl File { /// Creates a new empty temporary file with read-write permissions. pub fn new() -> std::io::Result { let c = "While creating a temporary file on a host filesystem"; - send_and_recv(|tx| FileSystemCommand::FileCreateTemp(tx)) + send_and_recv(FileSystemCommand::FileCreateTemp) .map(|(key, temp_file_name)| Self { key, path: None, diff --git a/crates/filesystem/src/web/util.rs b/crates/filesystem/src/web/util.rs index bd2af6c8..a1cd91fa 100644 --- a/crates/filesystem/src/web/util.rs +++ b/crates/filesystem/src/web/util.rs @@ -115,10 +115,10 @@ pub(super) async fn idb( // Create store for our directory handles if it doesn't exist db_req.set_on_upgrade_needed(Some(|e: &IdbVersionChangeEvent| { - if e.db() + if !e + .db() .object_store_names() - .find(|n| n == "filesystem.dir_handles") - .is_none() + .any(|n| n == "filesystem.dir_handles") { e.db().create_object_store("filesystem.dir_handles")?; } diff --git a/crates/graphics/src/lib.rs b/crates/graphics/src/lib.rs index 2882aaf2..07103b11 100644 --- a/crates/graphics/src/lib.rs +++ b/crates/graphics/src/lib.rs @@ -14,6 +14,7 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . +#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] pub mod binding_helpers; pub use binding_helpers::{BindGroupBuilder, BindGroupLayoutBuilder}; diff --git a/src/main.rs b/src/main.rs index a5135b1d..19ca6720 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ // it with Steamworks API by Valve Corporation, containing parts covered by // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +#![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(target_arch = "wasm32", no_main)] // there is no main function in web builds From 1a534013daf3e40da756d488919388631e818900 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 00:04:17 -0500 Subject: [PATCH 13/38] feat: implement most of the reporter UI --- Cargo.lock | 37 ++++++++ crates/core/src/window.rs | 16 ++++ crates/ui/Cargo.toml | 3 + crates/ui/src/windows/items.rs | 2 +- crates/ui/src/windows/mod.rs | 2 + crates/ui/src/windows/reporter.rs | 136 ++++++++++++++++++++++++++++++ src/app/mod.rs | 10 ++- src/main.rs | 15 ++-- 8 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 crates/ui/src/windows/reporter.rs diff --git a/Cargo.lock b/Cargo.lock index b566a926..3526916a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3290,7 +3290,9 @@ dependencies = [ "poll-promise", "qp-trie", "reqwest", + "strip-ansi-escapes", "strum", + "target-triple", "zip", ] @@ -5316,6 +5318,15 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -5486,6 +5497,12 @@ version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +[[package]] +name = "target-triple" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea67965c3f2666e59325eec14b6f0e296d27044051e65fc2a7d77ed3a3eff82d" + [[package]] name = "tempfile" version = "3.8.1" @@ -6129,6 +6146,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "vtparse" version = "0.6.2" diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs index 584a18cd..324d0960 100644 --- a/crates/core/src/window.rs +++ b/crates/core/src/window.rs @@ -41,6 +41,16 @@ pub struct EditWindows { type CleanFn = Box) -> bool>; impl Windows { + pub fn new() -> Self { + Default::default() + } + + pub fn new_with_windows(windows: Vec) -> Self { + Self { + windows: windows.into_iter().map(|w| Box::new(w) as Box<_>).collect(), + } + } + /// A function to add a window. pub fn add_window(&mut self, window: impl Window + 'static) { self.add_boxed_window(Box::new(window)) @@ -177,3 +187,9 @@ impl Window for Box { } } */ + +impl From>> for Windows { + fn from(windows: Vec>) -> Self { + Self { windows } + } +} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index b80e1fa6..687e3e99 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -35,6 +35,9 @@ camino.workspace = true strum.workspace = true git-version.workspace = true +target-triple = "0.1.2" + +strip-ansi-escapes = "0.2.0" poll-promise.workspace = true async-std.workspace = true diff --git a/crates/ui/src/windows/items.rs b/crates/ui/src/windows/items.rs index f0bbc86d..826dd0f4 100644 --- a/crates/ui/src/windows/items.rs +++ b/crates/ui/src/windows/items.rs @@ -54,7 +54,7 @@ impl luminol_core::Window for Window { } fn id(&self) -> egui::Id { - egui::Id::new("Item Editor") + egui::Id::new("item_editor") } fn requires_filesystem(&self) -> bool { diff --git a/crates/ui/src/windows/mod.rs b/crates/ui/src/windows/mod.rs index 5f103472..b619ddc7 100644 --- a/crates/ui/src/windows/mod.rs +++ b/crates/ui/src/windows/mod.rs @@ -45,6 +45,8 @@ pub mod map_picker; pub mod misc; /// New project window pub mod new_project; +/// The crash reporter. +pub mod reporter; /// The script editor pub mod script_edit; /// The sound test. diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs new file mode 100644 index 00000000..d829cfa3 --- /dev/null +++ b/crates/ui/src/windows/reporter.rs @@ -0,0 +1,136 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +/// Crash reporter window. +pub struct Window { + normalized_report: String, + json: ReportJson, +} + +struct ReportJson { + reporter_version: u32, + luminol_version: String, + target: String, + debug: bool, + report: String, +} + +impl Window { + pub fn new(report: impl Into) -> Self { + let report: String = report.into(); + + Self { + normalized_report: strip_ansi_escapes::strip_str(&report), + json: ReportJson { + reporter_version: 1, + luminol_version: git_version::git_version!().to_string(), + target: target_triple::target!().to_string(), + #[cfg(debug_assertions)] + debug: true, + #[cfg(not(debug_assertions))] + debug: false, + report, + }, + } + } +} + +impl luminol_core::Window for Window { + fn name(&self) -> String { + "Crash Reporter".into() + } + + fn id(&self) -> egui::Id { + egui::Id::new("reporter") + } + + fn requires_filesystem(&self) -> bool { + false + } + + fn show( + &mut self, + ctx: &egui::Context, + open: &mut bool, + _update_state: &mut luminol_core::UpdateState<'_>, + ) { + egui::Window::new(self.name()) + .id(egui::Id::new("reporter")) + .default_width(500.) + .open(open) + .show(ctx, |ui| { + ui.label("Luminol has crashed!"); + ui.label( + "Would you like to send the following crash report to the Luminol developers?", + ); + + ui.add_space(ui.spacing().indent); + + ui.label(format!("Luminol version: {}", self.json.luminol_version)); + ui.label(format!("Target platform: {}", self.json.target)); + ui.label(format!( + "Build profile: {}", + if self.json.debug { "debug" } else { "release" } + )); + + ui.group(|ui| { + ui.with_layout( + egui::Layout { + cross_justify: true, + ..Default::default() + }, + |ui| { + egui::ScrollArea::both().show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut self.normalized_report.as_str()) + .layouter(&mut |ui, text, wrap_width| { + // Make the text monospace and non-wrapping + egui::WidgetText::from(text) + .color( + ui.visuals() + .override_text_color + .unwrap_or_else(|| { + ui.visuals() + .widgets + .noninteractive + .fg_stroke + .color + }), + ) + .into_galley( + ui, + Some(false), + wrap_width, + egui::TextStyle::Monospace, + ) + .galley + }), + ); + }); + }, + ); + }); + }); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index a937f44b..5f1b96b8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -83,6 +83,7 @@ impl App { #[must_use] pub fn new( cc: &luminol_eframe::CreationContext<'_>, + report: Option, modified: luminol_core::ModifiedState, #[cfg(not(target_arch = "wasm32"))] log_term_rx: luminol_term::TermReceiver, #[cfg(not(target_arch = "wasm32"))] log_byte_rx: luminol_term::ByteReceiver, @@ -238,7 +239,14 @@ impl App { bytes_loader, toasts, - windows: luminol_core::Windows::default(), + windows: report.map_or_else( + luminol_core::Windows::new, + |report| { + luminol_core::Windows::new_with_windows(vec![ + luminol_ui::windows::reporter::Window::new(report), + ]) + }, + ), tabs: luminol_core::Tabs::new_with_tabs( "luminol_main_tabs", vec![luminol_ui::tabs::started::Tab::default()], diff --git a/src/main.rs b/src/main.rs index 19ca6720..bc14a6a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,11 +127,13 @@ impl Write for LogWriter { #[cfg(not(target_arch = "wasm32"))] fn main() { // Load the panic report from the previous run if it exists + let mut report = None; if let Some(path) = std::env::var_os("LUMINOL_PANIC_REPORT_FILE") { if let Ok(mut file) = std::fs::File::open(&path) { let mut buffer = String::new(); - let _success = file.read_to_string(&mut buffer).is_ok(); - // TODO: use this report to open a panic reporter + if file.read_to_string(&mut buffer).is_ok() { + report = Some(buffer); + } } let _ = std::fs::remove_file(path); } @@ -311,6 +313,7 @@ fn main() { CONTEXT.set(cc.egui_ctx.clone()).unwrap(); Box::new(app::App::new( cc, + report, Default::default(), log_term_rx, log_byte_rx, @@ -337,6 +340,7 @@ const CANVAS_ID: &str = "luminol-canvas"; #[cfg(target_arch = "wasm32")] struct WorkerData { + report: Option, audio: luminol_audio::AudioWrapper, modified: luminol_core::ModifiedState, prefers_color_scheme_dark: Option, @@ -351,8 +355,7 @@ static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex: #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn luminol_main_start(fallback: bool) { - // TODO: use this report to open a panic reporter - let _report = get_panic_report(); + let report = get_panic_report(); let worker_cell = std::rc::Rc::new(once_cell::unsync::OnceCell::::new()); let before_unload_cell = std::rc::Rc::new(std::cell::RefCell::new( @@ -457,6 +460,7 @@ pub fn luminol_main_start(fallback: bool) { let modified = luminol_core::ModifiedState::default(); *WORKER_DATA.lock() = Some(WorkerData { + report, audio: luminol_audio::Audio::default().into(), modified: modified.clone(), prefers_color_scheme_dark, @@ -504,6 +508,7 @@ pub fn luminol_main_start(fallback: bool) { #[wasm_bindgen] pub async fn luminol_worker_start(canvas: web_sys::OffscreenCanvas) { let WorkerData { + report, audio, modified, prefers_color_scheme_dark, @@ -520,7 +525,7 @@ pub async fn luminol_worker_start(canvas: web_sys::OffscreenCanvas) { .start( canvas, web_options, - Box::new(|cc| Box::new(app::App::new(cc, modified, audio))), + Box::new(|cc| Box::new(app::App::new(cc, report, modified, audio))), luminol_eframe::web::WorkerOptions { prefers_color_scheme_dark, channels: runner_worker_channels, From 2cb71e8672fb4d181d346fa2b70c479869dd84f6 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 00:10:08 -0500 Subject: [PATCH 14/38] chore: rustfmt --- src/app/mod.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 5f1b96b8..d88b6f8b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -239,14 +239,11 @@ impl App { bytes_loader, toasts, - windows: report.map_or_else( - luminol_core::Windows::new, - |report| { - luminol_core::Windows::new_with_windows(vec![ - luminol_ui::windows::reporter::Window::new(report), - ]) - }, - ), + windows: report.map_or_else(luminol_core::Windows::new, |report| { + luminol_core::Windows::new_with_windows(vec![ + luminol_ui::windows::reporter::Window::new(report), + ]) + }), tabs: luminol_core::Tabs::new_with_tabs( "luminol_main_tabs", vec![luminol_ui::tabs::started::Tab::default()], From 8ee23eadf51ea9c80e23a4032b3402e84aed19c1 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 01:05:25 -0500 Subject: [PATCH 15/38] feat: implement sending reports --- Cargo.lock | 5 +- crates/ui/Cargo.toml | 4 +- crates/ui/src/windows/reporter.rs | 131 +++++++++++++++++++++++------- 3 files changed, 109 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3526916a..6c0eac0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3290,6 +3290,7 @@ dependencies = [ "poll-promise", "qp-trie", "reqwest", + "serde", "strip-ansi-escapes", "strum", "target-triple", @@ -4687,9 +4688,9 @@ checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.5", "bytes", diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 687e3e99..69e077a6 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -34,6 +34,8 @@ camino.workspace = true strum.workspace = true +serde.workspace = true + git-version.workspace = true target-triple = "0.1.2" @@ -42,7 +44,7 @@ strip-ansi-escapes = "0.2.0" poll-promise.workspace = true async-std.workspace = true futures-util = "0.3.30" -reqwest = "0.11.22" +reqwest = { version = "0.11.23", features = ["json"] } zip = { version = "0.6.6", default-features = false, features = ["deflate"] } diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index d829cfa3..4627165f 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -26,8 +26,10 @@ pub struct Window { normalized_report: String, json: ReportJson, + send_promise: Option>>, } +#[derive(Debug, Clone, serde::Serialize)] struct ReportJson { reporter_version: u32, luminol_version: String, @@ -52,6 +54,7 @@ impl Window { debug: false, report, }, + send_promise: None, } } } @@ -73,8 +76,10 @@ impl luminol_core::Window for Window { &mut self, ctx: &egui::Context, open: &mut bool, - _update_state: &mut luminol_core::UpdateState<'_>, + update_state: &mut luminol_core::UpdateState<'_>, ) { + let mut should_close = false; + egui::Window::new(self.name()) .id(egui::Id::new("reporter")) .default_width(500.) @@ -101,36 +106,106 @@ impl luminol_core::Window for Window { ..Default::default() }, |ui| { - egui::ScrollArea::both().show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(&mut self.normalized_report.as_str()) - .layouter(&mut |ui, text, wrap_width| { - // Make the text monospace and non-wrapping - egui::WidgetText::from(text) - .color( - ui.visuals() - .override_text_color - .unwrap_or_else(|| { - ui.visuals() - .widgets - .noninteractive - .fg_stroke - .color - }), - ) - .into_galley( - ui, - Some(false), - wrap_width, - egui::TextStyle::Monospace, - ) - .galley - }), - ); - }); + egui::ScrollArea::both() + .max_height(ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + )) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline( + &mut self.normalized_report.as_str(), + ) + .layouter( + &mut |ui, text, wrap_width| { + // Make the text monospace and non-wrapping + egui::WidgetText::from(text) + .color( + ui.visuals() + .override_text_color + .unwrap_or_else(|| { + ui.visuals() + .widgets + .noninteractive + .fg_stroke + .color + }), + ) + .into_galley( + ui, + Some(false), + wrap_width, + egui::TextStyle::Monospace, + ) + .galley + }, + ), + ); + }); }, ); }); + + ui.with_layout( + egui::Layout { + cross_justify: true, + cross_align: egui::Align::Center, + ..Default::default() + }, + |ui| { + if self.send_promise.is_none() { + ui.columns(2, |columns| { + if columns[0].button("Don't send").clicked() { + should_close = true; + } + + if columns[1].button("Send").clicked() { + let json = self.json.clone(); + self.send_promise = + Some(luminol_core::spawn_future(async move { + let client = reqwest::Client::new(); + let response = client + .post("http://localhost:3246") + .json(&json) + .send() + .await + .map_err(|e| color_eyre::eyre::eyre!(e))?; + if response.status().is_success() { + Ok(()) + } else { + Err(color_eyre::eyre::eyre!(format!( + "Request returned {}", + response.status() + ))) + } + })); + } + }); + } else { + ui.spinner(); + } + }, + ); }); + + if let Some(p) = self.send_promise.take() { + match p.try_take() { + Ok(Ok(())) => { + luminol_core::info!(update_state.toasts, "Crash report sent!"); + should_close = true; + } + Ok(Err(e)) => { + luminol_core::error!( + update_state.toasts, + e.wrap_err("Error sending crash report") + ); + } + Err(p) => self.send_promise = Some(p), + } + } + + if should_close { + *open = false; + } } } From dfd628c871c524218042b1c5453e1dd640ca9215 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 12:15:19 -0500 Subject: [PATCH 16/38] chore(web): update coi-serviceworker to commit 7b1d2a092d0d2dd2b7270b6f12f13605de26f214 --- assets/coi-serviceworker.js | 74 +++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/assets/coi-serviceworker.js b/assets/coi-serviceworker.js index af2caf1b..9901474c 100644 --- a/assets/coi-serviceworker.js +++ b/assets/coi-serviceworker.js @@ -60,23 +60,46 @@ if (typeof window === 'undefined') { } else { (() => { + const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf"); + window.sessionStorage.removeItem("coiReloadedBySelf"); + const coepDegrading = (reloadedBySelf == "coepdegrade"); + // You can customize the behavior of this script through a global `coi` variable. const coi = { - shouldRegister: () => true, + shouldRegister: () => !reloadedBySelf, shouldDeregister: () => false, - coepCredentialless: () => (window.chrome !== undefined || window.netscape !== undefined), + coepCredentialless: () => true, + coepDegrade: () => true, doReload: () => window.location.reload(), quiet: false, ...window.coi }; const n = navigator; + const controlling = n.serviceWorker && n.serviceWorker.controller; + + // Record the failure if the page is served by serviceWorker. + if (controlling && !window.crossOriginIsolated) { + window.sessionStorage.setItem("coiCoepHasFailed", "true"); + } + const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed"); - if (n.serviceWorker && n.serviceWorker.controller) { + if (controlling) { + // Reload only on the first failure. + const reloadToDegrade = coi.coepDegrade() && !( + coepDegrading || window.crossOriginIsolated + ); n.serviceWorker.controller.postMessage({ type: "coepCredentialless", - value: coi.coepCredentialless(), + value: (reloadToDegrade || coepHasFailed && coi.coepDegrade()) + ? false + : coi.coepCredentialless(), }); + if (reloadToDegrade) { + !coi.quiet && console.log("Reloading page to degrade COEP."); + window.sessionStorage.setItem("coiReloadedBySelf", "coepdegrade"); + coi.doReload("coepdegrade"); + } if (coi.shouldDeregister()) { n.serviceWorker.controller.postMessage({ type: "deregister" }); @@ -92,27 +115,32 @@ if (typeof window === 'undefined') { return; } - // In some environments (e.g. Chrome incognito mode) this won't be available - if (n.serviceWorker) { - n.serviceWorker.register(window.document.currentScript.src).then( - (registration) => { - !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope); + // In some environments (e.g. Firefox private mode) this won't be available + if (!n.serviceWorker) { + !coi.quiet && console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."); + return; + } - registration.addEventListener("updatefound", () => { - !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker."); - coi.doReload(); - }); + n.serviceWorker.register(window.document.currentScript.src).then( + (registration) => { + !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope); - // If the registration is active, but it's not controlling the page - if (registration.active && !n.serviceWorker.controller) { - !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker."); - coi.doReload(); - } - }, - (err) => { - !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err); + registration.addEventListener("updatefound", () => { + !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker."); + window.sessionStorage.setItem("coiReloadedBySelf", "updatefound"); + coi.doReload(); + }); + + // If the registration is active, but it's not controlling the page + if (registration.active && !n.serviceWorker.controller) { + !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker."); + window.sessionStorage.setItem("coiReloadedBySelf", "notcontrolling"); + coi.doReload(); } - ); - } + }, + (err) => { + !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err); + } + ); })(); } From ee427ae6869b5354254fa5c120948c69149c5805 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 13:32:55 -0500 Subject: [PATCH 17/38] fix(web): reporter now uses no-cors when sending reports --- .gitattributes | 1 - assets/{coi-serviceworker.js => sw.js} | 29 ++++++++++++++++++++++++-- crates/ui/src/windows/reporter.rs | 1 + index.html | 4 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) delete mode 100644 .gitattributes rename assets/{coi-serviceworker.js => sw.js} (81%) diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 8524696a..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -**/coi-serviceworker.js linguist-vendored diff --git a/assets/coi-serviceworker.js b/assets/sw.js similarity index 81% rename from assets/coi-serviceworker.js rename to assets/sw.js index 9901474c..11956305 100644 --- a/assets/coi-serviceworker.js +++ b/assets/sw.js @@ -1,4 +1,29 @@ -/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ +//! This is a slightly modified version of coi-serviceworker, commit 7b1d2a092d0d2dd2b7270b6f12f13605de26f214 +//! https://github.com/gzuidhof/coi-serviceworker +/*! + * MIT License + * + * Copyright (c) 2021 Guido Zuidhof + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. +*/ + let coepCredentialless = false; if (typeof window === 'undefined') { self.addEventListener("install", () => self.skipWaiting()); @@ -36,7 +61,7 @@ if (typeof window === 'undefined') { fetch(request) .then((response) => { if (response.status === 0) { - return response; + return new Response(); } const newHeaders = new Headers(response.headers); diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index 4627165f..4499a906 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -167,6 +167,7 @@ impl luminol_core::Window for Window { let response = client .post("http://localhost:3246") .json(&json) + .fetch_mode_no_cors() .send() .await .map_err(|e| color_eyre::eyre::eyre!(e))?; diff --git a/index.html b/index.html index 6b7e9e28..5a7b842a 100644 --- a/index.html +++ b/index.html @@ -25,7 +25,7 @@ - + @@ -130,7 +130,7 @@ - + From f6a6de5928808a25ef75ea5202e7ee8756cf53a4 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 13:48:24 -0500 Subject: [PATCH 18/38] fix: fix reporter window height being too small --- crates/ui/src/windows/reporter.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index 4499a906..79fbd49d 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -107,10 +107,14 @@ impl luminol_core::Window for Window { }, |ui| { egui::ScrollArea::both() - .max_height(ui.spacing().interact_size.y.max( - ui.text_style_height(&egui::TextStyle::Button) - + 2. * ui.spacing().button_padding.y, - )) + .max_height( + ui.available_height() + - ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + ) + - ui.spacing().item_spacing.y, + ) .show(ui, |ui| { ui.add( egui::TextEdit::multiline( From b15d4f4598188a7be4ee6ddf7f77ec1c0e0c2a43 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 13:56:51 -0500 Subject: [PATCH 19/38] fix(native): always print full backtraces --- src/main.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index bc14a6a8..616ec1af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -188,10 +188,8 @@ fn main() { std::process::abort(); }); - // Enable full backtraces unless the user manually set the RUST_BACKTRACE environment variable - if std::env::var("RUST_BACKTRACE").is_err() { - std::env::set_var("RUST_BACKTRACE", "full"); - } + // Enable full backtraces + std::env::set_var("RUST_BACKTRACE", "full"); // Set up hooks for formatting errors and panics let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() From c0b14ce527f2a71debc53dd1384d060429df538c Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 16:57:30 -0500 Subject: [PATCH 20/38] fix: don't restart after panicking if UI failed to start --- src/app/mod.rs | 4 ++++ src/main.rs | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index d88b6f8b..aa8d9f25 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -271,6 +271,8 @@ impl luminol_eframe::App for App { #[cfg(not(target_arch = "wasm32"))] ctx.input(|i| { if let Some(f) = i.raw.dropped_files.first() { + super::RESTART_AFTER_PANIC.store(true, std::sync::atomic::Ordering::Relaxed); + let path = f.path.clone().expect("dropped file has no path"); let path = camino::Utf8PathBuf::from_path_buf(path).expect("path was not utf8"); @@ -383,6 +385,8 @@ impl luminol_eframe::App for App { self.lumi.ui(ctx); + super::RESTART_AFTER_PANIC.store(true, std::sync::atomic::Ordering::Relaxed); + self.bytes_loader.load_unloaded_files(ctx, &self.filesystem); #[cfg(feature = "steamworks")] diff --git a/src/main.rs b/src/main.rs index 616ec1af..90b223be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,9 @@ use wasm_bindgen::prelude::*; /// Embedded icon 256x256 in size. const ICON: &[u8] = include_bytes!("../assets/icon-256.png"); +static RESTART_AFTER_PANIC: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + mod app; mod lumi; @@ -231,6 +234,10 @@ fn main() { let report = panic_hook.panic_report(info).to_string(); eprintln!("{report}"); + if !RESTART_AFTER_PANIC.load(std::sync::atomic::Ordering::Relaxed) { + return; + } + let mut args = std::env::args_os(); let arg0 = args.next(); let exe_path = std::env::current_exe().map_or_else( @@ -381,7 +388,11 @@ pub fn luminol_main_start(fallback: bool) { ); } - set_panic_report(report); + if RESTART_AFTER_PANIC.load(std::sync::atomic::Ordering::Relaxed) { + set_panic_report(report); + } else { + let _ = web_sys::window().map(|window| window.alert_with_message("Luminol has crashed! Please check your browser's developer console for more details.")); + } } }); } From 01cd88d73125664ae12068b9d31df0d79ca014d2 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 18:01:58 -0500 Subject: [PATCH 21/38] fix(web): compute Git version before running Trunk hooks Without this, the Git version will always have "-modified" at the end in web builds even if the code is unmodified, because the Trunk hooks modify .cargo/config.toml. --- Cargo.lock | 2 -- Cargo.toml | 4 ---- crates/core/Cargo.toml | 2 +- crates/core/src/lib.rs | 19 +++++++++++++++++++ crates/core/src/toasts.rs | 2 +- crates/ui/Cargo.toml | 1 - crates/ui/src/windows/about.rs | 2 +- crates/ui/src/windows/reporter.rs | 2 +- hooks/trunk_enable_build_std.sh.cmd | 1 + .../trunk_enable_build_std_background.sh.cmd | 1 + hooks/trunk_enable_build_std_pre.sh | 8 +++++++- hooks/trunk_enable_build_std_pre.sh.cmd | 9 ++++++++- src/main.rs | 5 +++-- 13 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c0eac0d..a39117fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2988,7 +2988,6 @@ dependencies = [ "egui", "egui_extras", "futures-lite 2.1.0", - "git-version", "image 0.24.7", "js-sys", "luminol-audio", @@ -3275,7 +3274,6 @@ dependencies = [ "color-eyre", "egui", "futures-util", - "git-version", "itertools", "luminol-audio", "luminol-components", diff --git a/Cargo.toml b/Cargo.toml index d581ae0a..b2912244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,8 +129,6 @@ tempfile = "3.8.1" rand = "0.8.5" -git-version = "0.3.9" - luminol-audio = { version = "0.4.0", path = "crates/audio/" } luminol-components = { version = "0.4.0", path = "crates/components/" } luminol-config = { version = "0.4.0", path = "crates/config/" } @@ -181,8 +179,6 @@ zstd = "0.13.0" async-std.workspace = true futures-lite.workspace = true -git-version.workspace = true - # Native [target.'cfg(not(target_arch = "wasm32"))'.dependencies] steamworks = { version = "0.10.0", optional = true } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index b26979e3..ccf29f08 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -38,7 +38,7 @@ alox-48.workspace = true rand.workspace = true -git-version.workspace = true +git-version = "0.3.9" luminol-audio.workspace = true luminol-config.workspace = true diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index ae6ff149..fe3f2263 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -22,6 +22,7 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +pub use git_version; use std::sync::Arc; pub use tracing; @@ -45,6 +46,24 @@ pub mod project_manager; pub use project_manager::spawn_future; pub use project_manager::ProjectManager; +#[cfg(not(target_arch = "wasm32"))] +/// Get Luminol's current Git version as a `&str`. +#[macro_export] +macro_rules! version { + () => { + $crate::git_version::git_version!() + }; +} + +#[cfg(target_arch = "wasm32")] +/// Get Luminol's current Git version as a `&str`. +#[macro_export] +macro_rules! version { + () => { + option_env!("LUMINOL_VERSION").expect("could not get Luminol version") + }; +} + pub struct UpdateState<'res> { pub ctx: &'res egui::Context, diff --git a/crates/core/src/toasts.rs b/crates/core/src/toasts.rs index 54387f0c..55de7e49 100644 --- a/crates/core/src/toasts.rs +++ b/crates/core/src/toasts.rs @@ -67,7 +67,7 @@ impl Toasts { #[doc(hidden)] pub fn _e_add_version_section(error: color_eyre::Report) -> color_eyre::Report { - error.section(format!("Luminol version: {}", git_version::git_version!())) + error.section(format!("Luminol version: {}", crate::version!())) } #[doc(hidden)] diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 69e077a6..6d18f72d 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -36,7 +36,6 @@ strum.workspace = true serde.workspace = true -git-version.workspace = true target-triple = "0.1.2" strip-ansi-escapes = "0.2.0" diff --git a/crates/ui/src/windows/about.rs b/crates/ui/src/windows/about.rs index d4ce6948..21617989 100644 --- a/crates/ui/src/windows/about.rs +++ b/crates/ui/src/windows/about.rs @@ -69,7 +69,7 @@ impl luminol_core::Window for Window { ui.separator(); ui.label(format!("Luminol version {}", env!("CARGO_PKG_VERSION"))); - ui.label(format!("git-rev {}", git_version::git_version!())); + ui.label(format!("git-rev {}", luminol_core::version!())); ui.separator(); ui.label("Luminol is a FOSS version of the RPG Maker XP editor."); diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index 79fbd49d..6a78378f 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -46,7 +46,7 @@ impl Window { normalized_report: strip_ansi_escapes::strip_str(&report), json: ReportJson { reporter_version: 1, - luminol_version: git_version::git_version!().to_string(), + luminol_version: luminol_core::version!().to_string(), target: target_triple::target!().to_string(), #[cfg(debug_assertions)] debug: true, diff --git a/hooks/trunk_enable_build_std.sh.cmd b/hooks/trunk_enable_build_std.sh.cmd index 6c28a0a9..05d263db 100644 --- a/hooks/trunk_enable_build_std.sh.cmd +++ b/hooks/trunk_enable_build_std.sh.cmd @@ -1,3 +1,4 @@ @echo off +setlocal start /b %TRUNK_SOURCE_DIR%\hooks\trunk_enable_build_std_background.sh diff --git a/hooks/trunk_enable_build_std_background.sh.cmd b/hooks/trunk_enable_build_std_background.sh.cmd index d0f587cd..d5d27c80 100644 --- a/hooks/trunk_enable_build_std_background.sh.cmd +++ b/hooks/trunk_enable_build_std_background.sh.cmd @@ -1,4 +1,5 @@ @echo off +setlocal :: Wait until Trunk errors out or builds successfully, then restore the old Cargo config :loop diff --git a/hooks/trunk_enable_build_std_pre.sh b/hooks/trunk_enable_build_std_pre.sh index 983b99bf..47df86dd 100755 --- a/hooks/trunk_enable_build_std_pre.sh +++ b/hooks/trunk_enable_build_std_pre.sh @@ -1,8 +1,14 @@ #!/bin/sh set -e -# Enable std support for multithreading +git_version=$(git describe --always --dirty=-modified) + +# Enable std support for multithreading and set the LUMINOL_VERSION environment variable [ ! -f $TRUNK_SOURCE_DIR/.cargo/config.toml.bak ] || mv $TRUNK_SOURCE_DIR/.cargo/config.toml.bak $TRUNK_SOURCE_DIR/.cargo/config.toml cp $TRUNK_SOURCE_DIR/.cargo/config.toml $TRUNK_SOURCE_DIR/.cargo/config.toml.bak + +echo '[env]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml +echo "LUMINOL_VERSION = { value = \"$git_version\", force = true }" >> $TRUNK_SOURCE_DIR/.cargo/config.toml + echo '[unstable]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml echo 'build-std = ["std", "panic_abort"]' >> $TRUNK_SOURCE_DIR/.cargo/config.toml diff --git a/hooks/trunk_enable_build_std_pre.sh.cmd b/hooks/trunk_enable_build_std_pre.sh.cmd index 21354b1d..f609e79c 100644 --- a/hooks/trunk_enable_build_std_pre.sh.cmd +++ b/hooks/trunk_enable_build_std_pre.sh.cmd @@ -1,7 +1,14 @@ @echo off +setlocal -:: Enable std support for multithreading +for /f "tokens=*" %%i in ('git describe --always --dirty=-modified') do set git_version=%%i + +:: Enable std support for multithreading and set the LUMINOL_VERSION environment variable if exist %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak move %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak %TRUNK_SOURCE_DIR%\.cargo\config.toml copy %TRUNK_SOURCE_DIR%\.cargo\config.toml %TRUNK_SOURCE_DIR%\.cargo\config.toml.bak + +echo [env] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml +echo LUMINOL_VERSION = { value = "%git_version%", force = true } >> %TRUNK_SOURCE_DIR%\.cargo\config.toml + echo [unstable] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml echo build-std = ["std", "panic_abort"] >> %TRUNK_SOURCE_DIR%\.cargo\config.toml diff --git a/src/main.rs b/src/main.rs index 90b223be..c8672260 100644 --- a/src/main.rs +++ b/src/main.rs @@ -196,7 +196,7 @@ fn main() { // Set up hooks for formatting errors and panics let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() - .panic_section(format!("Luminol version: {}", git_version::git_version!())) + .panic_section(format!("Luminol version: {}", luminol_core::version!())) .add_frame_filter(Box::new(|frames| { let filters = &[ "_", @@ -360,6 +360,7 @@ static WORKER_DATA: parking_lot::Mutex> = parking_lot::Mutex: #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn luminol_main_start(fallback: bool) { + // Load the panic report from the previous run if it exists let report = get_panic_report(); let worker_cell = std::rc::Rc::new(once_cell::unsync::OnceCell::::new()); @@ -399,7 +400,7 @@ pub fn luminol_main_start(fallback: bool) { // Set up hooks for formatting errors and panics let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() - .panic_section(format!("Luminol version: {}", git_version::git_version!())) + .panic_section(format!("Luminol version: {}", luminol_core::version!())) .into_hooks(); eyre_hook .install() From a366e7a6cf72eecc1dff023433c34aa471797f72 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 18:06:01 -0500 Subject: [PATCH 22/38] chore: rename `luminol_version` to `luminol_revision` --- crates/ui/src/windows/reporter.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index 6a78378f..00aa7689 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -32,7 +32,7 @@ pub struct Window { #[derive(Debug, Clone, serde::Serialize)] struct ReportJson { reporter_version: u32, - luminol_version: String, + luminol_revision: String, target: String, debug: bool, report: String, @@ -46,7 +46,7 @@ impl Window { normalized_report: strip_ansi_escapes::strip_str(&report), json: ReportJson { reporter_version: 1, - luminol_version: luminol_core::version!().to_string(), + luminol_revision: luminol_core::version!().to_string(), target: target_triple::target!().to_string(), #[cfg(debug_assertions)] debug: true, @@ -92,7 +92,7 @@ impl luminol_core::Window for Window { ui.add_space(ui.spacing().indent); - ui.label(format!("Luminol version: {}", self.json.luminol_version)); + ui.label(format!("Luminol version: {}", self.json.luminol_revision)); ui.label(format!("Target platform: {}", self.json.target)); ui.label(format!( "Build profile: {}", From bb0286a248245cc4fb4e3190b774d05fbf9dc1bf Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 19:13:48 -0500 Subject: [PATCH 23/38] feat(web): service worker now caches Luminol's files Luminol can now be run offline after loading it in a browser once. --- assets/main.js | 6 +++--- assets/sw.js | 25 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/assets/main.js b/assets/main.js index 9f6de497..89ec0620 100644 --- a/assets/main.js +++ b/assets/main.js @@ -43,13 +43,13 @@ window.restartLuminol = async function() { let fallback = false; let luminol; if (gpu) { - luminol = await import(`/luminol.js?v=${invalidator}`); + luminol = await import(`/luminol.js?luminol-invalidator=${invalidator}`); } else { try { - luminol = await import(`/luminol_webgl.js?v=${invalidator}`); + luminol = await import(`/luminol_webgl.js?luminol-invalidator=${invalidator}`); fallback = true; } catch (e) { - luminol = await import(`/luminol.js?v=${invalidator}`); + luminol = await import(`/luminol.js?luminol-invalidator=${invalidator}`); } } diff --git a/assets/sw.js b/assets/sw.js index 11956305..f561c4b2 100644 --- a/assets/sw.js +++ b/assets/sw.js @@ -24,6 +24,8 @@ * SOFTWARE. */ +const CACHE_NAME = "luminol"; + let coepCredentialless = false; if (typeof window === 'undefined') { self.addEventListener("install", () => self.skipWaiting()); @@ -56,9 +58,18 @@ if (typeof window === 'undefined') { ? new Request(r, { credentials: "omit", }) + : new URL(r.url).searchParams.has("luminol-invalidator") + ? (() => { + // Remove 'luminol-invalidator' from the request's query string if it exists in the query string + const url = new URL(r.url); + url.searchParams.delete("luminol-invalidator"); + return new Request(url, r); + })() : r; event.respondWith( - fetch(request) + self.caches + .match(request) + .then((cached) => cached || fetch(request)) // Respond with cached response if one exists for this request .then((response) => { if (response.status === 0) { return new Response(); @@ -73,11 +84,21 @@ if (typeof window === 'undefined') { } newHeaders.set("Cross-Origin-Opener-Policy", "same-origin"); - return new Response(response.body, { + const newResponse = new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders, }); + + // Auto-cache non-error, non-opaque responses for all same-origin requests + if (response.type === "error" || new URL(request.url).origin !== self.origin) { + return newResponse; + } else { + return self.caches + .open(CACHE_NAME) + .then((cache) => cache.put(request, newResponse.clone())) + .then(() => newResponse); + } }) .catch((e) => console.error(e)) ); From 6326c0666622952e6b1022bfd1d01518d585abdf Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 19:21:41 -0500 Subject: [PATCH 24/38] chore(web): clippy --- crates/core/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index fe3f2263..d79601b2 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -59,9 +59,10 @@ macro_rules! version { /// Get Luminol's current Git version as a `&str`. #[macro_export] macro_rules! version { - () => { + () => {{ + #[allow(clippy::option_env_unwrap)] option_env!("LUMINOL_VERSION").expect("could not get Luminol version") - }; + }}; } pub struct UpdateState<'res> { From a1823ef66284cac5372b74801982184c6c33cc02 Mon Sep 17 00:00:00 2001 From: white-axe Date: Thu, 11 Jan 2024 21:35:15 -0500 Subject: [PATCH 25/38] style: use declarative `cfg` macro for report `debug` field --- crates/ui/src/windows/reporter.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index 00aa7689..10c29811 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -48,10 +48,7 @@ impl Window { reporter_version: 1, luminol_revision: luminol_core::version!().to_string(), target: target_triple::target!().to_string(), - #[cfg(debug_assertions)] - debug: true, - #[cfg(not(debug_assertions))] - debug: false, + debug: cfg!(debug_assertions), report, }, send_promise: None, From adcb06eeda41947b77a787d10a0da02b2bbad291 Mon Sep 17 00:00:00 2001 From: white-axe Date: Fri, 12 Jan 2024 12:09:22 -0500 Subject: [PATCH 26/38] fix(web): fix environment being inconsistent between page reloads This fixes a problem where sometimes, either cross-origin isolation would not be enabled at all or sw.js would have its own COEP (but not the COEP for other files) set to require-corp instead of credentialless. This would be fixed by refreshing the page a number of times. Now, COI should always be enabled and the COEP for sw.js should always be credentialless, without the user having to refresh the page: the page is automatically refreshed. --- assets/sw.js | 10 +++++++++- src/main.rs | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/assets/sw.js b/assets/sw.js index f561c4b2..c316d298 100644 --- a/assets/sw.js +++ b/assets/sw.js @@ -154,7 +154,15 @@ if (typeof window === 'undefined') { // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here. - if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return; + if (window.crossOriginIsolated !== false || !coi.shouldRegister()) { + // Reload once to set the COEP for this service worker as well + if (!window.sessionStorage.getItem("coiReloadedAfterSuccess")) { + !coi.quiet && console.log("Reloading page to set COEP for this service worker."); + window.sessionStorage.setItem("coiReloadedAfterSuccess", "true"); + coi.doReload("coepaftersuccess"); + } + return; + } if (!window.isSecureContext) { !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required."); diff --git a/src/main.rs b/src/main.rs index c8672260..b9ac4f6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -451,7 +451,8 @@ pub fn luminol_main_start(fallback: bool) { tracing_log::LogTracer::init().expect("failed to initialize tracing-log"); if !luminol_web::bindings::cross_origin_isolated() { - tracing::error!("Luminol requires Cross-Origin Isolation to be enabled in order to run."); + tracing::error!("Cross-Origin Isolation is not enabled. Reloading page to attempt to enable it."); + window.location().reload().expect("failed to reload page"); return; } From d648a39b47f8eef598b0d2eef4a4679c03eca1cf Mon Sep 17 00:00:00 2001 From: white-axe Date: Fri, 12 Jan 2024 12:11:15 -0500 Subject: [PATCH 27/38] chore: rustfmt --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index b9ac4f6c..706034f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -451,7 +451,9 @@ pub fn luminol_main_start(fallback: bool) { tracing_log::LogTracer::init().expect("failed to initialize tracing-log"); if !luminol_web::bindings::cross_origin_isolated() { - tracing::error!("Cross-Origin Isolation is not enabled. Reloading page to attempt to enable it."); + tracing::error!( + "Cross-Origin Isolation is not enabled. Reloading page to attempt to enable it." + ); window.location().reload().expect("failed to reload page"); return; } From 8dbb19a6b4b8ce064c4579d04e7d1852b641d666 Mon Sep 17 00:00:00 2001 From: white-axe Date: Fri, 12 Jan 2024 19:27:30 -0500 Subject: [PATCH 28/38] fix(native): remove report file after failing to restart --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 706034f1..5597c506 100644 --- a/src/main.rs +++ b/src/main.rs @@ -250,7 +250,7 @@ fn main() { .expect("failed to write to temporary file"); file.flush().expect("failed to flush temporary file"); let (_, path) = file.keep().expect("failed to persist temporary file"); - std::env::set_var("LUMINOL_PANIC_REPORT_FILE", path); + std::env::set_var("LUMINOL_PANIC_REPORT_FILE", &path); #[cfg(unix)] { @@ -258,12 +258,14 @@ fn main() { let error = std::process::Command::new(exe_path).args(args).exec(); eprintln!("Failed to restart Luminol: {error:?}"); + let _ = std::fs::remove_file(&path); } #[cfg(not(unix))] { if let Err(error) = std::process::Command::new(exe_path).args(args).spawn() { eprintln!("Failed to restart Luminol: {error:?}"); + let _ = std::fs::remove_file(&path); } } })); From 1cfbe5f85ab719fc64cc81137778fa97f20f9900 Mon Sep 17 00:00:00 2001 From: white-axe Date: Fri, 12 Jan 2024 19:34:05 -0500 Subject: [PATCH 29/38] fix: don't persist reporter's scroll position --- crates/ui/src/windows/reporter.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index 10c29811..d0779be5 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -27,6 +27,7 @@ pub struct Window { normalized_report: String, json: ReportJson, send_promise: Option>>, + first_render: bool, } #[derive(Debug, Clone, serde::Serialize)] @@ -52,6 +53,7 @@ impl Window { report, }, send_promise: None, + first_render: true, } } } @@ -103,7 +105,17 @@ impl luminol_core::Window for Window { ..Default::default() }, |ui| { + // Forget the scroll position from the last time the reporter opened + let scroll_area_id_source = "scroll_area"; + if self.first_render { + self.first_render = false; + let scroll_area_id = + ui.make_persistent_id(egui::Id::new(scroll_area_id_source)); + egui::scroll_area::State::default().store(ui.ctx(), scroll_area_id); + } + egui::ScrollArea::both() + .id_source(scroll_area_id_source) .max_height( ui.available_height() - ui.spacing().interact_size.y.max( From bec6b9138e594621b6b9b2ca155e60df514a12b5 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sat, 13 Jan 2024 00:06:32 -0500 Subject: [PATCH 30/38] fix(web): disable credentialless COEP Haha, turns out we don't even need credentialless COEP to send no-cors requests! The require-corp COEP works just fine. --- assets/sw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/sw.js b/assets/sw.js index c316d298..bffb1a85 100644 --- a/assets/sw.js +++ b/assets/sw.js @@ -114,7 +114,7 @@ if (typeof window === 'undefined') { const coi = { shouldRegister: () => !reloadedBySelf, shouldDeregister: () => false, - coepCredentialless: () => true, + coepCredentialless: () => false, coepDegrade: () => true, doReload: () => window.location.reload(), quiet: false, From 1d183cd18eb65edb9ab8fb55a740ad851eaa9d68 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sat, 13 Jan 2024 01:14:56 -0500 Subject: [PATCH 31/38] fix(web): undo last commit After further testing, it seems Firefox does require credentialless COEP for this type of no-cors request, so Chromium allowing it is probably a bug. --- assets/sw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/sw.js b/assets/sw.js index bffb1a85..c316d298 100644 --- a/assets/sw.js +++ b/assets/sw.js @@ -114,7 +114,7 @@ if (typeof window === 'undefined') { const coi = { shouldRegister: () => !reloadedBySelf, shouldDeregister: () => false, - coepCredentialless: () => false, + coepCredentialless: () => true, coepDegrade: () => true, doReload: () => window.location.reload(), quiet: false, From d1f58ff7077029d90fe833150da6ca88e0a79980 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sat, 13 Jan 2024 01:38:34 -0500 Subject: [PATCH 32/38] fix(web): increase robustness of the service worker This fixes some edge cases I experienced when testing in Firefox where the user refreshing the page sometimes causes the page's COEP to revert to "require-corp" (the service worker is supposed to set it to "credentialless"). I hate web development so much. --- assets/sw.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/sw.js b/assets/sw.js index c316d298..c9626394 100644 --- a/assets/sw.js +++ b/assets/sw.js @@ -163,6 +163,7 @@ if (typeof window === 'undefined') { } return; } + window.sessionStorage.removeItem("coiReloadedAfterSuccess"); if (!window.isSecureContext) { !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required."); From d015f86ff9890ab6160e244bcaf62aee63e66457 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sat, 13 Jan 2024 01:51:44 -0500 Subject: [PATCH 33/38] fix(web): fix Git revision not always being updated This fixes a bug where updating one of the HTML or JavaScript files (which changes the output of the `luminol_core::version!` macro) doesn't cause every crate that uses the `luminol_core::version!` macro to be recompiled, resulting in parts of Luminol getting different Git revision strings from this macro. --- crates/core/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index d79601b2..180e2e6a 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -60,8 +60,7 @@ macro_rules! version { #[macro_export] macro_rules! version { () => {{ - #[allow(clippy::option_env_unwrap)] - option_env!("LUMINOL_VERSION").expect("could not get Luminol version") + option_env!("LUMINOL_VERSION").unwrap_or($crate::git_version::git_version!()) }}; } From 2eb555e47c44852c041fef2b73881463e0338825 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sat, 20 Jan 2024 13:20:42 -0500 Subject: [PATCH 34/38] refactor: move Git version into `UpdateState` This prevents the Git revision from being miscalculated if some of the crates are at a different version than others. --- Cargo.lock | 3 ++- Cargo.toml | 2 ++ crates/core/Cargo.toml | 3 +-- crates/core/src/lib.rs | 25 ++++++++++--------------- crates/core/src/toasts.rs | 8 +++++++- crates/ui/src/windows/about.rs | 4 ++-- crates/ui/src/windows/reporter.rs | 4 ++-- src/app/mod.rs | 5 ++++- src/main.rs | 13 +++++++++++-- 9 files changed, 41 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 015eba10..4e6c2ae4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2988,6 +2988,7 @@ dependencies = [ "egui", "egui_extras", "futures-lite 2.1.0", + "git-version", "image 0.24.7", "js-sys", "luminol-audio", @@ -3089,13 +3090,13 @@ dependencies = [ "egui-modal", "egui-notify", "egui_dock", - "git-version", "itertools", "luminol-audio", "luminol-config", "luminol-data", "luminol-filesystem", "luminol-graphics", + "once_cell", "poll-promise", "rand", "serde", diff --git a/Cargo.toml b/Cargo.toml index b2912244..e29f10a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,6 +179,8 @@ zstd = "0.13.0" async-std.workspace = true futures-lite.workspace = true +git-version = "0.3.9" + # Native [target.'cfg(not(target_arch = "wasm32"))'.dependencies] steamworks = { version = "0.10.0", optional = true } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ccf29f08..cdada753 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -24,6 +24,7 @@ egui-notify = "0.11.0" egui-modal = "0.3.1" poll-promise.workspace = true +once_cell.workspace = true tracing.workspace = true color-eyre.workspace = true @@ -38,8 +39,6 @@ alox-48.workspace = true rand.workspace = true -git-version = "0.3.9" - luminol-audio.workspace = true luminol-config.workspace = true luminol-data.workspace = true diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 180e2e6a..fc2b0e82 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -22,7 +22,6 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. -pub use git_version; use std::sync::Arc; pub use tracing; @@ -46,22 +45,14 @@ pub mod project_manager; pub use project_manager::spawn_future; pub use project_manager::ProjectManager; -#[cfg(not(target_arch = "wasm32"))] -/// Get Luminol's current Git version as a `&str`. -#[macro_export] -macro_rules! version { - () => { - $crate::git_version::git_version!() - }; +thread_local! { + static GIT_REVISION: once_cell::unsync::OnceCell<&'static str> = once_cell::unsync::OnceCell::new(); } -#[cfg(target_arch = "wasm32")] -/// Get Luminol's current Git version as a `&str`. -#[macro_export] -macro_rules! version { - () => {{ - option_env!("LUMINOL_VERSION").unwrap_or($crate::git_version::git_version!()) - }}; +pub fn set_git_revision(revision: &'static str) { + GIT_REVISION.with(|git_revision| { + let _ = git_revision.set(revision); + }); } pub struct UpdateState<'res> { @@ -91,6 +82,8 @@ pub struct UpdateState<'res> { pub modified: ModifiedState, pub project_manager: &'res mut ProjectManager, + + pub git_revision: &'static str, } /// This stores whether or not there are unsaved changes in any file in the current project and is @@ -167,6 +160,7 @@ impl<'res> UpdateState<'res> { toolbar: self.toolbar, modified: self.modified.clone(), project_manager: self.project_manager, + git_revision: self.git_revision, } } @@ -189,6 +183,7 @@ impl<'res> UpdateState<'res> { toolbar: self.toolbar, modified: self.modified.clone(), project_manager: self.project_manager, + git_revision: self.git_revision, } } diff --git a/crates/core/src/toasts.rs b/crates/core/src/toasts.rs index 55de7e49..ad8e4875 100644 --- a/crates/core/src/toasts.rs +++ b/crates/core/src/toasts.rs @@ -67,7 +67,13 @@ impl Toasts { #[doc(hidden)] pub fn _e_add_version_section(error: color_eyre::Report) -> color_eyre::Report { - error.section(format!("Luminol version: {}", crate::version!())) + crate::GIT_REVISION.with(|git_revision| { + if let Some(git_revision) = git_revision.get() { + error.section(format!("Luminol version: {git_revision}")) + } else { + error + } + }) } #[doc(hidden)] diff --git a/crates/ui/src/windows/about.rs b/crates/ui/src/windows/about.rs index 21617989..d4e8c888 100644 --- a/crates/ui/src/windows/about.rs +++ b/crates/ui/src/windows/about.rs @@ -54,7 +54,7 @@ impl luminol_core::Window for Window { &mut self, ctx: &egui::Context, open: &mut bool, - _update_state: &mut luminol_core::UpdateState<'_>, + update_state: &mut luminol_core::UpdateState<'_>, ) { // Show the window. Name it "About Luminol" egui::Window::new("About Luminol") @@ -69,7 +69,7 @@ impl luminol_core::Window for Window { ui.separator(); ui.label(format!("Luminol version {}", env!("CARGO_PKG_VERSION"))); - ui.label(format!("git-rev {}", luminol_core::version!())); + ui.label(format!("git-rev {}", update_state.git_revision)); ui.separator(); ui.label("Luminol is a FOSS version of the RPG Maker XP editor."); diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index d0779be5..1c5eba5e 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -40,14 +40,14 @@ struct ReportJson { } impl Window { - pub fn new(report: impl Into) -> Self { + pub fn new(report: impl Into, git_revision: impl Into) -> Self { let report: String = report.into(); Self { normalized_report: strip_ansi_escapes::strip_str(&report), json: ReportJson { reporter_version: 1, - luminol_revision: luminol_core::version!().to_string(), + luminol_revision: git_revision.into(), target: target_triple::target!().to_string(), debug: cfg!(debug_assertions), report, diff --git a/src/app/mod.rs b/src/app/mod.rs index aa8d9f25..a0f2256a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -91,6 +91,8 @@ impl App { #[cfg(target_arch = "wasm32")] audio: luminol_audio::AudioWrapper, #[cfg(feature = "steamworks")] steamworks: Steamworks, ) -> Self { + luminol_core::set_git_revision(crate::git_revision()); + let render_state = cc .wgpu_render_state .clone() @@ -241,7 +243,7 @@ impl App { toasts, windows: report.map_or_else(luminol_core::Windows::new, |report| { luminol_core::Windows::new_with_windows(vec![ - luminol_ui::windows::reporter::Window::new(report), + luminol_ui::windows::reporter::Window::new(report, crate::git_revision()), ]) }), tabs: luminol_core::Tabs::new_with_tabs( @@ -312,6 +314,7 @@ impl luminol_eframe::App for App { toolbar: &mut self.toolbar, modified: self.modified.clone(), project_manager: &mut self.project_manager, + git_revision: crate::git_revision(), }; // If a file/folder picker is open, prevent the user from interacting with the application diff --git a/src/main.rs b/src/main.rs index 5597c506..1cd7972d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,15 @@ compile_error!("Steamworks is not supported on webassembly"); #[cfg(feature = "steamworks")] mod steam; +pub fn git_revision() -> &'static str { + #[cfg(not(target_arch = "wasm32"))] + { + git_version::git_version!() + } + #[cfg(target_arch = "wasm32")] + option_env!("LUMINOL_VERSION").unwrap_or(git_version::git_version!()) +} + #[cfg(not(target_arch = "wasm32"))] /// A writer that copies whatever is written to it to two other writers. struct CopyWriter(A, B); @@ -196,7 +205,7 @@ fn main() { // Set up hooks for formatting errors and panics let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() - .panic_section(format!("Luminol version: {}", luminol_core::version!())) + .panic_section(format!("Luminol version: {}", git_revision())) .add_frame_filter(Box::new(|frames| { let filters = &[ "_", @@ -402,7 +411,7 @@ pub fn luminol_main_start(fallback: bool) { // Set up hooks for formatting errors and panics let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() - .panic_section(format!("Luminol version: {}", luminol_core::version!())) + .panic_section(format!("Luminol version: {}", git_revision())) .into_hooks(); eyre_hook .install() From 9a2db7322986c9917008e61a53cc56123a2b4a50 Mon Sep 17 00:00:00 2001 From: white-axe Date: Sat, 20 Jan 2024 14:24:24 -0500 Subject: [PATCH 35/38] style: remove unnecessary `thread_local` in `luminol_core` --- crates/core/src/lib.rs | 8 ++------ crates/core/src/toasts.rs | 12 +++++------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index fc2b0e82..fa0553f6 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -45,14 +45,10 @@ pub mod project_manager; pub use project_manager::spawn_future; pub use project_manager::ProjectManager; -thread_local! { - static GIT_REVISION: once_cell::unsync::OnceCell<&'static str> = once_cell::unsync::OnceCell::new(); -} +static GIT_REVISION: once_cell::sync::OnceCell<&'static str> = once_cell::sync::OnceCell::new(); pub fn set_git_revision(revision: &'static str) { - GIT_REVISION.with(|git_revision| { - let _ = git_revision.set(revision); - }); + let _ = GIT_REVISION.set(revision); } pub struct UpdateState<'res> { diff --git a/crates/core/src/toasts.rs b/crates/core/src/toasts.rs index ad8e4875..d5dc3903 100644 --- a/crates/core/src/toasts.rs +++ b/crates/core/src/toasts.rs @@ -67,13 +67,11 @@ impl Toasts { #[doc(hidden)] pub fn _e_add_version_section(error: color_eyre::Report) -> color_eyre::Report { - crate::GIT_REVISION.with(|git_revision| { - if let Some(git_revision) = git_revision.get() { - error.section(format!("Luminol version: {git_revision}")) - } else { - error - } - }) + if let Some(git_revision) = crate::GIT_REVISION.get() { + error.section(format!("Luminol version: {git_revision}")) + } else { + error + } } #[doc(hidden)] From 02219cfff9060e3bda829caa179f093dac523f43 Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 31 Jan 2024 11:07:19 -0500 Subject: [PATCH 36/38] feat: add wgpu backend to report info Since wgpu errors result in panics, this seems like a useful thing to have. --- crates/ui/src/windows/reporter.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index 1c5eba5e..af9cf8f9 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -35,6 +35,7 @@ struct ReportJson { reporter_version: u32, luminol_revision: String, target: String, + wgpu_backend: String, debug: bool, report: String, } @@ -49,6 +50,7 @@ impl Window { reporter_version: 1, luminol_revision: git_revision.into(), target: target_triple::target!().to_string(), + wgpu_backend: "empty".into(), debug: cfg!(debug_assertions), report, }, @@ -77,6 +79,17 @@ impl luminol_core::Window for Window { open: &mut bool, update_state: &mut luminol_core::UpdateState<'_>, ) { + if self.first_render { + self.json.wgpu_backend = update_state + .graphics + .render_state + .adapter + .get_info() + .backend + .to_str() + .to_string(); + } + let mut should_close = false; egui::Window::new(self.name()) @@ -93,6 +106,7 @@ impl luminol_core::Window for Window { ui.label(format!("Luminol version: {}", self.json.luminol_revision)); ui.label(format!("Target platform: {}", self.json.target)); + ui.label(format!("Graphics backend: {}", self.json.wgpu_backend)); ui.label(format!( "Build profile: {}", if self.json.debug { "debug" } else { "release" } From d6979cd08f862dd7b293977b036326c862b594e6 Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 31 Jan 2024 12:09:48 -0500 Subject: [PATCH 37/38] chore: fix an update-induced compiler error --- crates/ui/src/windows/reporter.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index af9cf8f9..c5b5e520 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -164,7 +164,6 @@ impl luminol_core::Window for Window { wrap_width, egui::TextStyle::Monospace, ) - .galley }, ), ); From 65ba1937780e1987bef7ef902fba552de2b99b6a Mon Sep 17 00:00:00 2001 From: white-axe Date: Wed, 31 Jan 2024 12:11:57 -0500 Subject: [PATCH 38/38] style: use `UiExt` in reporter window --- crates/ui/src/windows/reporter.rs | 178 ++++++++++++++---------------- 1 file changed, 81 insertions(+), 97 deletions(-) diff --git a/crates/ui/src/windows/reporter.rs b/crates/ui/src/windows/reporter.rs index c5b5e520..bd6194d6 100644 --- a/crates/ui/src/windows/reporter.rs +++ b/crates/ui/src/windows/reporter.rs @@ -22,6 +22,8 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +use luminol_components::UiExt; + /// Crash reporter window. pub struct Window { normalized_report: String, @@ -113,106 +115,88 @@ impl luminol_core::Window for Window { )); ui.group(|ui| { - ui.with_layout( - egui::Layout { - cross_justify: true, - ..Default::default() - }, - |ui| { - // Forget the scroll position from the last time the reporter opened - let scroll_area_id_source = "scroll_area"; - if self.first_render { - self.first_render = false; - let scroll_area_id = - ui.make_persistent_id(egui::Id::new(scroll_area_id_source)); - egui::scroll_area::State::default().store(ui.ctx(), scroll_area_id); - } + ui.with_cross_justify(|ui| { + // Forget the scroll position from the last time the reporter opened + let scroll_area_id_source = "scroll_area"; + if self.first_render { + self.first_render = false; + let scroll_area_id = + ui.make_persistent_id(egui::Id::new(scroll_area_id_source)); + egui::scroll_area::State::default().store(ui.ctx(), scroll_area_id); + } - egui::ScrollArea::both() - .id_source(scroll_area_id_source) - .max_height( - ui.available_height() - - ui.spacing().interact_size.y.max( - ui.text_style_height(&egui::TextStyle::Button) - + 2. * ui.spacing().button_padding.y, - ) - - ui.spacing().item_spacing.y, - ) - .show(ui, |ui| { - ui.add( - egui::TextEdit::multiline( - &mut self.normalized_report.as_str(), - ) - .layouter( - &mut |ui, text, wrap_width| { - // Make the text monospace and non-wrapping - egui::WidgetText::from(text) - .color( - ui.visuals() - .override_text_color - .unwrap_or_else(|| { - ui.visuals() - .widgets - .noninteractive - .fg_stroke - .color - }), - ) - .into_galley( - ui, - Some(false), - wrap_width, - egui::TextStyle::Monospace, - ) - }, - ), - ); - }); - }, - ); + egui::ScrollArea::both() + .id_source(scroll_area_id_source) + .max_height( + ui.available_height() + - ui.spacing().interact_size.y.max( + ui.text_style_height(&egui::TextStyle::Button) + + 2. * ui.spacing().button_padding.y, + ) + - ui.spacing().item_spacing.y, + ) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut self.normalized_report.as_str()) + .layouter(&mut |ui, text, wrap_width| { + // Make the text monospace and non-wrapping + egui::WidgetText::from(text) + .color( + ui.visuals() + .override_text_color + .unwrap_or_else(|| { + ui.visuals() + .widgets + .noninteractive + .fg_stroke + .color + }), + ) + .into_galley( + ui, + Some(false), + wrap_width, + egui::TextStyle::Monospace, + ) + }), + ); + }); + }); }); - ui.with_layout( - egui::Layout { - cross_justify: true, - cross_align: egui::Align::Center, - ..Default::default() - }, - |ui| { - if self.send_promise.is_none() { - ui.columns(2, |columns| { - if columns[0].button("Don't send").clicked() { - should_close = true; - } - - if columns[1].button("Send").clicked() { - let json = self.json.clone(); - self.send_promise = - Some(luminol_core::spawn_future(async move { - let client = reqwest::Client::new(); - let response = client - .post("http://localhost:3246") - .json(&json) - .fetch_mode_no_cors() - .send() - .await - .map_err(|e| color_eyre::eyre::eyre!(e))?; - if response.status().is_success() { - Ok(()) - } else { - Err(color_eyre::eyre::eyre!(format!( - "Request returned {}", - response.status() - ))) - } - })); - } - }); - } else { - ui.spinner(); - } - }, - ); + ui.with_cross_justify_center(|ui| { + if self.send_promise.is_none() { + ui.columns(2, |columns| { + if columns[0].button("Don't send").clicked() { + should_close = true; + } + + if columns[1].button("Send").clicked() { + let json = self.json.clone(); + self.send_promise = Some(luminol_core::spawn_future(async move { + let client = reqwest::Client::new(); + let response = client + .post("http://localhost:3246") + .json(&json) + .fetch_mode_no_cors() + .send() + .await + .map_err(|e| color_eyre::eyre::eyre!(e))?; + if response.status().is_success() { + Ok(()) + } else { + Err(color_eyre::eyre::eyre!(format!( + "Request returned {}", + response.status() + ))) + } + })); + } + }); + } else { + ui.spinner(); + } + }); }); if let Some(p) = self.send_promise.take() {