From db18508d155a844677f0c4ef44e357226c43fc12 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 17 Dec 2024 11:38:53 +0100 Subject: [PATCH] Add `MainThreadToken` to ensure file-dialogs only run on the main thread (#8467) ### What We have a foot-gun in our code: our file dialogs (via `rfd`) [are only allowed to be run from the main thread (at least on Mac)](https://docs.rs/rfd/latest/rfd/#macos-non-windowed-applications-async-and-threading). However, there is nothing stopping a user from accidentally calling these functions from a background thread, and if you test it on e.g. Linux, it may very well work. So this PR introduces a new crate `re_capabilities` and a new type `MainThreadToken`. Any function that uses `rfd` should require the `MainThreadToken` as an argument. The `MainThreadToken` is only allowed to be created in `fn main`, and since it is neither `Send` nor `Sync`, this guarantees at compile-time that any functions that take a `MainThreadToken` can only be called from the main thread. NOTE: I have no way to enforce that all uses of `rfd` also require `MainThreadToken` - we need to remember this ourselves, but I've made sure that all our _current_ uses of `rfd` require a `MainThreadToken`. --- ARCHITECTURE.md | 1 + Cargo.lock | 13 ++++ Cargo.toml | 1 + crates/top/rerun-cli/src/bin/rerun.rs | 8 ++- crates/top/rerun/Cargo.toml | 3 +- crates/top/rerun/src/commands/entrypoint.rs | 6 +- crates/top/rerun/src/lib.rs | 2 + crates/top/rerun/src/native_viewer.rs | 6 +- crates/utils/re_capabilities/Cargo.toml | 35 +++++++++++ crates/utils/re_capabilities/README.md | 11 ++++ crates/utils/re_capabilities/src/lib.rs | 19 ++++++ .../re_capabilities/src/main_thread_token.rs | 60 +++++++++++++++++++ crates/utils/re_crash_handler/README.md | 2 +- crates/viewer/re_data_ui/Cargo.toml | 1 + crates/viewer/re_data_ui/src/blob.rs | 1 + crates/viewer/re_data_ui/src/instance_path.rs | 8 ++- crates/viewer/re_viewer/Cargo.toml | 1 + crates/viewer/re_viewer/src/app.rs | 19 +++--- crates/viewer/re_viewer/src/lib.rs | 2 + crates/viewer/re_viewer/src/loading.rs | 22 ++----- crates/viewer/re_viewer/src/native.rs | 6 ++ crates/viewer/re_viewer/src/web.rs | 6 +- crates/viewer/re_viewer_context/Cargo.toml | 1 + .../re_viewer_context/src/cache/caches.rs | 2 +- .../re_viewer_context/src/file_dialog.rs | 10 +++- examples/rust/custom_data_loader/src/main.rs | 10 +++- .../rust/custom_store_subscriber/src/main.rs | 12 +++- examples/rust/custom_view/src/main.rs | 4 ++ examples/rust/extend_viewer_ui/src/main.rs | 3 + lychee.toml | 2 +- 30 files changed, 235 insertions(+), 42 deletions(-) create mode 100644 crates/utils/re_capabilities/Cargo.toml create mode 100644 crates/utils/re_capabilities/README.md create mode 100644 crates/utils/re_capabilities/src/lib.rs create mode 100644 crates/utils/re_capabilities/src/main_thread_token.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7ac2edc0cfa9..abaa65b1725b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -209,6 +209,7 @@ Update instructions: | Crate | Description | |--------------------|--------------------------------------------------------------------------------------| | re_analytics | Rerun's analytics SDK | +| re_capabilities | Capability tokens | | re_case | Case conversions, the way Rerun likes them | | re_crash_handler | Detect panics and signals, logging them and optionally sending them to analytics. | | re_error | Helpers for handling errors. | diff --git a/Cargo.lock b/Cargo.lock index 7a74b631318a..2a5d6c58e6fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5592,6 +5592,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "re_capabilities" +version = "0.21.0-alpha.1+dev" +dependencies = [ + "document-features", + "egui", + "static_assertions", +] + [[package]] name = "re_case" version = "0.21.0-alpha.1+dev" @@ -5788,6 +5797,7 @@ dependencies = [ "image", "itertools 0.13.0", "nohash-hasher", + "re_capabilities", "re_chunk_store", "re_entity_db", "re_format", @@ -6786,6 +6796,7 @@ dependencies = [ "re_blueprint_tree", "re_build_info", "re_build_tools", + "re_capabilities", "re_chunk", "re_chunk_store", "re_chunk_store_ui", @@ -6867,6 +6878,7 @@ dependencies = [ "nohash-hasher", "once_cell", "parking_lot", + "re_capabilities", "re_chunk", "re_chunk_store", "re_data_source", @@ -7167,6 +7179,7 @@ dependencies = [ "re_analytics", "re_build_info", "re_build_tools", + "re_capabilities", "re_chunk", "re_chunk_store", "re_crash_handler", diff --git a/Cargo.toml b/Cargo.toml index 1d8cd74da2fc..39f77691d90b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ rerun-cli = { path = "crates/top/rerun-cli", version = "=0.21.0-alpha.1", defaul # crates/utils: re_analytics = { path = "crates/utils/re_analytics", version = "=0.21.0-alpha.1", default-features = false } +re_capabilities = { path = "crates/utils/re_capabilities", version = "=0.21.0-alpha.1", default-features = false } re_case = { path = "crates/utils/re_case", version = "=0.21.0-alpha.1", default-features = false } re_crash_handler = { path = "crates/utils/re_crash_handler", version = "=0.21.0-alpha.1", default-features = false } re_error = { path = "crates/utils/re_error", version = "=0.21.0-alpha.1", default-features = false } diff --git a/crates/top/rerun-cli/src/bin/rerun.rs b/crates/top/rerun-cli/src/bin/rerun.rs index 6c2ec7b1e55c..dbe64430b010 100644 --- a/crates/top/rerun-cli/src/bin/rerun.rs +++ b/crates/top/rerun-cli/src/bin/rerun.rs @@ -28,11 +28,17 @@ fn main() -> std::process::ExitCode { } fn main_impl() -> std::process::ExitCode { + let main_thread_token = rerun::MainThreadToken::i_promise_i_am_on_the_main_thread(); re_log::setup_logging(); let build_info = re_build_info::build_info!(); - let result = rerun::run(build_info, rerun::CallSource::Cli, std::env::args()); + let result = rerun::run( + main_thread_token, + build_info, + rerun::CallSource::Cli, + std::env::args(), + ); match result { Ok(exit_code) => std::process::ExitCode::from(exit_code), diff --git a/crates/top/rerun/Cargo.toml b/crates/top/rerun/Cargo.toml index d9739ec159f8..7c3aa2e113d7 100644 --- a/crates/top/rerun/Cargo.toml +++ b/crates/top/rerun/Cargo.toml @@ -124,6 +124,7 @@ web_viewer = ["server", "dep:re_web_viewer_server", "re_sdk?/web_viewer"] [dependencies] re_build_info.workspace = true +re_capabilities.workspace = true re_chunk.workspace = true re_crash_handler.workspace = true re_entity_db.workspace = true @@ -131,11 +132,11 @@ re_error.workspace = true re_format.workspace = true re_log_encoding.workspace = true re_log_types.workspace = true -re_video.workspace = true re_log.workspace = true re_memory.workspace = true re_smart_channel.workspace = true re_tracing.workspace = true +re_video.workspace = true anyhow.workspace = true document-features.workspace = true diff --git a/crates/top/rerun/src/commands/entrypoint.rs b/crates/top/rerun/src/commands/entrypoint.rs index bc437f46ce35..5ef16f301e40 100644 --- a/crates/top/rerun/src/commands/entrypoint.rs +++ b/crates/top/rerun/src/commands/entrypoint.rs @@ -539,6 +539,7 @@ enum Command { // It would be nice to use [`std::process::ExitCode`] here but // then there's no good way to get back at the exit code from python pub fn run( + main_thread_token: crate::MainThreadToken, build_info: re_build_info::BuildInfo, call_source: CallSource, args: I, @@ -595,7 +596,7 @@ where } } } else { - run_impl(build_info, call_source, args) + run_impl(main_thread_token, build_info, call_source, args) }; match res { @@ -620,6 +621,7 @@ where } fn run_impl( + main_thread_token: crate::MainThreadToken, _build_info: re_build_info::BuildInfo, call_source: CallSource, args: Args, @@ -836,8 +838,10 @@ fn run_impl( } else { #[cfg(feature = "native_viewer")] return re_viewer::run_native_app( + main_thread_token, Box::new(move |cc| { let mut app = re_viewer::App::new( + main_thread_token, _build_info, &call_source.app_env(), startup_options, diff --git a/crates/top/rerun/src/lib.rs b/crates/top/rerun/src/lib.rs index 2f6dbf8f1c8b..486e96a4ae7a 100644 --- a/crates/top/rerun/src/lib.rs +++ b/crates/top/rerun/src/lib.rs @@ -151,6 +151,8 @@ pub use re_entity_db::external::re_chunk_store::{ }; pub use re_log_types::StoreKind; +pub use re_capabilities::MainThreadToken; + /// To register a new external data loader, simply add an executable in your $PATH whose name /// starts with this prefix. // NOTE: this constant is duplicated in `re_data_source` to avoid an extra dependency here. diff --git a/crates/top/rerun/src/native_viewer.rs b/crates/top/rerun/src/native_viewer.rs index 84df33e69ebd..761ce0842c72 100644 --- a/crates/top/rerun/src/native_viewer.rs +++ b/crates/top/rerun/src/native_viewer.rs @@ -5,7 +5,10 @@ use re_log_types::LogMsg; /// /// ⚠️ This function must be called from the main thread since some platforms require that /// their UI runs on the main thread! ⚠️ -pub fn show(msgs: Vec) -> re_viewer::external::eframe::Result { +pub fn show( + main_thread_token: crate::MainThreadToken, + msgs: Vec, +) -> re_viewer::external::eframe::Result { if msgs.is_empty() { re_log::debug!("Empty array of msgs - call to show() ignored"); return Ok(()); @@ -18,6 +21,7 @@ pub fn show(msgs: Vec) -> re_viewer::external::eframe::Result { let startup_options = re_viewer::StartupOptions::default(); re_viewer::run_native_viewer_with_messages( + main_thread_token, re_build_info::build_info!(), re_viewer::AppEnvironment::from_store_source(&store_source), startup_options, diff --git a/crates/utils/re_capabilities/Cargo.toml b/crates/utils/re_capabilities/Cargo.toml new file mode 100644 index 000000000000..d4c36f8b600e --- /dev/null +++ b/crates/utils/re_capabilities/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "re_capabilities" +authors.workspace = true +description = "Capability tokens for the Rerun code base." +edition.workspace = true +homepage.workspace = true +include.workspace = true +license.workspace = true +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true + + +[features] +default = [] + +## Enable constructing the [`MainThreadToken`] from an [`egui::Ui`]. +egui = ["dep:egui"] + + +[dependencies] +# Internal dependencies: + +# External dependencies: +document-features.workspace = true +egui = { workspace = true, default-features = false, optional = true } +static_assertions.workspace = true diff --git a/crates/utils/re_capabilities/README.md b/crates/utils/re_capabilities/README.md new file mode 100644 index 000000000000..c61326593afe --- /dev/null +++ b/crates/utils/re_capabilities/README.md @@ -0,0 +1,11 @@ +# re_capabilities + +Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. + +[![Latest version](https://img.shields.io/crates/v/re_capabilities.svg)](https://crates.io/crates/re_capabilitiescrates/utils/) +[![Documentation](https://docs.rs/re_capabilities/badge.svg?speculative-link)](https://docs.rs/re_capabilities?speculative-link) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +Specifies capability tokens, required by different parts of the code base. +These are tokens passed down the call tree, to explicitly allow different capabilities in different parts of the code base. diff --git a/crates/utils/re_capabilities/src/lib.rs b/crates/utils/re_capabilities/src/lib.rs new file mode 100644 index 000000000000..dbaeb7ab46c8 --- /dev/null +++ b/crates/utils/re_capabilities/src/lib.rs @@ -0,0 +1,19 @@ +//! Specifies capability tokens, required by different parts of the code base. +//! These are tokens passed down the call tree, to explicitly allow different capabilities in different parts of the code base. +//! +//! For instance, the [`MainThreadToken`] is taken by argument in functions that needs to run on the main thread. +//! By requiring this token, you guarantee at compile-time that the function is only called on the main thread. +//! +//! All capability tokens should be created in the top-level of the call tree, +//! (i.e. in `fn main`) and passed down to all functions that require it. +//! That way you can be certain in what an area of code is allowed to do. +//! +//! See [`cap-std`](https://crates.io/crates/cap-std) for another capability-centric crate. +//! +//! ## Feature flags +#![doc = document_features::document_features!()] +//! + +mod main_thread_token; + +pub use main_thread_token::MainThreadToken; diff --git a/crates/utils/re_capabilities/src/main_thread_token.rs b/crates/utils/re_capabilities/src/main_thread_token.rs new file mode 100644 index 000000000000..bc8db90802e4 --- /dev/null +++ b/crates/utils/re_capabilities/src/main_thread_token.rs @@ -0,0 +1,60 @@ +use static_assertions::assert_not_impl_any; + +/// A token that (almost) proves we are on the main thread. +/// +/// Certain operations are only allowed on the main thread. +/// These operations should require this token. +/// For instance, any function using file dialogs (e.g. using [`rfd`](https://docs.rs/rfd/latest/rfd/)) should require this token. +/// +/// The token should only be constructed in `fn main`, using [`MainThreadToken::i_promise_i_am_on_the_main_thread`], +/// and then be passed down the call tree to where it is needed. +/// [`MainThreadToken`] is neither `Send` nor `Sync`, +/// thus guaranteeing that it cannot be found in other threads. +/// +/// Of course, there is nothing stopping you from calling [`MainThreadToken::i_promise_i_am_on_the_main_thread`] from a background thread, +/// but PLEASE DON'T DO THAT. +/// In other words, don't use this as a guarantee for unsafe code. +/// +/// There is also [`MainThreadToken::from_egui_ui`] which uses the implicit guarantee of egui +/// (which _usually_ is run on the main thread) to construct a [`MainThreadToken`]. +/// Use this only in a code base where you are sure that egui is running only on the main thread. +#[derive(Clone, Copy)] +pub struct MainThreadToken { + /// Prevent from being sent between threads. + /// + /// Workaround until `impl !Send for X {}` is stable. + _dont_send_me: std::marker::PhantomData<*const ()>, +} + +impl MainThreadToken { + /// Only call this from `fn main`, or you may get weird runtime errors! + pub fn i_promise_i_am_on_the_main_thread() -> Self { + // On web there is no thread name. + // On native the thread-name is always "main" in Rust, + // but there is nothing preventing a user from also naming another thread "main". + // In any case, since `MainThreadToken` is just best-effort, we only check this in debug builds. + #[cfg(not(target_arch = "wasm32"))] + debug_assert_eq!(std::thread::current().name(), Some("main"), + "DEBUG ASSERT: Trying to construct a MainThreadToken on a thread that is not the main thread!" + ); + + Self { + _dont_send_me: std::marker::PhantomData, + } + } + + /// We _should_ only create an [`egui::Ui`] on the main thread, + /// so having it is good enough to "prove" that we are on the main thread. + /// + /// Use this only in a code base where you are sure that egui is running only on the main thread. + /// + /// In theory there is nothing preventing anyone from creating a [`egui::Ui`] on another thread, + /// but practice that is unlikely (or intentionally malicious). + #[cfg(feature = "egui")] + pub fn from_egui_ui(_ui: &egui::Ui) -> Self { + Self::i_promise_i_am_on_the_main_thread() + } +} + +assert_not_impl_any!(MainThreadToken: Send, Sync); +assert_not_impl_any!(&MainThreadToken: Send, Sync); diff --git a/crates/utils/re_crash_handler/README.md b/crates/utils/re_crash_handler/README.md index 2d6aef65300d..363c7ddfc4bd 100644 --- a/crates/utils/re_crash_handler/README.md +++ b/crates/utils/re_crash_handler/README.md @@ -2,7 +2,7 @@ Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. -[![Latest version](https://img.shields.io/crates/v/re_crash_handler.svg)](https://crates.io/crates/utils/re_crash_handler) +[![Latest version](https://img.shields.io/crates/v/re_crash_handler.svg)](https://crates.io/crates/re_crash_handler) [![Documentation](https://docs.rs/re_crash_handler/badge.svg)](https://docs.rs/re_crash_handler) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) diff --git a/crates/viewer/re_data_ui/Cargo.toml b/crates/viewer/re_data_ui/Cargo.toml index bfc13fba300a..5cec64d98580 100644 --- a/crates/viewer/re_data_ui/Cargo.toml +++ b/crates/viewer/re_data_ui/Cargo.toml @@ -19,6 +19,7 @@ workspace = true all-features = true [dependencies] +re_capabilities = { workspace = true, features = ["egui"] } re_chunk_store.workspace = true re_entity_db.workspace = true re_format.workspace = true diff --git a/crates/viewer/re_data_ui/src/blob.rs b/crates/viewer/re_data_ui/src/blob.rs index 3246da88f532..2508772bd596 100644 --- a/crates/viewer/re_data_ui/src/blob.rs +++ b/crates/viewer/re_data_ui/src/blob.rs @@ -156,6 +156,7 @@ pub fn blob_preview_and_save_ui( } ctx.command_sender.save_file_dialog( + re_capabilities::MainThreadToken::from_egui_ui(ui), &file_name, "Save blob".to_owned(), blob.to_vec(), diff --git a/crates/viewer/re_data_ui/src/instance_path.rs b/crates/viewer/re_data_ui/src/instance_path.rs index c0b271a9b91c..c2bdecf82db7 100644 --- a/crates/viewer/re_data_ui/src/instance_path.rs +++ b/crates/viewer/re_data_ui/src/instance_path.rs @@ -389,8 +389,12 @@ fn image_download_button_ui( .map_or("image", |name| name.unescaped_str()) .to_owned() ); - ctx.command_sender - .save_file_dialog(&file_name, "Save image".to_owned(), png_bytes); + ctx.command_sender.save_file_dialog( + re_capabilities::MainThreadToken::from_egui_ui(ui), + &file_name, + "Save image".to_owned(), + png_bytes, + ); } Err(err) => { re_log::error!("{err}"); diff --git a/crates/viewer/re_viewer/Cargo.toml b/crates/viewer/re_viewer/Cargo.toml index 3b076ba1df97..497b6524c896 100644 --- a/crates/viewer/re_viewer/Cargo.toml +++ b/crates/viewer/re_viewer/Cargo.toml @@ -48,6 +48,7 @@ grpc = ["re_data_source/grpc", "dep:re_grpc_client"] # Internal: re_blueprint_tree.workspace = true re_build_info.workspace = true +re_capabilities.workspace = true re_chunk.workspace = true re_chunk_store.workspace = true re_component_ui.workspace = true diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index d2b77e887fea..6e2a2e441079 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use itertools::Itertools as _; use re_build_info::CrateVersion; +use re_capabilities::MainThreadToken; use re_data_source::{DataSource, FileContents}; use re_entity_db::entity_db::EntityDb; use re_log_types::{ApplicationId, FileSource, LogMsg, StoreKind}; @@ -158,6 +159,8 @@ struct PendingFilePromise { /// The Rerun Viewer as an [`eframe`] application. pub struct App { + #[allow(dead_code)] // Unused on wasm32 + main_thread_token: MainThreadToken, build_info: re_build_info::BuildInfo, startup_options: StartupOptions, start_time: web_time::Instant, @@ -222,6 +225,7 @@ pub struct App { impl App { /// Create a viewer that receives new log messages over time pub fn new( + main_thread_token: MainThreadToken, build_info: re_build_info::BuildInfo, app_env: &crate::AppEnvironment, startup_options: StartupOptions, @@ -304,6 +308,7 @@ impl App { }); Self { + main_thread_token, build_info, startup_options, start_time: web_time::Instant::now(), @@ -645,7 +650,7 @@ impl App { #[cfg(not(target_arch = "wasm32"))] UICommand::Open => { - for file_path in open_file_dialog_native() { + for file_path in open_file_dialog_native(self.main_thread_token) { self.command_sender .send_system(SystemCommand::LoadDataSource(DataSource::FilePath( FileSource::FileDialog { @@ -677,7 +682,7 @@ impl App { #[cfg(not(target_arch = "wasm32"))] UICommand::Import => { - for file_path in open_file_dialog_native() { + for file_path in open_file_dialog_native(self.main_thread_token) { self.command_sender .send_system(SystemCommand::LoadDataSource(DataSource::FilePath( FileSource::FileDialog { @@ -1648,6 +1653,7 @@ impl App { } else { let file_name = format!("{name}.png"); self.command_sender.save_file_dialog( + self.main_thread_token, &file_name, "Save screenshot".to_owned(), png_bytes, @@ -1684,11 +1690,7 @@ fn blueprint_loader() -> BlueprintPersistence { re_log::debug!("Trying to load blueprint for {app_id} from {blueprint_path:?}"); - let with_notifications = false; - - if let Some(bundle) = - crate::loading::load_blueprint_file(&blueprint_path, with_notifications) - { + if let Some(bundle) = crate::loading::load_blueprint_file(&blueprint_path) { for store in bundle.entity_dbs() { if store.store_kind() == StoreKind::Blueprint && !crate::blueprint::is_valid_blueprint(store) @@ -2092,8 +2094,9 @@ fn file_saver_progress_ui(egui_ctx: &egui::Context, background_tasks: &mut Backg } } +/// [This may only be called on the main thread](https://docs.rs/rfd/latest/rfd/#macos-non-windowed-applications-async-and-threading). #[cfg(not(target_arch = "wasm32"))] -fn open_file_dialog_native() -> Vec { +fn open_file_dialog_native(_: crate::MainThreadToken) -> Vec { re_tracing::profile_function!(); let supported: Vec<_> = if re_data_loader::iter_external_loaders().len() == 0 { diff --git a/crates/viewer/re_viewer/src/lib.rs b/crates/viewer/re_viewer/src/lib.rs index 3b6bede7f1cd..70875688acd8 100644 --- a/crates/viewer/re_viewer/src/lib.rs +++ b/crates/viewer/re_viewer/src/lib.rs @@ -30,6 +30,8 @@ pub(crate) use {app_state::AppState, ui::memory_panel}; pub use app::{App, StartupOptions}; +pub use re_capabilities::MainThreadToken; + pub mod external { pub use {eframe, egui}; pub use { diff --git a/crates/viewer/re_viewer/src/loading.rs b/crates/viewer/re_viewer/src/loading.rs index 476e3d969afb..90ef742c3b71 100644 --- a/crates/viewer/re_viewer/src/loading.rs +++ b/crates/viewer/re_viewer/src/loading.rs @@ -13,10 +13,7 @@ enum BlueprintLoadError { /// /// The file must be of a matching version of rerun. #[must_use] -pub fn load_blueprint_file( - path: &std::path::Path, - with_notifications: bool, -) -> Option { +pub fn load_blueprint_file(path: &std::path::Path) -> Option { fn load_file_path_impl(path: &std::path::Path) -> Result { re_tracing::profile_function!(); @@ -32,10 +29,6 @@ pub fn load_blueprint_file( match load_file_path_impl(path) { Ok(mut rrd) => { - if with_notifications { - re_log::info!("Loaded {path:?}"); - } - for entity_db in rrd.entity_dbs_mut() { entity_db.data_source = Some(re_smart_channel::SmartChannelSource::File(path.into())); @@ -45,16 +38,9 @@ pub fn load_blueprint_file( Err(err) => { let msg = format!("Failed loading {path:?}: {err}"); - if with_notifications { - re_log::error!("{msg}"); - rfd::MessageDialog::new() - .set_level(rfd::MessageLevel::Error) - .set_description(&msg) - .show(); - } else { - // Silently ignore - re_log::debug!("{msg}"); - } + // Silently ignore + re_log::debug!("{msg}"); + None } } diff --git a/crates/viewer/re_viewer/src/native.rs b/crates/viewer/re_viewer/src/native.rs index a79df3463777..5d48f24cbe39 100644 --- a/crates/viewer/re_viewer/src/native.rs +++ b/crates/viewer/re_viewer/src/native.rs @@ -1,3 +1,4 @@ +use re_capabilities::MainThreadToken; use re_log_types::LogMsg; /// Used by `eframe` to decide where to store the app state. @@ -7,6 +8,8 @@ type AppCreator = Box) -> Box, ) -> eframe::Result { @@ -79,6 +82,7 @@ fn icon_data() -> egui::IconData { } pub fn run_native_viewer_with_messages( + main_thread_token: MainThreadToken, build_info: re_build_info::BuildInfo, app_env: crate::AppEnvironment, startup_options: crate::StartupOptions, @@ -94,8 +98,10 @@ pub fn run_native_viewer_with_messages( let force_wgpu_backend = startup_options.force_wgpu_backend.clone(); run_native_app( + main_thread_token, Box::new(move |cc| { let mut app = crate::App::new( + main_thread_token, build_info, &app_env, startup_options, diff --git a/crates/viewer/re_viewer/src/web.rs b/crates/viewer/re_viewer/src/web.rs index 99b171c2c0a0..8d5ade891bfe 100644 --- a/crates/viewer/re_viewer/src/web.rs +++ b/crates/viewer/re_viewer/src/web.rs @@ -49,6 +49,8 @@ impl WebHandle { #[wasm_bindgen] pub async fn start(&self, canvas: JsValue) -> Result<(), wasm_bindgen::JsValue> { + let main_thread_token = crate::MainThreadToken::i_promise_i_am_on_the_main_thread(); + let canvas = if let Some(canvas_id) = canvas.as_string() { // For backwards compatibility with old JS/HTML written before 2024-08-30 let document = web_sys::window().unwrap().document().unwrap(); @@ -80,7 +82,7 @@ impl WebHandle { .start( canvas, web_options, - Box::new(move |cc| Ok(Box::new(create_app(cc, app_options)?))), + Box::new(move |cc| Ok(Box::new(create_app(main_thread_token, cc, app_options)?))), ) .await?; @@ -356,6 +358,7 @@ impl From for crate::app_blueprint::PanelStateOverrides { } fn create_app( + main_thread_token: crate::MainThreadToken, cc: &eframe::CreationContext<'_>, app_options: AppOptions, ) -> Result { @@ -408,6 +411,7 @@ fn create_app( crate::customize_eframe_and_setup_renderer(cc)?; let mut app = crate::App::new( + main_thread_token, build_info, &app_env, startup_options, diff --git a/crates/viewer/re_viewer_context/Cargo.toml b/crates/viewer/re_viewer_context/Cargo.toml index 71bd3774d201..0734eb629afd 100644 --- a/crates/viewer/re_viewer_context/Cargo.toml +++ b/crates/viewer/re_viewer_context/Cargo.toml @@ -19,6 +19,7 @@ workspace = true all-features = true [dependencies] +re_capabilities.workspace = true re_chunk_store.workspace = true re_chunk.workspace = true re_data_source.workspace = true diff --git a/crates/viewer/re_viewer_context/src/cache/caches.rs b/crates/viewer/re_viewer_context/src/cache/caches.rs index d35c6276cd09..f28a22c99942 100644 --- a/crates/viewer/re_viewer_context/src/cache/caches.rs +++ b/crates/viewer/re_viewer_context/src/cache/caches.rs @@ -56,7 +56,7 @@ impl Caches { /// A cache for memoizing things in order to speed up immediate mode UI & other immediate mode style things. /// -/// See also egus's cache system, in [`egui::cache`] (). +/// See also egus's cache system, in [`egui::cache`] (). pub trait Cache: std::any::Any + Send + Sync { /// Called once per frame to potentially flush the cache. /// diff --git a/crates/viewer/re_viewer_context/src/file_dialog.rs b/crates/viewer/re_viewer_context/src/file_dialog.rs index 9fbd18746b1b..fdf7e7d0e4c5 100644 --- a/crates/viewer/re_viewer_context/src/file_dialog.rs +++ b/crates/viewer/re_viewer_context/src/file_dialog.rs @@ -11,8 +11,16 @@ pub fn santitize_file_name(file_name: &str) -> String { impl CommandSender { /// Save some bytes to disk, after first showing a save dialog. + /// + /// [This may only be called on the main thread](https://docs.rs/rfd/latest/rfd/#macos-non-windowed-applications-async-and-threading). #[allow(clippy::unused_self)] // Not used on Wasm - pub fn save_file_dialog(&self, file_name: &str, title: String, data: Vec) { + pub fn save_file_dialog( + &self, + _: re_capabilities::MainThreadToken, + file_name: &str, + title: String, + data: Vec, + ) { re_tracing::profile_function!(); let file_name = santitize_file_name(file_name); diff --git a/examples/rust/custom_data_loader/src/main.rs b/examples/rust/custom_data_loader/src/main.rs index bde1dd382484..5db06d2680af 100644 --- a/examples/rust/custom_data_loader/src/main.rs +++ b/examples/rust/custom_data_loader/src/main.rs @@ -13,13 +13,19 @@ use rerun::{ }; fn main() -> anyhow::Result { + let main_thread_token = rerun::MainThreadToken::i_promise_i_am_on_the_main_thread(); re_log::setup_logging(); re_data_loader::register_custom_data_loader(HashLoader); let build_info = re_build_info::build_info!(); - rerun::run(build_info, rerun::CallSource::Cli, std::env::args()) - .map(std::process::ExitCode::from) + rerun::run( + main_thread_token, + build_info, + rerun::CallSource::Cli, + std::env::args(), + ) + .map(std::process::ExitCode::from) } // --- diff --git a/examples/rust/custom_store_subscriber/src/main.rs b/examples/rust/custom_store_subscriber/src/main.rs index d787980859f5..282b7db5ce88 100644 --- a/examples/rust/custom_store_subscriber/src/main.rs +++ b/examples/rust/custom_store_subscriber/src/main.rs @@ -8,7 +8,7 @@ //! //! # Log any kind of data from another terminal: //! $ cargo r -p objectron -- --connect -//! ``` +//! ```` use std::collections::BTreeMap; @@ -19,14 +19,20 @@ use rerun::{ }; fn main() -> anyhow::Result { + let main_thread_token = rerun::MainThreadToken::i_promise_i_am_on_the_main_thread(); re_log::setup_logging(); let _handle = re_chunk_store::ChunkStore::register_subscriber(Box::::default()); // Could use the returned handle to get a reference to the view if needed. let build_info = re_build_info::build_info!(); - rerun::run(build_info, rerun::CallSource::Cli, std::env::args()) - .map(std::process::ExitCode::from) + rerun::run( + main_thread_token, + build_info, + rerun::CallSource::Cli, + std::env::args(), + ) + .map(std::process::ExitCode::from) } // --- diff --git a/examples/rust/custom_view/src/main.rs b/examples/rust/custom_view/src/main.rs index 58e98f3b9a85..4522d2936758 100644 --- a/examples/rust/custom_view/src/main.rs +++ b/examples/rust/custom_view/src/main.rs @@ -13,6 +13,8 @@ static GLOBAL: re_memory::AccountingAllocator = re_memory::AccountingAllocator::new(mimalloc::MiMalloc); fn main() -> Result<(), Box> { + let main_thread_token = re_viewer::MainThreadToken::i_promise_i_am_on_the_main_thread(); + // Direct calls using the `log` crate to stderr. Control with `RUST_LOG=debug` etc. re_log::setup_logging(); @@ -39,8 +41,10 @@ fn main() -> Result<(), Box> { println!("Try for example to run: `cargo run -p minimal_options -- --connect` in another terminal instance."); re_viewer::run_native_app( + main_thread_token, Box::new(move |cc| { let mut app = re_viewer::App::new( + main_thread_token, re_viewer::build_info(), &app_env, startup_options, diff --git a/examples/rust/extend_viewer_ui/src/main.rs b/examples/rust/extend_viewer_ui/src/main.rs index 319a832909c8..76724ef27de2 100644 --- a/examples/rust/extend_viewer_ui/src/main.rs +++ b/examples/rust/extend_viewer_ui/src/main.rs @@ -12,6 +12,8 @@ static GLOBAL: re_memory::AccountingAllocator = re_memory::AccountingAllocator::new(mimalloc::MiMalloc); fn main() -> Result<(), Box> { + let main_thread_token = re_viewer::MainThreadToken::i_promise_i_am_on_the_main_thread(); + // Direct calls using the `log` crate to stderr. Control with `RUST_LOG=debug` etc. re_log::setup_logging(); @@ -45,6 +47,7 @@ fn main() -> Result<(), Box> { re_viewer::customize_eframe_and_setup_renderer(cc)?; let mut rerun_app = re_viewer::App::new( + main_thread_token, re_viewer::build_info(), &app_env, startup_options, diff --git a/lychee.toml b/lychee.toml index d11974584ef0..bb97b38fa5ab 100644 --- a/lychee.toml +++ b/lychee.toml @@ -103,7 +103,7 @@ exclude = [ 'https://overpass-api.de/api/interpreter', # Used by openstreetmap_data example # Avoid rate limiting. - 'https://crates.io/crates/.*', # Avoid crates.io rate-limiting + 'https://crates.io/crates/w\+', # Avoid crates.io rate-limiting 'https://github.com/rerun-io/rerun/commit/\.*', # Ignore links to our own commits (typically in changelog). 'https://github.com/rerun-io/rerun/pull/\.*', # Ignore links to our own pull requests (typically in changelog). 'https://github.com/rerun-io/rerun/issues/\.*', # Ignore links to our own issues.