aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0052-Ensure-commands-are-not-ran-async.patch
blob: f007b288e80f38bc76f3dafa56f0984433b5d83b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Thu, 3 Mar 2016 01:17:12 -0600
Subject: [PATCH] Ensure commands are not ran async

Plugins calling Player.chat("/foo") or Server.dispatchCommand() could
trigger the server to execute a command while on another thread.

These commands would then process EXPECTING to be on the main thread, leaving to
very hard to trace concurrency issues.

This change will synchronize the command execution back to the main thread, causing a
big slowdown in execution but throwing an exception at same time to raise awareness
that it is happening so that plugin authors can fix their code to stop executing commands async.

This also properly splits up the chat and command handling to reflect the server now
having separate packets for both, and the client always using the correct packet. Text
from a chat packet should never be parsed into a command, even if it starts with the `/`
character.

Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>

diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 37b03e45546429b4c86b44ea6e7718d04b071670..d7b522940dd062c81cd967773407ec7ab415ccb1 100644
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -2044,7 +2044,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
         return true;
     }
 
-    private static boolean isChatMessageIllegal(String message) {
+    public static boolean isChatMessageIllegal(String message) { // Paper - private -> public
         for (int i = 0; i < message.length(); ++i) {
             if (!SharedConstants.isAllowedChatCharacter(message.charAt(i))) {
                 return true;
@@ -2061,7 +2061,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
         }
         OutgoingChatMessage outgoing = OutgoingChatMessage.create(original);
 
-        if (!async && s.startsWith("/")) {
+        if (false && !async && s.startsWith("/")) { // Paper - don't handle commands in chat logic
             this.handleCommand(s);
         } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
             // Do nothing, this is coming from a plugin
@@ -2151,7 +2151,29 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
         }
     }
 
-    private void handleCommand(String s) {
+    public void handleCommand(String s) { // Paper - private -> public
+        // Paper Start
+        if (!org.spigotmc.AsyncCatcher.shuttingDown && !org.bukkit.Bukkit.isPrimaryThread()) {
+            LOGGER.error("Command Dispatched Async: " + s);
+            LOGGER.error("Please notify author of plugin causing this execution to fix this bug! see: http://bit.ly/1oSiM6C", new Throwable());
+            Waitable<Void> wait = new Waitable<>() {
+                @Override
+                protected Void evaluate() {
+                    ServerGamePacketListenerImpl.this.handleCommand(s);
+                    return null;
+                }
+            };
+            server.processQueue.add(wait);
+            try {
+                wait.get();
+                return;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt(); // This is proper habit for java. If we aren't handling it, pass it on!
+            } catch (Exception e) {
+                throw new RuntimeException("Exception processing chat command", e.getCause());
+            }
+        }
+        // Paper End
         co.aikar.timings.MinecraftTimings.playerCommandTimer.startTiming(); // Paper
         if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot
         this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s);
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 9da9b5b4b92996240cc93be2db82bf3a931e76c1..c84e32f53783b2e00b829ce839894c0a3a73325e 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -882,6 +882,28 @@ public final class CraftServer implements Server {
         Preconditions.checkArgument(commandLine != null, "commandLine cannot be null");
         org.spigotmc.AsyncCatcher.catchOp("command dispatch"); // Spigot
 
+        // Paper Start
+        if (!org.spigotmc.AsyncCatcher.shuttingDown && !Bukkit.isPrimaryThread()) {
+            final CommandSender fSender = sender;
+            final String fCommandLine = commandLine;
+            Bukkit.getLogger().log(Level.SEVERE, "Command Dispatched Async: " + commandLine);
+            Bukkit.getLogger().log(Level.SEVERE, "Please notify author of plugin causing this execution to fix this bug! see: http://bit.ly/1oSiM6C", new Throwable());
+            org.bukkit.craftbukkit.util.Waitable<Boolean> wait = new org.bukkit.craftbukkit.util.Waitable<Boolean>() {
+                @Override
+                protected Boolean evaluate() {
+                    return dispatchCommand(fSender, fCommandLine);
+                }
+            };
+            net.minecraft.server.MinecraftServer.getServer().processQueue.add(wait);
+            try {
+                return wait.get();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt(); // This is proper habit for java. If we aren't handling it, pass it on!
+            } catch (Exception e) {
+                throw new RuntimeException("Exception processing dispatch command", e.getCause());
+            }
+        }
+        // Paper End
         if (this.commandMap.dispatch(sender, commandLine)) {
             return true;
         }
diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 83a5a1d39892206ba69ece47aa20811b1d71709e..aa3a11a4869678f1fcb126bfde3381e0470c8246 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -484,7 +484,20 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
 
         if (this.getHandle().connection == null) return;
 
-        this.getHandle().connection.chat(msg, PlayerChatMessage.system(msg), false);
+        // Paper start - improve chat handling
+        if (ServerGamePacketListenerImpl.isChatMessageIllegal(msg)) {
+            this.getHandle().connection.disconnect(Component.translatable("multiplayer.disconnect.illegal_characters"));
+        } else {
+            if (msg.startsWith("/")) {
+                this.getHandle().connection.handleCommand(msg);
+            } else {
+                final PlayerChatMessage playerChatMessage = PlayerChatMessage.system(msg).withResult(new net.minecraft.network.chat.ChatDecorator.ModernResult(Component.literal(msg), true, false));
+                // TODO chat decorating
+                // TODO text filtering
+                this.getHandle().connection.chat(msg, playerChatMessage, false);
+            }
+        }
+        // Paper end
     }
 
     @Override
diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
index 19c44daaa407b7c1c7a7ffe56fef8c8814c6d5b2..6a073a9dc44d93eba296a0e18a9c7be8a7881725 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java
@@ -13,6 +13,7 @@ public class ServerShutdownThread extends Thread {
     public void run() {
         try {
             org.spigotmc.AsyncCatcher.enabled = false; // Spigot
+            org.spigotmc.AsyncCatcher.shuttingDown = true; // Paper
             this.server.close();
         } finally {
             try {
diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java
index 05e94702e42b8f5c35d2a112c486d57948a3acba..5409f230fdd53b70fc03c58177438534731ad4e6 100644
--- a/src/main/java/org/spigotmc/AsyncCatcher.java
+++ b/src/main/java/org/spigotmc/AsyncCatcher.java
@@ -6,6 +6,7 @@ public class AsyncCatcher
 {
 
     public static boolean enabled = true;
+    public static boolean shuttingDown = false; // Paper
 
     public static void catchOp(String reason)
     {
diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java
index 882e93ad4471e3688f2fcfb1e6f16926786ee5e7..94d8ba376cd1f024b244654cac9bb62bb19e3060 100644
--- a/src/main/java/org/spigotmc/RestartCommand.java
+++ b/src/main/java/org/spigotmc/RestartCommand.java
@@ -43,6 +43,7 @@ public class RestartCommand extends Command
     private static void restart(final String restartScript)
     {
         AsyncCatcher.enabled = false; // Disable async catcher incase it interferes with us
+        org.spigotmc.AsyncCatcher.shuttingDown = true; // Paper
         try
         {
             String[] split = restartScript.split( " " );