From 489403c58becbe3afff6fc1c14a65614850c533f Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Wed, 19 Aug 2026 18:57:31 +0200 Subject: [PATCH] chore: switch command_dispatcher to ArcSwap i need this for patchbukkit, but this is also safer in terms of deadlocks --- crates/pumpkin/src/block/blocks/command.rs | 3 +-- crates/pumpkin/src/command/args/command.rs | 4 ++-- crates/pumpkin/src/command/commands/deop.rs | 2 +- crates/pumpkin/src/command/commands/help.rs | 10 ++++----- crates/pumpkin/src/command/commands/op.rs | 2 +- crates/pumpkin/src/command/dispatcher.rs | 2 +- crates/pumpkin/src/command/node/dispatcher.rs | 1 + crates/pumpkin/src/command/tree/mod.rs | 1 + crates/pumpkin/src/lib.rs | 6 ++--- crates/pumpkin/src/logging.rs | 2 +- .../src/net/bedrock/play/chat_command.rs | 2 +- .../pumpkin/src/net/java/play/chat_command.rs | 2 +- .../src/net/java/play/command_suggestion.rs | 3 +-- crates/pumpkin/src/net/rcon/mod.rs | 3 +-- crates/pumpkin/src/plugin/api/context.rs | 22 ++++++++++--------- .../loader/wasm/wasm_host/wit/v0_1/player.rs | 2 +- .../loader/wasm/wasm_host/wit/v0_1/server.rs | 6 ++--- crates/pumpkin/src/server/mod.rs | 4 ++-- crates/pumpkin/src/world/mod.rs | 4 ++-- 19 files changed, 40 insertions(+), 41 deletions(-) diff --git a/crates/pumpkin/src/block/blocks/command.rs b/crates/pumpkin/src/block/blocks/command.rs index 907fd4196..926175541 100644 --- a/crates/pumpkin/src/block/blocks/command.rs +++ b/crates/pumpkin/src/block/blocks/command.rs @@ -141,8 +141,7 @@ impl CommandBlock { server .command_dispatcher - .read() - .await + .load() .handle_command(&source, command) .await; } diff --git a/crates/pumpkin/src/command/args/command.rs b/crates/pumpkin/src/command/args/command.rs index 54b08de72..742815671 100644 --- a/crates/pumpkin/src/command/args/command.rs +++ b/crates/pumpkin/src/command/args/command.rs @@ -40,7 +40,7 @@ impl ArgumentConsumer for CommandTreeArgumentConsumer { }; Box::pin(async move { - let dispatcher = server.command_dispatcher.read().await; + let dispatcher = server.command_dispatcher.load(); dispatcher .fallback_dispatcher @@ -61,7 +61,7 @@ impl ArgumentConsumer for CommandTreeArgumentConsumer { return Ok(None); }; - let dispatcher = server.command_dispatcher.read().await; + let dispatcher = server.command_dispatcher.load(); let suggestions = dispatcher .fallback_dispatcher .commands diff --git a/crates/pumpkin/src/command/commands/deop.rs b/crates/pumpkin/src/command/commands/deop.rs index cadfe5e77..16e0d91af 100644 --- a/crates/pumpkin/src/command/commands/deop.rs +++ b/crates/pumpkin/src/command/commands/deop.rs @@ -41,7 +41,7 @@ impl CommandExecutor for Executor { succeeded_deops += 1; if let Some(player) = server.get_player_by_uuid(profile.id) { - let command_dispatcher = server.command_dispatcher.read().await; + let command_dispatcher = server.command_dispatcher.load(); player .set_permission_lvl( server, diff --git a/crates/pumpkin/src/command/commands/help.rs b/crates/pumpkin/src/command/commands/help.rs index 5bdeacc65..deb543d1b 100644 --- a/crates/pumpkin/src/command/commands/help.rs +++ b/crates/pumpkin/src/command/commands/help.rs @@ -116,7 +116,7 @@ impl HelpCommandExecutor { Box::pin(async move { let server = context.server(); - let dispatcher = server.command_dispatcher.read().await; + let dispatcher = server.command_dispatcher.load(); let commands = dispatcher .get_all_permitted_commands_usage(&context.source) .await; @@ -206,7 +206,7 @@ impl HelpCommandExecutor { fn command<'a>(context: &'a CommandContext, command: &'a str) -> CommandExecutorResult<'a> { Box::pin(async move { - let dispatcher = context.server().command_dispatcher.read().await; + let dispatcher = context.server().command_dispatcher.load(); let Some((description, usage)) = dispatcher .get_permitted_command_usage(&context.source, command) @@ -274,7 +274,7 @@ impl HelpCommandExecutor { fn plugin<'a>(context: &'a CommandContext, plugin_name: &'a str) -> CommandExecutorResult<'a> { Box::pin(async move { let server = context.server(); - let dispatcher = server.command_dispatcher.read().await; + let dispatcher = server.command_dispatcher.load(); let commands = dispatcher .get_all_permitted_commands_usage_by_plugin(&context.source, plugin_name) .await; @@ -335,7 +335,7 @@ impl HelpCommandExecutor { Box::pin(async move { // Prioritize commands ig { - let dispatcher = context.server().command_dispatcher.read().await; + let dispatcher = context.server().command_dispatcher.load(); if dispatcher .get_permitted_command_usage(&context.source, input) .await @@ -346,7 +346,7 @@ impl HelpCommandExecutor { } { - let dispatcher = context.server().command_dispatcher.read().await; + let dispatcher = context.server().command_dispatcher.load(); let plugin_commands = dispatcher .get_all_permitted_commands_usage_by_plugin(&context.source, input) .await; diff --git a/crates/pumpkin/src/command/commands/op.rs b/crates/pumpkin/src/command/commands/op.rs index fbbaeed52..4e9c863c4 100644 --- a/crates/pumpkin/src/command/commands/op.rs +++ b/crates/pumpkin/src/command/commands/op.rs @@ -50,7 +50,7 @@ impl CommandExecutor for OpCommandExecutor { } if let Some(player) = server.get_player_by_uuid(profile.id) { - let command_dispatcher = server.command_dispatcher.read().await; + let command_dispatcher = server.command_dispatcher.load(); player .set_permission_lvl(server, new_level, &command_dispatcher) .await; diff --git a/crates/pumpkin/src/command/dispatcher.rs b/crates/pumpkin/src/command/dispatcher.rs index 00c6935be..10103bb55 100644 --- a/crates/pumpkin/src/command/dispatcher.rs +++ b/crates/pumpkin/src/command/dispatcher.rs @@ -228,7 +228,7 @@ fn path_failure( } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct CommandDispatcher { pub commands: FxHashMap, pub permissions: FxHashMap, diff --git a/crates/pumpkin/src/command/node/dispatcher.rs b/crates/pumpkin/src/command/node/dispatcher.rs index b7ecc5140..4e075ced6 100644 --- a/crates/pumpkin/src/command/node/dispatcher.rs +++ b/crates/pumpkin/src/command/node/dispatcher.rs @@ -95,6 +95,7 @@ pub static RESULT_DEFERRER: LazyLock> = /// /// Internally, this dispatcher stores a [`Tree`]. Refer to its documentation /// for more information about nodes. +#[derive(Clone)] pub struct CommandDispatcher { pub tree: Tree, pub consumer: Arc, diff --git a/crates/pumpkin/src/command/tree/mod.rs b/crates/pumpkin/src/command/tree/mod.rs index 4d2aa2831..9f8df85ff 100644 --- a/crates/pumpkin/src/command/tree/mod.rs +++ b/crates/pumpkin/src/command/tree/mod.rs @@ -67,6 +67,7 @@ impl Debug for NodeType { } } +#[derive(Clone, Debug)] pub enum Command { Tree(CommandTree), Alias(String), diff --git a/crates/pumpkin/src/lib.rs b/crates/pumpkin/src/lib.rs index a90479804..7907c3bb9 100644 --- a/crates/pumpkin/src/lib.rs +++ b/crates/pumpkin/src/lib.rs @@ -722,8 +722,7 @@ fn setup_stdin_console(server: Arc) { if !event.cancelled { server .command_dispatcher - .read() - .await + .load() .handle_command( &command::CommandSender::Console.into_source(&server).await, command.as_str(), @@ -795,8 +794,7 @@ fn setup_console(mut rl: Editor, server: A if !event.cancelled { server .command_dispatcher - .read() - .await + .load() .handle_command( &command::CommandSender::Console.into_source(&server).await, &line, diff --git a/crates/pumpkin/src/logging.rs b/crates/pumpkin/src/logging.rs index 08c48293f..6fbf6b71b 100644 --- a/crates/pumpkin/src/logging.rs +++ b/crates/pumpkin/src/logging.rs @@ -476,7 +476,7 @@ impl Completer for PumpkinCommandCompleter { let ends_with_space = cmd.ends_with(' '); handle.block_on(async { - let dispatcher = server.command_dispatcher.read().await; + let dispatcher = server.command_dispatcher.load(); let source = CommandSender::Console.into_source(server).await; // Temporary setups to unify both dispatchers for now: diff --git a/crates/pumpkin/src/net/bedrock/play/chat_command.rs b/crates/pumpkin/src/net/bedrock/play/chat_command.rs index 48801db05..3eaa876bc 100644 --- a/crates/pumpkin/src/net/bedrock/play/chat_command.rs +++ b/crates/pumpkin/src/net/bedrock/play/chat_command.rs @@ -31,7 +31,7 @@ impl BedrockClient { // Some commands can take a long time to execute. If they do, they block packet processing for the player. // That's why we will spawn a task instead. server.spawn_task(async move { - let dispatcher = server_clone.command_dispatcher.read().await; + let dispatcher = server_clone.command_dispatcher.load(); dispatcher.handle_command( &player_clone.get_command_source(&server_clone).await, &command_clone diff --git a/crates/pumpkin/src/net/java/play/chat_command.rs b/crates/pumpkin/src/net/java/play/chat_command.rs index ceb793e13..1844047c0 100644 --- a/crates/pumpkin/src/net/java/play/chat_command.rs +++ b/crates/pumpkin/src/net/java/play/chat_command.rs @@ -28,7 +28,7 @@ impl JavaClient { // Some commands can take a long time to execute. If they do, they block packet processing for the player. // That's why we will spawn a task instead. server.spawn_task(async move { - let dispatcher = server_clone.command_dispatcher.read().await; + let dispatcher = server_clone.command_dispatcher.load(); dispatcher.handle_command( &player_clone.get_command_source(&server_clone).await, &command_clone diff --git a/crates/pumpkin/src/net/java/play/command_suggestion.rs b/crates/pumpkin/src/net/java/play/command_suggestion.rs index 7be007735..86363ffab 100644 --- a/crates/pumpkin/src/net/java/play/command_suggestion.rs +++ b/crates/pumpkin/src/net/java/play/command_suggestion.rs @@ -19,8 +19,7 @@ impl JavaClient { let suggestions = server .command_dispatcher - .read() - .await + .load() .suggest(cmd, &player.get_command_source(server).await) .await; diff --git a/crates/pumpkin/src/net/rcon/mod.rs b/crates/pumpkin/src/net/rcon/mod.rs index fdec1b578..4de057961 100644 --- a/crates/pumpkin/src/net/rcon/mod.rs +++ b/crates/pumpkin/src/net/rcon/mod.rs @@ -146,8 +146,7 @@ impl RCONClient { let _ = tokio::spawn(async move { server_clone .command_dispatcher - .read() - .await + .load() .handle_command(&command_source, &packet_body) .await; }) diff --git a/crates/pumpkin/src/plugin/api/context.rs b/crates/pumpkin/src/plugin/api/context.rs index 878448d60..00c85f1ef 100644 --- a/crates/pumpkin/src/plugin/api/context.rs +++ b/crates/pumpkin/src/plugin/api/context.rs @@ -175,12 +175,13 @@ impl Context { format!("{}:{permission}", self.metadata.name) }; - { - let mut dispatcher_lock = self.server.command_dispatcher.write().await; - dispatcher_lock + self.server.command_dispatcher.rcu(|dispatcher| { + let mut new_dispatcher = (**dispatcher).clone(); + new_dispatcher .fallback_dispatcher - .register(tree, full_permission_node); - }; + .register(tree.clone(), full_permission_node.clone()); + Arc::new(new_dispatcher) + }); self.reload_commands_for_everyone().await; } @@ -190,10 +191,11 @@ impl Context { /// # Arguments /// - `name`: The name of the command to unregister. pub async fn unregister_command(&self, name: &str) { - { - let mut dispatcher_lock = self.server.command_dispatcher.write().await; - dispatcher_lock.fallback_dispatcher.unregister(name); - }; + self.server.command_dispatcher.rcu(|dispatcher| { + let mut new_dispatcher = (**dispatcher).clone(); + new_dispatcher.fallback_dispatcher.unregister(name); + Arc::new(new_dispatcher) + }); self.reload_commands_for_everyone().await; } @@ -212,7 +214,7 @@ impl Context { /// # Arguments /// - `player`: The player for which the commands will be reloaded. pub async fn reload_commands_for(&self, player: &Arc) { - let command_dispatcher = self.server.command_dispatcher.read().await; + let command_dispatcher = self.server.command_dispatcher.load(); if let ClientPlatform::Bedrock(_) = player.client.as_ref() { client_suggestions::send_bedrock_commands_packet( player, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index debebed83..d3fa10563 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -1349,7 +1349,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { let player = player_from_resource(self, &player)?; let server = self.server.as_ref().expect("server not available"); let level = from_wit_permission_level(level); - let command_dispatcher = server.command_dispatcher.read().await; + let command_dispatcher = server.command_dispatcher.load(); player .set_permission_lvl(server, level, &command_dispatcher) .await; diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs index 30b25f15c..26077c9b6 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs @@ -348,7 +348,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { } }; - let dispatcher = server.command_dispatcher.read().await; + let dispatcher = server.command_dispatcher.load(); dispatcher .handle_command(&native_sender.into_source(server).await, &command) .await; @@ -700,7 +700,7 @@ impl pumpkin::plugin::server::HostOpManager for PluginHostState { drop(config); if let Some(player) = server.get_player_by_uuid(uuid) { - let command_dispatcher = server.command_dispatcher.read().await; + let command_dispatcher = server.command_dispatcher.load(); player .set_permission_lvl(server, internal_level, &command_dispatcher) .await; @@ -727,7 +727,7 @@ impl pumpkin::plugin::server::HostOpManager for PluginHostState { drop(config); if let Some(player) = server.get_player_by_uuid(uuid) { - let command_dispatcher = server.command_dispatcher.read().await; + let command_dispatcher = server.command_dispatcher.load(); player .set_permission_lvl( server, diff --git a/crates/pumpkin/src/server/mod.rs b/crates/pumpkin/src/server/mod.rs index 63d309426..77459a3b8 100644 --- a/crates/pumpkin/src/server/mod.rs +++ b/crates/pumpkin/src/server/mod.rs @@ -94,7 +94,7 @@ pub struct Server { /// Saves server branding information. branding: CachedBranding, /// Saves and dispatches commands to appropriate handlers. - pub command_dispatcher: RwLock, + pub command_dispatcher: ArcSwap, /// Block behaviour. pub block_registry: Arc, /// Item behaviour. @@ -156,7 +156,7 @@ impl Server { ) -> Arc { let permission_registry = Arc::new(RwLock::new(PermissionRegistry::new())); // First register the default commands. After that, plugins can put in their own. - let command_dispatcher = RwLock::new( + let command_dispatcher = ArcSwap::from_pointee( default_dispatcher( &permission_registry, &basic_config, diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index d0a1b2e50..a333dbc8e 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -2714,7 +2714,7 @@ impl World { player.send_abilities_update().await; { - let command_dispatcher = server.command_dispatcher.read().await; + let command_dispatcher = server.command_dispatcher.load(); client_suggestions::send_bedrock_commands_packet(&player, server, &command_dispatcher) .await; }; @@ -3092,7 +3092,7 @@ impl World { // Difficulty of the world player.send_difficulty_update().await; { - let command_dispatcher = server.command_dispatcher.read().await; + let command_dispatcher = server.command_dispatcher.load(); client_suggestions::send_c_commands_packet(player, server, &command_dispatcher).await; };